From 50eea98fc67cd22ad2600c6bf9d305ea77eaa2e0 Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Fri, 2 Aug 2024 12:18:34 -0400 Subject: [PATCH] langgraph: remove deprecations and add new warnings (#1196) * langgraph: remove deprecations and add new warnings --- libs/langgraph/langgraph/_api/deprecation.py | 64 +++++-- libs/langgraph/langgraph/prebuilt/__init__.py | 4 - .../langgraph/prebuilt/agent_executor.py | 181 ------------------ .../langgraph/prebuilt/chat_agent_executor.py | 143 +------------- .../langgraph/prebuilt/tool_executor.py | 4 + libs/langgraph/tests/test_pregel.py | 135 ------------- libs/langgraph/tests/test_pregel_async.py | 133 ------------- 7 files changed, 53 insertions(+), 611 deletions(-) delete mode 100644 libs/langgraph/langgraph/prebuilt/agent_executor.py diff --git a/libs/langgraph/langgraph/_api/deprecation.py b/libs/langgraph/langgraph/_api/deprecation.py index 6ce811f90..6fa419e83 100644 --- a/libs/langgraph/langgraph/_api/deprecation.py +++ b/libs/langgraph/langgraph/_api/deprecation.py @@ -1,6 +1,6 @@ import functools import warnings -from typing import Any, Callable, TypeVar, cast +from typing import Any, Callable, Type, TypeVar, Union, cast class LangGraphDeprecationWarning(DeprecationWarning): @@ -8,31 +8,57 @@ class LangGraphDeprecationWarning(DeprecationWarning): F = TypeVar("F", bound=Callable[..., Any]) +C = TypeVar("C", bound=Type[Any]) def deprecated( since: str, alternative: str, *, removal: str = "", example: str = "" ) -> Callable[[F], F]: - def decorator(func: F) -> F: - @functools.wraps(func) - def wrapper(*args: Any, **kwargs: Any) -> Any: - removal_str = removal if removal else "a future version" - message = ( - f"{func.__name__} is deprecated as of version {since} and will be" - f" removed in {removal_str}. Use {alternative} instead.{example}" - ) - warnings.warn(message, LangGraphDeprecationWarning, stacklevel=2) - return func(*args, **kwargs) - - docstring = ( - f"**Deprecated**: This function is deprecated as of version {since}. " - f"Use `{alternative}` instead." + def decorator(obj: Union[F, C]) -> Union[F, C]: + removal_str = removal if removal else "a future version" + message = ( + f"{obj.__name__} is deprecated as of version {since} and will be" + f" removed in {removal_str}. Use {alternative} instead.{example}" ) - if func.__doc__: - docstring = docstring + f"\n\n{func.__doc__}" - wrapper.__doc__ = docstring + if isinstance(obj, type): + original_init = obj.__init__ - return cast(F, wrapper) + @functools.wraps(original_init) + def new_init(self, *args: Any, **kwargs: Any) -> None: + warnings.warn(message, LangGraphDeprecationWarning, stacklevel=2) + original_init(self, *args, **kwargs) + + obj.__init__ = new_init + + docstring = ( + f"**Deprecated**: This class is deprecated as of version {since}. " + f"Use `{alternative}` instead." + ) + if obj.__doc__: + docstring = docstring + f"\n\n{obj.__doc__}" + obj.__doc__ = docstring + + return cast(C, obj) + elif callable(obj): + + @functools.wraps(obj) + def wrapper(*args: Any, **kwargs: Any) -> Any: + warnings.warn(message, LangGraphDeprecationWarning, stacklevel=2) + return obj(*args, **kwargs) + + docstring = ( + f"**Deprecated**: This function is deprecated as of version {since}. " + f"Use `{alternative}` instead." + ) + if obj.__doc__: + docstring = docstring + f"\n\n{obj.__doc__}" + wrapper.__doc__ = docstring + + return cast(F, wrapper) + else: + raise TypeError( + f"Can only add deprecation decorator to classes or callables, got '{type(obj)}' instead." + ) return decorator diff --git a/libs/langgraph/langgraph/prebuilt/__init__.py b/libs/langgraph/langgraph/prebuilt/__init__.py index 615de4c7b..4ad055730 100644 --- a/libs/langgraph/langgraph/prebuilt/__init__.py +++ b/libs/langgraph/langgraph/prebuilt/__init__.py @@ -1,14 +1,10 @@ """langgraph.prebuilt exposes a higher-level API for creating and executing agents and tools.""" -from langgraph.prebuilt import chat_agent_executor -from langgraph.prebuilt.agent_executor import create_agent_executor from langgraph.prebuilt.chat_agent_executor import create_react_agent from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation from langgraph.prebuilt.tool_node import InjectedState, ToolNode, tools_condition from langgraph.prebuilt.tool_validator import ValidationNode __all__ = [ - "create_agent_executor", - "chat_agent_executor", "create_react_agent", "ToolExecutor", "ToolInvocation", diff --git a/libs/langgraph/langgraph/prebuilt/agent_executor.py b/libs/langgraph/langgraph/prebuilt/agent_executor.py deleted file mode 100644 index c8b541a1b..000000000 --- a/libs/langgraph/langgraph/prebuilt/agent_executor.py +++ /dev/null @@ -1,181 +0,0 @@ -import operator -from typing import Annotated, Sequence, TypedDict, Union - -from langchain_core.agents import AgentAction, AgentFinish -from langchain_core.messages import BaseMessage - -from langgraph._api.deprecation import deprecated -from langgraph.graph import END, StateGraph -from langgraph.graph.state import CompiledStateGraph -from langgraph.prebuilt.tool_executor import ToolExecutor -from langgraph.utils import RunnableCallable - - -def _get_agent_state(input_schema=None): - if input_schema is None: - - class AgentState(TypedDict): - # The input string - input: str - # The list of previous messages in the conversation - chat_history: Sequence[BaseMessage] - # The outcome of a given call to the agent - # Needs `None` as a valid type, since this is what this will start as - agent_outcome: Union[AgentAction, AgentFinish, None] - # List of actions and corresponding observations - # Here we annotate this with `operator.add` to indicate that operations to - # this state should be ADDED to the existing values (not overwrite it) - intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add] - - else: - - class AgentState(input_schema): - # The outcome of a given call to the agent - # Needs `None` as a valid type, since this is what this will start as - agent_outcome: Union[AgentAction, AgentFinish, None] - # List of actions and corresponding observations - # Here we annotate this with `operator.add` to indicate that operations to - # this state should be ADDED to the existing values (not overwrite it) - intermediate_steps: Annotated[list[tuple[AgentAction, str]], operator.add] - - return AgentState - - -@deprecated( - "0.0.44", - alternative="create_react_agent", - removal="0.2.0", - example=""" -from langgraph.prebuilt import create_react_agent - -create_react_agent(...) -""", -) -def create_agent_executor( - agent_runnable, tools, input_schema=None -) -> CompiledStateGraph: - """This is a helper function for creating a graph that works with LangChain Agents. - - Args: - agent_runnable (RunnableLike): The agent runnable. - tools (list): A list of tools to be used by the agent. - input_schema (dict, optional): The input schema for the agent. Defaults to None. - - Returns: - The `CompiledStateGraph` object. - - - Examples: - - # Since this is deprecated, you should use `create_react_agent` instead. - # Example usage: - from langgraph.prebuilt import create_react_agent - from langchain_openai import ChatOpenAI - from langchain_community.tools.tavily_search import TavilySearchResults - - tools = [TavilySearchResults(max_results=1)] - model = ChatOpenAI() - - app = create_react_agent(model, tools) - - inputs = {"messages": [("user", "what is the weather in sf")]} - for s in app.stream(inputs): - print(list(s.values())[0]) - print("----") - """ - - if isinstance(tools, ToolExecutor): - tool_executor = tools - else: - tool_executor = ToolExecutor(tools) - - state = _get_agent_state(input_schema) - - # Define logic that will be used to determine which conditional edge to go down - - def should_continue(data): - # If the agent outcome is an AgentFinish, then we return `exit` string - # This will be used when setting up the graph to define the flow - if isinstance(data["agent_outcome"], AgentFinish): - return "end" - # Otherwise, an AgentAction is returned - # Here we return `continue` string - # This will be used when setting up the graph to define the flow - else: - return "continue" - - def run_agent(data, config): - agent_outcome = agent_runnable.invoke(data, config) - return {"agent_outcome": agent_outcome} - - async def arun_agent(data, config): - agent_outcome = await agent_runnable.ainvoke(data, config) - return {"agent_outcome": agent_outcome} - - # Define the function to execute tools - def execute_tools(data, config): - # Get the most recent agent_outcome - this is the key added in the `agent` above - agent_action = data["agent_outcome"] - if not isinstance(agent_action, list): - agent_action = [agent_action] - output = tool_executor.batch(agent_action, config, return_exceptions=True) - return { - "intermediate_steps": [ - (action, str(out)) for action, out in zip(agent_action, output) - ] - } - - async def aexecute_tools(data, config): - # Get the most recent agent_outcome - this is the key added in the `agent` above - agent_action = data["agent_outcome"] - if not isinstance(agent_action, list): - agent_action = [agent_action] - output = await tool_executor.abatch( - agent_action, config, return_exceptions=True - ) - return { - "intermediate_steps": [ - (action, str(out)) for action, out in zip(agent_action, output) - ] - } - - # Define a new graph - workflow = StateGraph(state) - - # Define the two nodes we will cycle between - workflow.add_node("agent", RunnableCallable(run_agent, arun_agent)) - workflow.add_node("tools", RunnableCallable(execute_tools, aexecute_tools)) - - # Set the entrypoint as `agent` - # This means that this node is the first one called - workflow.set_entry_point("agent") - - # We now add a conditional edge - workflow.add_conditional_edges( - # First, we define the start node. We use `agent`. - # This means these are the edges taken after the `agent` node is called. - "agent", - # Next, we pass in the function that will determine which node is called next. - should_continue, - # Finally we pass in a mapping. - # The keys are strings, and the values are other nodes. - # END is a special node marking that the graph should finish. - # What will happen is we will call `should_continue`, and then the output of that - # will be matched against the keys in this mapping. - # Based on which one it matches, that node will then be called. - { - # If `tools`, then we call the tool node. - "continue": "tools", - # Otherwise we finish. - "end": END, - }, - ) - - # We now add a normal edge from `tools` to `agent`. - # This means that after `tools` is called, `agent` node is called next. - workflow.add_edge("tools", "agent") - - # Finally, we compile it! - # This compiles it into a LangChain Runnable, - # meaning you can use it as you would any other runnable - return workflow.compile() diff --git a/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py b/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py index 221b5b725..86a1c13e5 100644 --- a/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py +++ b/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py @@ -1,5 +1,3 @@ -import json -import types from typing import ( Annotated, Callable, @@ -15,20 +13,18 @@ from langchain_core.language_models import LanguageModelLike from langchain_core.messages import ( AIMessage, BaseMessage, - FunctionMessage, SystemMessage, ) from langchain_core.runnables import Runnable, RunnableConfig, RunnableLambda from langchain_core.tools import BaseTool -from langchain_core.utils.function_calling import convert_to_openai_function -from langgraph._api.deprecation import deprecated, deprecated_parameter +from langgraph._api.deprecation import deprecated_parameter from langgraph.checkpoint.base import BaseCheckpointSaver from langgraph.graph import END, StateGraph from langgraph.graph.graph import CompiledGraph from langgraph.graph.message import add_messages from langgraph.managed import IsLastStep -from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation +from langgraph.prebuilt.tool_executor import ToolExecutor from langgraph.prebuilt.tool_node import ToolNode @@ -64,136 +60,6 @@ StateModifier = Union[ ] -@deprecated("0.0.44", "create_react_agent", removal="0.2.0") -def create_function_calling_executor( - model: LanguageModelLike, tools: Union[ToolExecutor, Sequence[BaseTool]] -) -> CompiledGraph: - """Creates a graph that works with a chat model that utilizes function calling. - - Examples: - ```pycon - >>> # Since this is deprecated, you should use `create_react_agent` instead. - >>> # Example usage: - >>> from langgraph.prebuilt import create_react_agent - >>> from langchain_openai import ChatOpenAI - >>> from langchain_community.tools.tavily_search import TavilySearchResults - >>> - >>> tools = [TavilySearchResults(max_results=1)] - >>> model = ChatOpenAI() - >>> - >>> app = create_react_agent(model, tools) - >>> inputs = {"messages": [("user", "what is the weather in sf")]} - >>> for s in app.stream(inputs): - ... print(list(s.values())[0]) - ... print("----") - ``` - """ - if isinstance(tools, ToolExecutor): - tool_executor = tools - tool_classes = tools.tools - else: - tool_executor = ToolExecutor(tools) - tool_classes = tools - model = model.bind(functions=[convert_to_openai_function(t) for t in tool_classes]) - - # Define the function that determines whether to continue or not - def should_continue(state: AgentState): - messages = state["messages"] - last_message = messages[-1] - # If there is no function call, then we finish - if "function_call" not in last_message.additional_kwargs: - return "end" - # Otherwise if there is, we continue - else: - return "continue" - - # Define the function that calls the model - def call_model(state: AgentState, config: RunnableConfig): - messages = state["messages"] - response = model.invoke(messages, config) - # We return a list, because this will get added to the existing list - return {"messages": [response]} - - async def acall_model(state: AgentState, config: RunnableConfig): - messages = state["messages"] - response = await model.ainvoke(messages, config) - # We return a list, because this will get added to the existing list - return {"messages": [response]} - - # Define the function to execute tools - def _get_action(state: AgentState): - messages = state["messages"] - # Based on the continue condition - # we know the last message involves a function call - last_message = messages[-1] - # We construct an AgentAction from the function_call - return ToolInvocation( - tool=last_message.additional_kwargs["function_call"]["name"], - tool_input=json.loads( - last_message.additional_kwargs["function_call"]["arguments"] - ), - ) - - def call_tool(state: AgentState, config: RunnableConfig): - action = _get_action(state) - # We call the tool_executor and get back a response - response = tool_executor.invoke(action, config) - # We use the response to create a FunctionMessage - function_message = FunctionMessage(content=str(response), name=action.tool) - # We return a list, because this will get added to the existing list - return {"messages": [function_message]} - - async def acall_tool(state: AgentState, config: RunnableConfig): - action = _get_action(state) - # We call the tool_executor and get back a response - response = await tool_executor.ainvoke(action, config) - # We use the response to create a FunctionMessage - function_message = FunctionMessage(content=str(response), name=action.tool) - # We return a list, because this will get added to the existing list - return {"messages": [function_message]} - - # Define a new graph - workflow = StateGraph(AgentState) - - # Define the two nodes we will cycle between - workflow.add_node("agent", RunnableLambda(call_model, acall_model)) - workflow.add_node("tools", RunnableLambda(call_tool, acall_tool)) - - # Set the entrypoint as `agent` - # This means that this node is the first one called - workflow.set_entry_point("agent") - - # We now add a conditional edge - workflow.add_conditional_edges( - # First, we define the start node. We use `agent`. - # This means these are the edges taken after the `agent` node is called. - "agent", - # Next, we pass in the function that will determine which node is called next. - should_continue, - # Finally we pass in a mapping. - # The keys are strings, and the values are other nodes. - # END is a special node marking that the graph should finish. - # What will happen is we will call `should_continue`, and then the output of that - # will be matched against the keys in this mapping. - # Based on which one it matches, that node will then be called. - { - # If `tools`, then we call the tool node. - "continue": "tools", - # Otherwise we finish. - "end": END, - }, - ) - - # We now add a normal edge from `tools` to `agent`. - # This means that after `tools` is called, `agent` node is called next. - workflow.add_edge("tools", "agent") - - # Finally, we compile it! - # This compiles it into a LangChain Runnable, - # meaning you can use it as you would any other runnable - return workflow.compile() - - def _get_state_modifier_runnable(state_modifier: Optional[StateModifier]) -> Runnable: state_modifier_runnable: Runnable if state_modifier is None: @@ -231,7 +97,7 @@ def _convert_messages_modifier_to_state_modifier( state_modifier: StateModifier if isinstance(messages_modifier, (str, SystemMessage)): return messages_modifier - elif isinstance(messages_modifier, types.FunctionType): + elif callable(messages_modifier): def state_modifier(state: AgentState) -> Sequence[BaseMessage]: return messages_modifier(state["messages"]) @@ -261,7 +127,7 @@ def _get_model_preprocessing_runnable( return _get_state_modifier_runnable(state_modifier) -@deprecated_parameter("messages_modifier", "0.1.9", "state_modifier", removal="0.2.0") +@deprecated_parameter("messages_modifier", "0.1.9", "state_modifier", removal="0.3.0") def create_react_agent( model: LanguageModelLike, tools: Union[ToolExecutor, Sequence[BaseTool]], @@ -656,6 +522,5 @@ create_tool_calling_executor = create_react_agent __all__ = [ "create_react_agent", "create_tool_calling_executor", - "create_function_calling_executor", "AgentState", ] diff --git a/libs/langgraph/langgraph/prebuilt/tool_executor.py b/libs/langgraph/langgraph/prebuilt/tool_executor.py index cfd217044..7590b3836 100644 --- a/libs/langgraph/langgraph/prebuilt/tool_executor.py +++ b/libs/langgraph/langgraph/prebuilt/tool_executor.py @@ -5,6 +5,7 @@ from langchain_core.runnables import RunnableConfig from langchain_core.tools import BaseTool from langchain_core.tools import tool as create_tool +from langgraph._api.deprecation import deprecated from langgraph.utils import RunnableCallable INVALID_TOOL_MSG_TEMPLATE = ( @@ -13,6 +14,7 @@ INVALID_TOOL_MSG_TEMPLATE = ( ) +@deprecated("0.2.0", "langgraph.prebuilt.ToolNode", removal="0.3.0") class ToolInvocationInterface: """Interface for invoking a tool. @@ -26,6 +28,7 @@ class ToolInvocationInterface: tool_input: Union[str, dict] +@deprecated("0.2.0", "langgraph.prebuilt.ToolNode", removal="0.3.0") class ToolInvocation(Serializable): """Information about how to invoke a tool. @@ -47,6 +50,7 @@ class ToolInvocation(Serializable): tool_input: Union[str, dict] +@deprecated("0.2.0", "langgraph.prebuilt.ToolNode", removal="0.3.0") class ToolExecutor(RunnableCallable): """Executes a tool invocation. diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 7095a5b39..cd40eab8a 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -55,7 +55,6 @@ from langgraph.graph.graph import START from langgraph.graph.message import MessageGraph, add_messages from langgraph.graph.state import StateGraph from langgraph.prebuilt.chat_agent_executor import ( - create_function_calling_executor, create_tool_calling_executor, ) from langgraph.prebuilt.tool_node import ToolNode @@ -3719,140 +3718,6 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None: ] -def test_prebuilt_chat(snapshot: SnapshotAssertion) -> None: - from langchain_core.language_models.fake_chat_models import ( - FakeMessagesListChatModel, - ) - from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage - from langchain_core.tools import tool - - class FakeFuntionChatModel(FakeMessagesListChatModel): - def bind_functions(self, functions: list): - return self - - @tool() - def search_api(query: str) -> str: - """Searches the API for the query.""" - return f"result for {query}" - - tools = [search_api] - - app = create_function_calling_executor( - FakeFuntionChatModel( - responses=[ - AIMessage( - content="", - additional_kwargs={ - "function_call": { - "name": "search_api", - "arguments": json.dumps("query"), - } - }, - ), - AIMessage( - content="", - additional_kwargs={ - "function_call": { - "name": "search_api", - "arguments": json.dumps("another"), - } - }, - ), - AIMessage(content="answer"), - ] - ), - tools, - ) - - assert app.get_input_schema().schema_json() == snapshot - assert app.get_output_schema().schema_json() == snapshot - assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot - assert app.get_graph().draw_mermaid(with_styles=False) == snapshot - - assert app.invoke( - {"messages": [HumanMessage(content="what is weather in sf")]} - ) == { - "messages": [ - _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - id=AnyStr(), - content="", - additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"query"'} - }, - ), - FunctionMessage(content="result for query", name="search_api", id=AnyStr()), - AIMessage( - id=AnyStr(), - content="", - additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"another"'} - }, - ), - FunctionMessage( - content="result for another", name="search_api", id=AnyStr() - ), - _AnyIdAIMessage(content="answer"), - ] - } - - assert [ - *app.stream({"messages": [HumanMessage(content="what is weather in sf")]}) - ] == [ - { - "agent": { - "messages": [ - AIMessage( - id=AnyStr(), - content="", - additional_kwargs={ - "function_call": { - "name": "search_api", - "arguments": '"query"', - } - }, - ) - ] - } - }, - { - "tools": { - "messages": [ - FunctionMessage( - content="result for query", name="search_api", id=AnyStr() - ) - ] - } - }, - { - "agent": { - "messages": [ - AIMessage( - id=AnyStr(), - content="", - additional_kwargs={ - "function_call": { - "name": "search_api", - "arguments": '"another"', - } - }, - ) - ] - } - }, - { - "tools": { - "messages": [ - FunctionMessage( - content="result for another", name="search_api", id=AnyStr() - ) - ] - } - }, - {"agent": {"messages": [_AnyIdAIMessage(content="answer")]}}, - ] - - @pytest.mark.parametrize("serde", [NoopSerializer(), JsonPlusSerializer()]) def test_state_graph_packets(serde: SerializerProtocol) -> None: from langchain_core.language_models.fake_chat_models import ( diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 9b3a5ec1f..b718f51c9 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -52,7 +52,6 @@ from langgraph.graph import END, Graph, StateGraph from langgraph.graph.graph import START from langgraph.graph.message import MessageGraph, add_messages from langgraph.prebuilt.chat_agent_executor import ( - create_function_calling_executor, create_tool_calling_executor, ) from langgraph.prebuilt.tool_executor import ToolExecutor @@ -3507,138 +3506,6 @@ async def test_prebuilt_tool_chat() -> None: ] -async def test_prebuilt_chat() -> None: - from langchain_core.language_models.fake_chat_models import ( - FakeMessagesListChatModel, - ) - from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage - from langchain_core.tools import tool - - class FakeFuntionChatModel(FakeMessagesListChatModel): - def bind_functions(self, functions: list): - return self - - @tool() - def search_api(query: str) -> str: - """Searches the API for the query.""" - return f"result for {query}" - - tools = [search_api] - - app = create_function_calling_executor( - FakeFuntionChatModel( - responses=[ - AIMessage( - content="", - additional_kwargs={ - "function_call": { - "name": "search_api", - "arguments": json.dumps("query"), - } - }, - ), - AIMessage( - content="", - additional_kwargs={ - "function_call": { - "name": "search_api", - "arguments": json.dumps("another"), - } - }, - ), - AIMessage(content="answer"), - ] - ), - tools, - ) - - assert await app.ainvoke( - {"messages": [HumanMessage(content="what is weather in sf")]} - ) == { - "messages": [ - _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - id=AnyStr(), - content="", - additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"query"'} - }, - ), - FunctionMessage(content="result for query", name="search_api", id=AnyStr()), - AIMessage( - id=AnyStr(), - content="", - additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"another"'} - }, - ), - FunctionMessage( - content="result for another", name="search_api", id=AnyStr() - ), - _AnyIdAIMessage(content="answer"), - ] - } - - assert [ - c - async for c in app.astream( - {"messages": [HumanMessage(content="what is weather in sf")]} - ) - ] == [ - { - "agent": { - "messages": [ - AIMessage( - id=AnyStr(), - content="", - additional_kwargs={ - "function_call": { - "name": "search_api", - "arguments": '"query"', - } - }, - ) - ] - } - }, - { - "tools": { - "messages": [ - FunctionMessage( - content="result for query", name="search_api", id=AnyStr() - ) - ] - } - }, - { - "agent": { - "messages": [ - AIMessage( - id=AnyStr(), - content="", - additional_kwargs={ - "function_call": { - "name": "search_api", - "arguments": '"another"', - } - }, - ) - ] - } - }, - { - "tools": { - "messages": [ - FunctionMessage( - content="result for another", name="search_api", id=AnyStr() - ) - ] - } - }, - {"agent": {"messages": [_AnyIdAIMessage(content="answer")]}}, - ] - - async def test_state_graph_packets() -> None: from langchain_core.language_models.fake_chat_models import ( FakeMessagesListChatModel,