-
Notifications
You must be signed in to change notification settings - Fork 63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Background task notifications #42
base: main
Are you sure you want to change the base?
Background task notifications #42
Conversation
Hmmm so here are my quick and candid thoughts on this:
It might also be that this is just too complex to include as a niche case in the main repo (unless we see more discussion / demand for it) and we shouldn't work too much harder to try to force it in. I hope this doesn't land too discouraging and sorry if so! |
Not discouraging at all :)
|
All makes sense to me. Using Thats an interesting point about o1 output and more general applicability. If you decide to open a langchain issue or discussion about this please do tag me in it / reference this! Will be interested to follow along. Re: an agent implementation - readme approach sounds fine, another idea would be to add a simple agent in a new file in agent/ which uses this, and instructions to update the service to point to that agent (a one line change I think?) to try it out. Then it would be easy to test and easy to extend when we have the multi-agent support. If you do that approach and it makes sense to move common model init etc to a shared file feel free. Thoughts? |
Just jumping in on the conversation of removing the ChatMessage (also mentioned in #41), I do think ChatMessage is a useful abstraction. It simplifies and encapsulates what the ChatMessage should be in the presentation layer. It makes that layer much cleaner, even if it makes the service a bit more convoluted. I think Langchain does not have a strong enough message abstraction as they can vary in structure inside depending on the model (as we see with the Anthropic models) and other context. Just my 2 cents. |
@antonioalegria I agree that Langchain's Message content field isn't the easiest to work with. The typing of However, personally, I still favor removing ChatMessage class. It seems to me more in line with the purpose of the repository to provide a starting point for anybody willing to implement their own AI agent with Langgraph. By removing this class, we are giving up on trying to solve the issue of Langchain's imperfect message abstraction layer and simply showing how one can use it to implement an end-to-end agent with some limited UI capabilities (surely we won't demonstrate handling of all possible variations of message That said, I believe the final decision rests with maintainer. @JoshuaC215 , please let us know how you decide. If you decide to remove the ChatMessage class, I think it would be best if you could open an issue for it, summarizing the reasons for the change. I think I would have some time next week to create a draft PR. Once we have this sorted out, I would finalize the background task notifications pull request. |
I wrote up the issue as requested, let me know what y'all think. |
# Conflicts: # src/agent/research_assistant.py # src/schema/__init__.py # src/schema/schema.py # src/service/service.py Rollback research_agent changes
Since we solved the chat message abstraction question, I resolved conflicts and added the "detailed_messages" state key. I have also added the simple version of the multiagent support as discussed in #28 , since I had it already developed. If you want to switch frontend to different backend agent, you need to switch URLs in client.py. Please review :) What remains is to write up Lanchain issue for new message type, I will reference this thread. |
src/client/client.py
Outdated
@@ -50,7 +50,7 @@ async def ainvoke( | |||
request.model = model | |||
async with httpx.AsyncClient() as client: | |||
response = await client.post( | |||
f"{self.base_url}/invoke", | |||
f"{self.base_url}/invoke/research-assistant", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Client should not be tied to the research-assistant, especially not in so many places, otherwise it will be harder to use for other agents we might implement.
The agent may be a path parameter. Also would make more sense to make this part of a separate changeset.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, removed multiagent implementation
src/schema/schema.py
Outdated
@@ -98,6 +104,25 @@ class ChatMessage(BaseModel): | |||
description="Original LangChain message in serialized form.", | |||
default={}, | |||
) | |||
task_name: str | None = Field( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to clean up ChatMessage, we could add a Task class with these fields and ChatMessage could have a task optional field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks appealing, but I think it breaks the proposed abstraction.
Task
class models a background task. As the task progresses (when it starts, finishes, or when it produces data during its execution), it emits TaskMessages, which (similar to how HumanMessages or AIMessages are handled) are then converted to ChatMessages, which serve as universal communication tool between server and client.
That said, I believe placing Task
itself into ChatMessage
is wrong, as this way ChatMessage
would contain a reference to background Task, which can progress during ChatMessage's lifetime. This contradicts ChatMessage's purpose to communicate a single notification produced by Task execution at given point in time.
src/schema/schema.py
Outdated
@@ -128,6 +153,17 @@ def from_langchain(cls, message: BaseMessage) -> "ChatMessage": | |||
original=original, | |||
) | |||
return tool_message | |||
case TaskMessage(): | |||
task_message = cls( | |||
type="task", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make TaskMessage have type=Task in the class definition, that way you don't need to pass the type here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this will work for two reasons:
- ChatMessage
type
is not the same as Langchain BaseMessagetype
. They are distinct fields. AIMessage, HumanMessage and ToolMessage have theirtype
set in the respective Langchain classes, but we still have to set ChatMessage's type manually as in these other cases. We could write everywhere:
type=message.type,
...
However, I am not sure of the value of this change.
- TaskMessage is based on Langchain's
ChatMessage
which hastype
already set tochat
. I couldn't base it on Langchain'sBaseMessage
, where I could settype
arbitrarily, becauseTaskMessage
needs the message to passAnyMessage
Langchain type checks, which Langchain and Langgraph rely on when working with messages. UsingBaseMessage
as base class would failAnyMessage
checks.
src/schema/schema.py
Outdated
|
||
role: str = "system_task" | ||
"""The role of the speaker.""" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
set type to Literal["task"] = "task"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussed above
src/service/service.py
Outdated
@@ -98,29 +99,50 @@ async def invoke(user_input: UserInput) -> ChatMessage: | |||
raise HTTPException(status_code=500, detail=str(e)) | |||
|
|||
|
|||
async def message_generator(user_input: StreamInput) -> AsyncGenerator[str, None]: | |||
@app.post("/invoke/research-assistant") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
research-assistant should not be in spread across the service code, only in the setup code (lifespan).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, removed multiagent implementation
I do think the multiagent code should be in a separate changeset, and the service and client code should be as unaware as possible as to what agent is setup. In the My suggestion is to move the multi-agent support to another issue/pull-request so that approaches could be discussed. |
Thanks for suggestions, multiagent implementation has been removed altogether, let's wait for progress on #28 . Instead, I added instructions to readme file. |
I left some thoughts on how to move this PR forward here I'm hoping I'll have some time soon (in the next week or so, maybe sooner) to work on some of the changes that will unblock this. |
Spent some more time looking at this. Here's a commit of how I'd want to roughly approach merging this after #75 is merged. Open to discussion. c5c7a2c#diff-b678273f64e8e9df8f3a41b23f9fba9ca9e4809b93b53843fb1c370a86e69110 ^ I tested this version and it seems to work! 🎉 I think we could do the changes in service.py in a generic way and maybe even put this in the main streamlit app (although I'm a bit hesitant about that 😅 for complexity reasons, but maybe its OK). One thing to call out, it wasn't obvious to me why we'd need the |
I checked the commit, thanks, looks great! We agreed on having For the toolkit's purpose, it probably makes sense to limit the use case to support only transient background task messages, which are not stored in history. In that case, neithed |
Sounds good - I guess when I was trying it I wasn't seeing the use case for Will revisit after #75 is merged in the next few days (hopefully). I want to get some review from other contributors since it's a big change. If you have any feedback on that PR (or other changes I made yesterday) please feel free to share! |
status.write("---") | ||
task_messages[task_data.run_id] = msg | ||
state = "complete" | ||
for message in task_messages.values(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
message
isn't actually used here. I was looking through this whole section of code and I'm not sure it works the way you intended - probably can use more cleanup
I merged all the pre-requisite changes including the general implementation for CustomData Here's my pass on adding a bg task agent: I didn't clean up the code in the streamlit app marked above ^ Feel free to adapt this PR / open a new one. Or we can just use my branch if you prefer. |
PR for #38 I chose the safety check as example implementation.
I have a few points I would like improve here, but let's first see if the general idea is sound:
research_agent.py
, I am not very happy that we would have to remember to remove task messages basically every time we want to use state["messages"] as model input. However, we need a place where all the messages including the Task messages are listed in order they were created, so that we can recreate the chat history including background task notifications.@JoshuaC215 please review the changes and let me know your thoughts.