Skip to content
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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

vbarda
Copy link
Collaborator

@vbarda vbarda commented Dec 20, 2024

class WeatherResponse(BaseModel):
    """Respond to the user with this"""

    temperature: float = Field(description="The temperature in fahrenheit")
    wind_direction: str = Field(
        description="The direction of the wind in abbreviated form"
    )
    wind_speed: float = Field(description="The speed of the wind in mph")

@tool
def get_weather(city: Literal["nyc", "sf"]):
    """Use this to get weather information."""
    if city == "nyc":
        return "It is cloudy in NYC, with 5 mph winds in the North-East direction and a temperature of 70 degrees"
    elif city == "sf":
        return "It is 75 degrees and sunny in SF, with 3 mph winds in the South-East direction"
    else:
        raise AssertionError("Unknown city")

model = ChatOpenAI()
tools = [get_weather]
agent_with_structured_output = create_react_agent(model, tools, response_format=WeatherResponse)
agent_with_structured_output.invoke({"messages": [("user", "what's the weather in nyc?")]})
{
    'messages': [...],
    'structured_response': WeatherResponse(temperature=70.0, wind_directon='NE', wind_speed=5.0)
}

@eyurtsev
Copy link
Contributor

@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")
Copy link
Contributor

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.
Copy link
Contributor

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",
Copy link
Contributor

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,
Copy link
Contributor

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?

Copy link

@achrafmam2 achrafmam2 left a 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)

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:

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)

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".

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__"]:

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__")

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.

@jorgeutd
Copy link

❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants