google / mesop

Rapidly build AI apps in Python
https://google.github.io/mesop/
Apache License 2.0
5.24k stars 248 forks source link

Concentric states causing issues during hot reload ("Tried to get the state instance for `ChatState`, but it's not a state class.") #545

Open dltn opened 2 months ago

dltn commented 2 months ago

Describe the bug I have two files: main.py (hosting my page) and chat.py (my component).

In each file, they use State and ChatState @me.stateclass, respectively:

# main.py

@me.stateclass
class State:
  attachment_path: str | None = None
  attachment_mime_type: str | None = None
# chat.py

@me.stateclass
class ChatState:
  input: str
  output: list[MesopMessage]
  in_progress: bool = False
  attachment_name: str
  attachment_size: int
  attachment_mime_type: str
  attachment_contents: str

If I edit main.py, hot reload works fine. If I edit chat.py, I get the following error:

**Mesop Developer Error:** Tried to get the state instance for `ChatState`, but it's not a state class.

Did you forget to decorate your state class `ChatState` with @stateclass?
**Mesop Developer Error:** Tried to get the state instance for `ChatState`, but it's not a state class.

Did you forget to decorate your state class `ChatState` with @stateclass?

and need to restart the server.

Button components and others have State inside ("concentric" state), so I'm not sure why this wouldn't be possible. I'm led to believe https://google.github.io/mesop/guides/troubleshooting/ should have information about this, but it's currently 404'ing, and the only snapshot on archive.org says the state must be serializable (which it is).

Expected behavior Hot reload will reload chat.py without state errors.

goran-markovic85 commented 2 months ago

Similar setup same error: Did you forget to decorate your state classStatewith @stateclass? **Mesop Developer Error:** Tried to get the state instance forState`, but it's not a state class.

Did you forget to decorate your state class State with @stateclass?`

wwwillchen commented 2 months ago

@dltn in your example ChatState is not annotated with @me.stateclass, but it should. Let me know if that solves your issue.

@goran-markovic85 - similar for your use case, I think you need to annotate your state class.

dltn commented 2 months ago

Whoops @wwwillchen, it is annotated @me.stateclass, I just missed it when copying into the issue.

dltn commented 2 months ago

After updating to 0.9.2 and picking up the changes in #520, I'm getting the new Exception

Unhandled stateclass deserialization where key=content, value=draw x=1, instance=MesopMessage(role='user')

which seems related?

dltn commented 2 months ago

Ah, yes, my issue is that I'm trying to use separate State in two different files. If I debug here, I can see an attempt to instantiate State with JSON data from ChatState:

Unhandled stateclass deserialization where key=input, value=, instance=State(attachment_path=None, attachment_mime_type=None)

State(attachment_path=None, attachment_mime_type=None)

{'input': '', 'output': [{'role': 'user', 'type': 'text', 'content': 'hi'}, {'role': 'assistant', 'type': 'text', 'content': 'Hi! How can I assist you today?'}], 'in_progress': False, 'attachment_name': '', 'attachment_size': 0, 'attachment_mime_type': '', 'attachment_contents': ''}
dltn commented 2 months ago

Resolved by only using one State object for my page. (My intuition was to expect React behavior, where State is scoped to its component/module.)

wwwillchen commented 2 months ago

You should be able to have multiple stateclasses scoped to different modules, so it does seem like there's a bug. @richard-to - maybe this is related to your recent state diff changes?

richard-to commented 2 months ago

It's possible there could be a regression with the state diffing changes. I'll check that out when I get the chance probably tomorrow.

My other thought is perhaps there's an issue with the external hot-reloading since it does work when it's in the main file. but not in a different file.

So one test could be to put everything in chat.py into main.py. In that scenario do you still get the ChatState error?

richard-to commented 2 months ago

In terms of the deserialization issue in 0.9.2, I think we had a change in our de-serialization code in https://github.com/google/mesop/pull/520

richard-to commented 2 months ago

I tried to reproduce the issue with a simple example. I'm using 0.9.2. But couldn't reproduce it.

So I think we first need to find the simplest example that can reproduce the issue. Is it any change to chat.py that will cause the issue?

app.py

import mesop as me

import chat

@me.stateclass
class State:
  text: str = "Text"

@me.page(path="/")
def page():
  state = me.state(State)
  me.text(state.text)
  chat.component()

chat.py

import mesop as me

@me.stateclass
class State:
  text: str = "Text 2"

def component():
  state = me.state(State)
  me.text(state.text)
russell commented 2 months ago

I'm having the same issue, @dltn can you tell me if you have the files in the same directory as you are executing mesop from?

because i think it's related to using subdirectiories, i can reproduce it if i use subdirectories

mkdir nested
mv app.py chat.py nested
mesop nested/app.py

to test this i am printing the runtime registered state objects

nested/app.py

import mesop as me
from mesop.runtime import runtime

import chat

@me.stateclass
class State:
  text: str = "Text"

@me.page(path="/")
def page():
  me.text("APP State:" + str(runtime()._state_classes))
  chat.component()

nested/chat.py

import mesop as me
from mesop.runtime import runtime

@me.stateclass
class State:
  text: str = "Text 2"

def component():
  me.text("Chat State: " + str(runtime()._state_classes))

execution

poetry run mesop nested/app.py

when i first run the application the page shows

APP State:[<class 'chat.State'>, <class '.Users.rsim.Code.test-mesop.nested.app.State'>]
Chat State: [<class 'chat.State'>, <class '.Users.rsim.Code.test-mesop.nested.app.State'>]

when i edit app.py and reload the page after the hot reload

APP State:[<class '.Users.rsim.Code.test-mesop.nested.app.State'>]
Chat State: [<class '.Users.rsim.Code.test-mesop.nested.app.State'>]

Fix

  1. create a toplevel entry point
$ cat run.py
import nested.app
  1. create a module by adding an __init__.py

    touch nested/__init__.py
  2. use relative imports

    
    --- /var/folders/s1/ly7d_m5n1tb9d9tp1zfn23d00000gq/T/buffer-content-Wdejuz  2024-06-27 10:33:45
    +++ /var/folders/s1/ly7d_m5n1tb9d9tp1zfn23d00000gq/T/buffer-content-lD44Hz  2024-06-27 10:33:45
    @@ -1,7 +1,7 @@
    import mesop as me
    from mesop.runtime import runtime

-import chat +from . import chat

@me.stateclass class State:


## Final layout

run.py nested/init.py nested/chat.py nested/app.py


### Execution output

APP State:[<class 'nested.chat.State'>, <class 'nested.app.State'>] Chat State: [<class 'nested.chat.State'>, <class 'nested.app.State'>]



# Addendum 

When i went to implement this in my own project i realised that there is a connection to the variable `mesop.bin.bin.app_modules` and the hot reload logic.  The hot loading issue is actually specifically a hot unloading issue. so it's that it never actually removes the loaded code.  A debug flag to print the code being unloaded would help with debugging, since the reset runtime state will be invalid if the modules aren't unloaded correctly.
richard-to commented 2 months ago

Thanks for the great debugging work, Russell. That's really helpful to know. Yes, I think our external hot reloading functionality needs more work since for core development, the hot reloading works differently than from the pip package, so we haven't stress tested the external hot reloading as much as we would like. So it's great to get these bug reports.

dltn commented 2 months ago

@russell

@dltn can you tell me if you have the files in the same directory as you are executing mesop from?

Correct, I have main.py and chat.py in the same directory