-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
langgraph: add structured output to create_react_agent #2848
base: main
Are you sure you want to change the base?
Conversation
vbarda
commented
Dec 20, 2024
@vbarda one question with this approach -- are users going to need to control the prompt used for the model is used for extracting structured data (e.g., w/ few shot examples, further formatting instructions etc.)? Do we know whether this is good enough for the average case? |
): | ||
required_keys = {"messages", "remaining_steps"} | ||
if response_format is not None: | ||
required_keys.add("structured_response") |
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.
did we decide on structured_response vs. parsed? I'm OK with current
`response_format` requires the model to support `.with_structured_output` | ||
|
||
!!! Note | ||
The graph will make a separate call to the LLM to generate the structured response after the agent loop is finished. |
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.
Does it make sense to make a note that other strategies are possible? (We could note to a guide on how to customize? or can also just not worry about it)
if not tool_calling_enabled: | ||
# Define a new graph | ||
workflow = StateGraph(state_schema or AgentState) | ||
workflow.add_node("agent", RunnableCallable(call_model, acall_model)) | ||
workflow.set_entry_point("agent") | ||
if response_format is not None: | ||
workflow.add_node( | ||
"generate_structured_response", |
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.
could also use a shorter name like structure_response
-- or can have chatty g brain storm a 1-2 word name
@@ -201,6 +226,7 @@ def create_react_agent( | |||
state_schema: Optional[StateSchemaType] = None, | |||
messages_modifier: Optional[MessagesModifier] = None, | |||
state_modifier: Optional[StateModifier] = None, | |||
response_format: Optional[Union[dict, type[BaseModel]]] = None, |
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.
is there a unit test for this?
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.
Hi, my name is Achraf. I wanted to learn the codebase and thought of starting with looking at recent pull requests.
I don't mean to add extra work. Please consider all my comments as optional. If you think it adds noise please let me know so to stop.
) | ||
# NOTE: we exclude the last message because there is enough information | ||
# for the LLM to generate the structured response | ||
response = model_with_structured_output.invoke(state["messages"][:-1], config) |
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.
Is there a downside in sending the full history? I am worried that this could cause bugs in the future.
@@ -162,6 +174,19 @@ def _should_bind_tools(model: LanguageModelLike, tools: Sequence[BaseTool]) -> b | |||
return False | |||
|
|||
|
|||
def _get_model(model: LanguageModelLike) -> BaseChatModel: |
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.
Consider _unwrap_chat_model
or _extract_chat_model
or something similar that reflects the purpose of the function.
state: AgentState, config: RunnableConfig | ||
) -> AgentState: | ||
model_with_structured_output = _get_model(model).with_structured_output( | ||
cast(Union[dict, type[BaseModel]], response_format) |
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.
nit: Starting PEP604 one can use the shortcut dict | type[BaseModel]
.
By the way you may get away from using this cast by adding a defensive check.
assert response_format is not None, "Internal error: calling generate_structured_response when response_format is None".
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.
The |
operator is only supported in python 3.10+, never mind the suggestion.
@@ -643,12 +720,14 @@ async def acall_model(state: AgentState, config: RunnableConfig) -> AgentState: | |||
) | |||
|
|||
# Define the function that determines whether to continue or not | |||
def should_continue(state: AgentState) -> Literal["tools", "__end__"]: |
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.
Why not extend the Literal
this provides extra safety and readability.
generate_structured_response, agenerate_structured_response | ||
), | ||
) | ||
workflow.add_edge("generate_structured_response", "__end__") |
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.
Consider using the END
constant. This is both safer, and helps in future refactoring.
❤️ |