Eigenbahn / ai-dungeon-cli

:european_castle: A cli client to play.aidungeon.io
MIT License
150 stars 34 forks source link

Port to newest API #23

Closed MatusGoc closed 3 years ago

MatusGoc commented 4 years ago

Sorry if this bothers you, I got an error and I think this is the best place to report it. Included the error log. AIDCLI.log

p3r7 commented 4 years ago

Hello,

Indeed we were relying on a soon-to-be-deprecated API (see latest release notes).

It seems that they have finally decommissioned it. Thanks for logging the issue.

Last version of play.aidungeon.io has moved from a REST API to a GraphQL one. There is some work needed to port the code to use the later.

I was reluctant to be too quick to adapt my code as AI Dungeon was in a big rework phase and changes would happen daily. So I was waiting for the API to stabilize.

I need to see how stable things are now and port the code.

otreblan commented 4 years ago

Any progress?

p3r7 commented 4 years ago

Welp, I've seen the API change several time.

They were quite busy testing different things.

The current version is built using GraphQL (but a different format from what I observed before).

The most drastic change: websocket (a persistent connection) is now used. This complexify reading the packets being exchanged as they get "poluted" w/ all the HTTP session keepalive mechanism...

I have a few experience doing GrahQL under python (w/ gql) but never did websocket in this language.

So, it's doable, but not as easy as previously...

EDIT: it seems like gql recently added support for websocket connections in the alpha version of their next release (v3.0.0). Sadly, the later is not yet on PyPI. I guess it would be safer to wait for this release of gql to drop to prevent BC breaks.

p3r7 commented 3 years ago

Just a small update.

I've been playing around w/ latest gql version.

I managed to port most of the code to the newest API. I just have some sort of bug where I can't seem to get the rest of the story when sending the first action. This has to do w/ async handling of queries / responses on a same websocket connection.

So, yeah, I'm pretty close to having something working.

mkualquiera commented 3 years ago

Hello! Me and a friend are also working on making a python API for the new version. There seems to be some overlap with what you're doing, and we're currently facing some issues ourselves... Is there any chance you could share your current experimental code? This way we could probably have some faster progress :D Thanks a lot!

p3r7 commented 3 years ago

Yeah sure. Lemme create a branch with what I have thus far.

I will also post a snippet of the part on which I'm stuck.

I was busy during the last few days so didn't make much progress.

p3r7 commented 3 years ago

There you go: v4.x

How to read:

Please note the following:

p3r7 commented 3 years ago

And now for a snippet of what I'm struggling with.

The code runs fine w/ all the mode / character selection and displaying the initial story pitch. It's when we try to send the first action that things get a tad more tricky as we need to deal w/ asynchronicity.

The client first sends this message (#23) to the server. It's a subscription, which means "notify me later, asynchronously", in occurrence with the next part of the story.

image

It then sends the user input (message #31) and gets the response for this request synchronously, in addition with the result of subscription #23..

image

Now, I tried to apply the example code from the gql doc (https://github.com/graphql-python/gql#websockets-async-transport):

import os
import sys
import asyncio
from gql import gql, Client, WebsocketsTransport

# retrieve this in the first websocket query message 
access_token = "f65542b0-cbea-11ea-8520-8335fafc2293"
# retrieve this in later messages once options have been selected
adventure_id = "adventure:27695673"

## ------------------------------------------------------------------------

DEBUG = True

def debug_print(msg):
    if DEBUG:
        print(msg)

def debug_pprint(msg):
    if DEBUG:
        print(msg)

## ------------------------------------------------------------------------

async def main():

    transport = WebsocketsTransport(url='wss://api.aidungeon.io/subscriptions',
                                    init_payload={'token': access_token})

    # Using `async with` on the client will start a connection on the transport
    # and provide a `session` variable to execute queries on this connection
    async with Client(
            transport=transport,
            fetch_schema_from_transport=True,
    ) as session:

        # basically, message #23 from above screenshots
        subscription = gql('''
        subscription subscribeContent($id: String) {  subscribeContent(id: $id) {    id    historyList    quests    error    memory    mode    died    actionLoading    characters {      id      userId      name      __typename    }    gameState    thirdPerson    __typename  }}
        ''')
        subscription_params = {
            "id": adventure_id
        }

        debug_print("subscribe to rest of story")

        async for result in session.subscribe(subscription,
                                               variable_values=subscription_params):
             debug_print("got response")
             debug_print(result)

        # basically, message #31 from above screenshots
        query_send = gql('''
        mutation ($input: ContentActionInput) {  sendAction(input: $input) {    id    actionLoading    memory    died    gameState    __typename  }}
        ''')
        query_params_send = {
            "input": {
                "type": "do",
                "text": "Play the ball",
                "id": adventure_id
            }
        }

        debug_print("send user input")
        result = await session.execute(query_send,
                                       variable_values=query_params_send)
        debug_print(result)

asyncio.run(main())

print('after loop declaration')

But the code gets stuck at the session.subscribe().

I don't know ape chimp about the coroutine syntax provided by asyncio in python but their example code seems kinda fishy. For starters, nested async blocks (which I assume are coroutine block delimiters) seems weird and here we get 3 whole levels!

I didn't take the time to read more about async / await does but that's on my TODO list (I suspect await is kinda like a deref in clojure, requesting the blocking/synchronous eval of a result computed asynchronously). If in the mean time any of you has prior experience with this syntax and can clearly spot what goes wrong, that'd help.

Devon7925 commented 3 years ago

Just making sure you got permission from the devs for this. They had trouble with people ddosing them before using the api.

sasha00123 commented 3 years ago

@p3r7 I have managed to write a working telegram/vk client last night with asyncio and python-gql also. But I didn't use subscribe, only execute. I used to try subscriptions with another library, but the code became too messy. I will try to share my code soon. It implements only some minimal functionality but might be a good starting point.

mkualquiera commented 3 years ago

@importnetminecraft Well, there would be no need for this if an official, public API was available :c

Devon7925 commented 3 years ago

@1Macho Can't really blame them for not wanting to spend time on documentation, especially with how often they add new features. You can also upvote this for that: https://aidungeon.featureupvote.com/suggestions/93028/send-and-receive-from-post-get

p3r7 commented 3 years ago

@importnetminecraft

A lib could indeed be used to build a nefarious script, generating a high request load.

But it's not like their API is a closed system, you can't protect yourself by asking politely "don't". There are design principles and countermeasures to mitigate a DDoS effect (rate limiting by IP origination, nonces, blocking via live audit rules ...). The attack surface is already there and people with the resources to launch an effective DDoS wouldn't be too desperate to have to do their own rev eng.

Anyway, if you've got any contact, I'm 100% OK with asking :smiley:.

@sasha00123 glad to hear. I guess gql, albeit the official GraphQL lib for python isn't necessarilly the best choice as of now for this use-case. I guess your code could really help.

@1Macho & @importnetminecraft welp the initial API on which this project was initially relying on was a pretty clean REST API. I don't think they'll go back to this design now that they focused on GraphQL / websockets. They completely overhauled their access layer (purposely w/ auto-scaling in mind) and having a separate access w/ traditional HTTP would be quite cumbersome.

Devon7925 commented 3 years ago

@p3r7 There are two easy ways to contact the devs. One is through the email support@aidungeon.io and the other is through discord.

p3r7 commented 3 years ago

@importnetminecraft I've joined the discord but it's basically alien territory to me. Do you know any guy I could DM?

Devon7925 commented 3 years ago

Nick(The Creator) is Nick Walton#5842 He's offline right now so it may take a bit for him to reply

sasha00123 commented 3 years ago

@p3r7 I have just created a repository so you can look at the code. https://github.com/sasha00123/ai-dungeon-bot

An interesting part for you is here: https://github.com/sasha00123/ai-dungeon-bot/blob/master/src/ai_dungeon/ai_dungeon_api/main.py

I didn't yet implement anything but basic functionality: starting a new game from scenarios and do/say/story actions and answers (+ translations and Redis storage, but It doesn't really have anything to do with your project). But at least it works and writes stories.)))

mkualquiera commented 3 years ago

I'm currently having issues with an UNAUTHENTICATED error... I have a function that looks like this

    def request_is_loading(self):
        result = self.client.request(
            '''
            query ($id: String) {
                content(id: $id) {
                    actionLoading
                }
            }
            ''',
            variables = {
                'id':'adventure:{}'.format(self.id)
            }
        )
        return result

But I get this error gql.transport.exceptions.TransportQueryError: {'message': 'Not authorized.', 'locations': [{'line': 2, 'column': 3}], 'path': ['content'], 'extensions': {'code': 'UNAUTHENTICATED'}} Is there anything special I need to do before...?

sasha00123 commented 3 years ago

Is there anything special I need to do before...?

Did you initialize self.client with an init_payload containing token?

mkualquiera commented 3 years ago

I figured it out :D I was trying it with a multiplayer adventure, and you first have to send a mutation like:

mutation ($adventurePlayPublicId: String) {
    addUserToAdventure(adventurePlayPublicId: $adventurePlayPublicId)
}
otreblan commented 3 years ago

I made a new aur package that builds from the v4.x branch https://aur.archlinux.org/packages/ai-dungeon-cli-gql-git/ Use python-gql-git, the normal one is outdated.

p3r7 commented 3 years ago

Alright, basic functionalities now working.

Kudos to @sasha00123 for the help and his very clear and well documented code. Basically the trick is to not require a subscription of the continuation of the story but alter the query to do it synchronously (link to his code). This replaces query #23 from my screenshots.

I was blinded by the fact that I wanted to be accurate to the web app client behavior.

@importnetminecraft discussion about asking official devs moved to https://github.com/Eigenbahn/ai-dungeon-cli/issues/25 to not have 2 interleaved discussions.

@otreblan Ok, nice for those who would want to try the ongoing changes. The v4.x branch will get merged into the master/main/whatever branch in a few days anyway so this aur package will be very ephemeral.

p3r7 commented 3 years ago

Next stuff to backport, in order:

New stuff to implement:

sasha00123 commented 3 years ago

By the way, I have just found out that fetch_schema_from_transport=True, in instantiating WebSocket connection is not necessary, but may slow down operations due to extra queries.

About backporting stuff: For custom - it's basically creating scenario:458625 with null prompt, while the next action should be a story. Maybe you can also send a prompt to that scenario, didn't try yet. For archive - I don't really know what is the difference with that mode. Could you hint me with that?

p3r7 commented 3 years ago

Thanks @sasha00123, I got that figured out. I had to reorganize a bit control flow of the code to handle this use-case as previously the API handled it differently.

As for the archive mode, IIRC it's just that we get additional prompts to select options.

As for the fetch_schema_from_transport=True if it's useless and degrades performance I should remove it as well. Thanks for the tip.

p3r7 commented 3 years ago

Got everything back into working order.

I guess I will merge into the master and do a tag / release.

There's still stuff I don't like so much in my code (method names, missing accessors, the need to split into several files for legibility) but for now that'll do.

mkualquiera commented 3 years ago

Hello! This is a link to my implementation of the library. https://github.com/1Macho/AIDungeonAPI I focused mostly on multiplayer adventures, but only with one player (weird stuff, yes, but the project I was working on required it). It could probably be helpful in some way nonetheless. This is an example of how I use it:

    aidc = await AIDungeonClient(debug=True)
    adventure = await aidc.get_adventure(public_adventure_id)
    await adventure.set_character_name("The user")
p3r7 commented 3 years ago

Branch v4.x merged into master. New release: 0.4.0 already available to install through pip.

@1Macho, @sasha00123 thanks for your lib implementations. They are objectively better (slimmer, better method names, better commented) than the one I patched together quickly in ai-dungeon-cli. Broader to the scope of this project, I guess it could be interesting to have a common python API client lib implementation. What do you think about this?

sasha00123 commented 3 years ago

I think @1Macho 's implementation seems the most mature now, so we can potentially rely on it and help improving it if he doesn't mind.

mkualquiera commented 3 years ago

It would be an honor. Feel free to contribute and tell me what I can improve :D

p3r7 commented 3 years ago

I guess I can close this ticket.

New ticket about relying on an external lib for the API calls: https://github.com/Eigenbahn/ai-dungeon-cli/issues/29

Ticket about multiplayer: https://github.com/Eigenbahn/ai-dungeon-cli/issues/30

Thanks for your support & encouragement guys :beers: