-
-
Notifications
You must be signed in to change notification settings - Fork 195
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
State machine for Assistant's execution phases #677
State machine for Assistant's execution phases #677
Conversation
|
||
# TODO: Should we return the final state along with the messages? |
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 think it's okay to skip for now. But I think the developer should be able to access it with:
assistant.state
#=> :in_progress
What do you think?
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.
Yes, adding a state
attribute to the assistant could be a valid solution without modifying the return value of the method.
A note: In synchronous execution, assistant.state
will always return the final state (:completed
, :failed
, or :expired
) because during the run
method, you cannot access the attribute. Therefore, you will never see :in_progress
or :running_tools
. Correct?
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.
Wouldn't you see the :running_tools
(:requires_action
)? For example:
irb(main):019> a.add_message content:"Latests news in politics?"
=>
[#<Langchain::Messages::OpenAIMessage:0x000000011f1de708 @content="You are a news reporter", @role="system", @tool_call_id=nil, @tool_calls=[]>,
#<Langchain::Messages::OpenAIMessage:0x00000001202b2ae8 @content="Latests news in politics?", @role="user", @tool_call_id=nil, @tool_calls=[]>]
irb(main):020> a.run auto_tool_execution: false
I, [2024-06-25T13:10:51.775951 #77146] INFO -- : [Langchain.rb] [Langchain::Assistant]: Sending a call to Langchain::LLM::OpenAI
=>
[#<Langchain::Messages::OpenAIMessage:0x000000011f1de708 @content="You are a news reporter", @role="system", @tool_call_id=nil, @tool_calls=[]>,
#<Langchain::Messages::OpenAIMessage:0x00000001202b2ae8 @content="Latests news in politics?", @role="user", @tool_call_id=nil, @tool_calls=[]>,
#<Langchain::Messages::OpenAIMessage:0x000000012011a370
@content="",
@role="assistant",
@tool_call_id=nil,
@tool_calls=
[{"id"=>"call_MnJgY7DHwfPT1yPXRM1KRF1H",
"type"=>"function",
"function"=>{"name"=>"news_retriever__get_top_headlines", "arguments"=>"{\"category\":\"general\",\"q\":\"politics\",\"page_size\":5}"}}]>]
irb(main):021>
At this point the state
would be :running_tools
(or :requires_action
), correct?
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.
No, as you can see in handle_llm_message
, if auto tool execution is disabled, the return value is currently :completed
. This is necessary to exit the loop (see run_finished?
).
if last_message.tool_calls.any?
auto_tool_execution ? :running_tools : :completed
else
As written in the TODO notes, I was thinking to implement a new state (like :requires_manual_action
). Or we should find a way to exit while keeping the state requires_action
. What do you think?
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.
@dghirardo My gut feeling is to simplify, so less states. Do you agree? It's much easier to add new states than to try to simplify it later.
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.
One last question. When the assistant is initialized for the first time, what will state
be set to? Nil?
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.
Good question. Either "new" or "initialized" or "ready". What do you think?
case state | ||
when :in_progress | ||
process_latest_message(auto_tool_execution) | ||
when :running_tools |
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'm wondering if we should just copy how OpenAI calls it and name it :requires_action
to be more aligned with that interface?
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.
Yes, sure, I agree!
when :running_tools | ||
execute_tools | ||
else | ||
Langchain.logger.error("Unexpected state encountered: #{state}") |
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.
What are some examples of when the execution would go 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 removed it. There can't be any other state different from the ones we have decided.
# @return [Symbol] The next state | ||
def execute_tools | ||
# TODO: Should we create a method parameter to let the user change the value of the tool timeout? | ||
Timeout.timeout(600) { run_tools(thread.messages.last.tool_calls) } |
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 is 600 seconds so 10 minutes, right? I was thinking of starting very lean and not putting these kinds of guardrails just yet.
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.
Yes, it's 10 minutes. I took inspiration from OpenAI's assistant, which does the same. If you confirm, I can remove this check and the :expired
state as well.
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.
Yep, let's get rid of it!
@dghirardo First off -- thank you for this PR! 🎉❤️ I left you a few comments but I had pulled it out, tested it out, and it was looking pretty good! |
@dghirardo I think I just realized our slight misunderstanding here. The way I was thinking about this is:
The developer has an option to execute the tools manually and submit the output if they want to, e.g.: |
Hi @andreibondarev, I just made a commit with the latest changes we discussed in the past few days. We should be good now! |
@dghirardo Wow, amazing work! |
Issue #566
This pull request introduces a simplified state machine for managing the Assistant's execution phases. Inspired by the OpenAI Assistants model, this leaner approach enhances code readability and simplifies debugging.
Changes:
in_progress
,running_tools
,completed
,failed
, andexpired
.case
statements for state management to improve code clarity and avoid nestedif
statements.standard_role
method for consistent message type handling across different LLMs, enablingcase
statements inprocess_latest_message
to manage the execution flow based on the role of the last message.expired
status similar to OpenAI, transitioning to expired if tool output is not provided within 10 minutes to prevent application hang.About implementation:
After analyzing OpenAI's approach, I believe a specialized state machine gem is over-engineered for our needs. OpenAI initializes a thread and creates a job for each run, allowing state access during execution, which is ideal for asynchronous processing. In our synchronous context, a simpler state machine within the run method is sufficient, reducing complexity while maintaining clear state management.
Notes:
I have left several TODO notes in the code for further improvements and would appreciate your feedback on these points. I am open to discussing the implementation and any potential improvements you might suggest. Thank you!