From 38ef3d5218bd175c967760227006306ea0c6145d Mon Sep 17 00:00:00 2001 From: midas8181919 Date: Sat, 10 Feb 2024 23:10:32 +0000 Subject: [PATCH 01/13] replace function_call with tool_call --- langgraph/prebuilt/chat_agent_executor.py | 116 ++++++- tests/test_pregel.py | 361 +++++++++++++++++++++- 2 files changed, 475 insertions(+), 2 deletions(-) diff --git a/langgraph/prebuilt/chat_agent_executor.py b/langgraph/prebuilt/chat_agent_executor.py index bc7208107..83e48a392 100644 --- a/langgraph/prebuilt/chat_agent_executor.py +++ b/langgraph/prebuilt/chat_agent_executor.py @@ -5,7 +5,7 @@ from typing import Annotated, Sequence, TypedDict from langchain_core.agents import AgentAction from langchain_core.messages import BaseMessage, FunctionMessage from langchain_core.runnables import RunnableLambda -from langchain_core.utils.function_calling import convert_to_openai_function +from langchain_core.utils.function_calling import convert_to_openai_function, convert_to_openai_tool from langgraph.graph import END, StateGraph from langgraph.prebuilt.tool_executor import ToolExecutor @@ -124,3 +124,117 @@ def create_function_calling_executor(model, tools): # This compiles it into a LangChain Runnable, # meaning you can use it as you would any other runnable return workflow.compile() + +def create_tool_calling_executor(model, tools): + 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_tool(t) for t in tool_classes]) + + # Define the function that determines whether to continue or not + def should_continue(state): + messages = state["messages"] + last_message = messages[-1] + # If there is no function call, then we finish + if "tool_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): + messages = state["messages"] + response = model.invoke(messages) + # We return a list, because this will get added to the existing list + return {"messages": [response]} + + async def acall_model(state): + messages = state["messages"] + response = await model.ainvoke(messages) + # 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): + messages = state["messages"] + # Based on the continue condition + # we know the last message involves a tool call + last_message = messages[-1] + # We construct an AgentAction from the tool_calls + return AgentAction( + tool=last_message.additional_kwargs["tool_calls"][0]["function"]["name"], + tool_input=json.loads( + last_message.additional_kwargs["tool_calls"][0]["function"]["arguments"] + ), + log="", + ) + + def call_tool(state): + action = _get_action(state) + # We call the tool_executor and get back a response + response = tool_executor.invoke(action) + # 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): + action = _get_action(state) + # We call the tool_executor and get back a response + response = await tool_executor.ainvoke(action) + # 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]} + + # We create the AgentState that we will pass around + # This simply involves a list of messages + # We want steps to return messages to append to the list + # So we annotate the messages attribute with operator.add + class AgentState(TypedDict): + messages: Annotated[Sequence[BaseMessage], operator.add] + + # 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("action", 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": "action", + # 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("action", "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/tests/test_pregel.py b/tests/test_pregel.py index f9fef277b..696555f61 100644 --- a/tests/test_pregel.py +++ b/tests/test_pregel.py @@ -10,6 +10,7 @@ import pytest from langchain_core.runnables import RunnablePassthrough from pytest_mock import MockerFixture +from langchain_core._api import deprecated from langgraph.channels.base import InvalidUpdateError from langgraph.channels.binop import BinaryOperatorAggregate from langgraph.channels.context import Context @@ -20,7 +21,7 @@ from langgraph.checkpoint.sqlite import SqliteSaver from langgraph.graph import END, Graph from langgraph.graph.message import MessageGraph from langgraph.graph.state import StateGraph -from langgraph.prebuilt.chat_agent_executor import create_function_calling_executor +from langgraph.prebuilt.chat_agent_executor import create_function_calling_executor, create_tool_calling_executor from langgraph.prebuilt.tool_executor import ToolExecutor from langgraph.pregel import Channel, GraphRecursionError, Pregel from langgraph.pregel.reserved import ReservedChannels @@ -1061,7 +1062,365 @@ def test_conditional_graph_state() -> None: }, ] +def test_prebuilt_tool_chat() -> None: + from langchain.chat_models.fake import FakeMessagesListChatModel + from langchain_community.tools import tool + from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage + 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_tool_calling_executor( + FakeFuntionChatModel( + responses=[ + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call123", + "type": "function", + "function":{ + "name": "search_api", + "arguments": json.dumps("query"), + } + }] + }, + ), + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call234", + "type": "function", + "function":{ + "name": "search_api", + "arguments": json.dumps("another"), + } + }] + }, + ), + AIMessage(content="answer"), + ] + ), + tools, + ) + + assert app.invoke( + {"messages": [HumanMessage(content="what is weather in sf")]} + ) == { + "messages": [ + HumanMessage(content="what is weather in sf"), + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call123", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "query", + } + }] + }, + ), + FunctionMessage(content="result for query", name="search_api"), + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call234", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "another", + } + }] + }, + ), + FunctionMessage(content="result for another", name="search_api"), + AIMessage(content="answer"), + ] + } + + assert [ + *app.stream({"messages": [HumanMessage(content="what is weather in sf")]}) + ] == [ + { + "agent": { + "messages": [ + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call123", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "query", + } + }] + }, + ) + ] + } + }, + { + "action": { + "messages": [ + FunctionMessage(content="result for query", name="search_api") + ] + } + }, + { + "agent": { + "messages": [ + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call234", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "another", + } + }] + }, + ) + ] + } + }, + { + "action": { + "messages": [ + FunctionMessage(content="result for another", name="search_api") + ] + } + }, + {"agent": {"messages": [AIMessage(content="answer")]}}, + { + "__end__": { + "messages": [ + HumanMessage(content="what is weather in sf"), + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call123", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "query", + } + }] + }, + ), + FunctionMessage(content="result for query", name="search_api"), + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call234", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "another", + } + }] + }, + ), + FunctionMessage(content="result for another", name="search_api"), + AIMessage(content="answer"), + ] + } + }, + ] + + +def test_message_graph() -> None: + from langchain.chat_models.fake import FakeMessagesListChatModel + from langchain_community.tools import tool + from langchain_core.agents import AgentAction + from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage + + 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] + + model = 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"), + ] + ) + + tool_executor = ToolExecutor(tools) + + # Define the function that determines whether to continue or not + def should_continue(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" + + def call_tool(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 + action = AgentAction( + tool=last_message.additional_kwargs["function_call"]["name"], + tool_input=json.loads( + last_message.additional_kwargs["function_call"]["arguments"] + ), + log="", + ) + # We call the tool_executor and get back a response + response = tool_executor.invoke(action) + # We use the response to create a FunctionMessage + return FunctionMessage(content=str(response), name=action.tool) + + # Define a new graph + workflow = MessageGraph() + + # Define the two nodes we will cycle between + workflow.add_node("agent", model) + workflow.add_node("action", call_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": "action", + # 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("action", "agent") + + # Finally, we compile it! + # This compiles it into a LangChain Runnable, + # meaning you can use it as you would any other runnable + app = workflow.compile() + + assert app.invoke(HumanMessage(content="what is weather in sf")) == [ + HumanMessage(content="what is weather in sf"), + AIMessage( + content="", + additional_kwargs={ + "function_call": {"name": "search_api", "arguments": '"query"'} + }, + ), + FunctionMessage(content="result for query", name="search_api"), + AIMessage( + content="", + additional_kwargs={ + "function_call": {"name": "search_api", "arguments": '"another"'} + }, + ), + FunctionMessage(content="result for another", name="search_api"), + AIMessage(content="answer"), + ] + + assert [*app.stream([HumanMessage(content="what is weather in sf")])] == [ + { + "agent": AIMessage( + content="", + additional_kwargs={ + "function_call": {"name": "search_api", "arguments": '"query"'} + }, + ) + }, + {"action": FunctionMessage(content="result for query", name="search_api")}, + { + "agent": AIMessage( + content="", + additional_kwargs={ + "function_call": {"name": "search_api", "arguments": '"another"'} + }, + ) + }, + {"action": FunctionMessage(content="result for another", name="search_api")}, + {"agent": AIMessage(content="answer")}, + { + "__end__": [ + HumanMessage(content="what is weather in sf"), + AIMessage( + content="", + additional_kwargs={ + "function_call": {"name": "search_api", "arguments": '"query"'} + }, + ), + FunctionMessage(content="result for query", name="search_api"), + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": '"another"', + } + }, + ), + FunctionMessage(content="result for another", name="search_api"), + AIMessage(content="answer"), + ] + }, + ] + +@deprecated("*") def test_prebuilt_chat() -> None: from langchain.chat_models.fake import FakeMessagesListChatModel from langchain_community.tools import tool From acaa4567e158a184cd79cc648b4f19b5176087c2 Mon Sep 17 00:00:00 2001 From: midas8181919 Date: Sat, 10 Feb 2024 23:25:17 +0000 Subject: [PATCH 02/13] fix test --- tests/test_pregel.py | 98 ++++++++++++++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 26 deletions(-) diff --git a/tests/test_pregel.py b/tests/test_pregel.py index 696555f61..6f0e3aad8 100644 --- a/tests/test_pregel.py +++ b/tests/test_pregel.py @@ -1244,8 +1244,7 @@ def test_prebuilt_tool_chat() -> None: }, ] - -def test_message_graph() -> None: +def test_tool_message_graph() -> None: from langchain.chat_models.fake import FakeMessagesListChatModel from langchain_community.tools import tool from langchain_core.agents import AgentAction @@ -1267,19 +1266,27 @@ def test_message_graph() -> None: AIMessage( content="", additional_kwargs={ - "function_call": { - "name": "search_api", - "arguments": json.dumps("query"), - } + "tool_calls": [{ + "id": "tool_call123", + "type": "function", + "function":{ + "name": "search_api", + "arguments": json.dumps("query"), + } + }] }, ), AIMessage( content="", additional_kwargs={ - "function_call": { - "name": "search_api", - "arguments": json.dumps("another"), - } + "tool_calls": [{ + "id": "tool_call234", + "type": "function", + "function":{ + "name": "search_api", + "arguments": json.dumps("another"), + } + }] }, ), AIMessage(content="answer"), @@ -1292,7 +1299,7 @@ def test_message_graph() -> None: def should_continue(messages): last_message = messages[-1] # If there is no function call, then we finish - if "function_call" not in last_message.additional_kwargs: + if "tool_calls" not in last_message.additional_kwargs: return "end" # Otherwise if there is, we continue else: @@ -1304,9 +1311,9 @@ def test_message_graph() -> None: last_message = messages[-1] # We construct an AgentAction from the function_call action = AgentAction( - tool=last_message.additional_kwargs["function_call"]["name"], + tool=last_message.additional_kwargs["tool_calls"][0]["function"]["name"], tool_input=json.loads( - last_message.additional_kwargs["function_call"]["arguments"] + last_message.additional_kwargs["tool_calls"][0]["function"]["arguments"] ), log="", ) @@ -1361,14 +1368,28 @@ def test_message_graph() -> None: AIMessage( content="", additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"query"'} + "tool_calls": [{ + "id": "tool_call123", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "query", + } + }] }, ), FunctionMessage(content="result for query", name="search_api"), AIMessage( content="", additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"another"'} + "tool_calls": [{ + "id": "tool_call234", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "another", + } + }] }, ), FunctionMessage(content="result for another", name="search_api"), @@ -1380,7 +1401,14 @@ def test_message_graph() -> None: "agent": AIMessage( content="", additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"query"'} + "tool_calls": [{ + "id": "tool_call123", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "query", + } + }] }, ) }, @@ -1389,8 +1417,15 @@ def test_message_graph() -> None: "agent": AIMessage( content="", additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"another"'} - }, + "tool_calls": [{ + "id": "tool_call234", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "another", + } + }] + }, ) }, {"action": FunctionMessage(content="result for another", name="search_api")}, @@ -1401,18 +1436,29 @@ def test_message_graph() -> None: AIMessage( content="", additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"query"'} + "tool_calls": [{ + "id": "tool_call123", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "query", + } + }] }, ), FunctionMessage(content="result for query", name="search_api"), AIMessage( content="", additional_kwargs={ - "function_call": { - "name": "search_api", - "arguments": '"another"', - } - }, + "tool_calls": [{ + "id": "tool_call234", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "another", + } + }] + }, ), FunctionMessage(content="result for another", name="search_api"), AIMessage(content="answer"), @@ -1565,7 +1611,7 @@ def test_prebuilt_chat() -> None: }, ] - +@deprecated("*") def test_message_graph() -> None: from langchain.chat_models.fake import FakeMessagesListChatModel from langchain_community.tools import tool @@ -1739,4 +1785,4 @@ def test_message_graph() -> None: AIMessage(content="answer"), ] }, - ] + ] \ No newline at end of file From b4269f64536b0a78abc0d92c0f5f036a6522c75d Mon Sep 17 00:00:00 2001 From: midas8181919 Date: Sun, 11 Feb 2024 02:04:09 +0000 Subject: [PATCH 03/13] finish test --- tests/test_pregel_async.py | 416 ++++++++++++++++++++++++++++++++++++- 1 file changed, 414 insertions(+), 2 deletions(-) diff --git a/tests/test_pregel_async.py b/tests/test_pregel_async.py index db281e7c6..24fa679b8 100644 --- a/tests/test_pregel_async.py +++ b/tests/test_pregel_async.py @@ -17,6 +17,7 @@ import pytest from langchain_core.runnables import RunnablePassthrough from pytest_mock import MockerFixture +from langchain_core._api import deprecated from langgraph.channels.base import InvalidUpdateError from langgraph.channels.binop import BinaryOperatorAggregate from langgraph.channels.context import Context @@ -26,7 +27,7 @@ from langgraph.checkpoint.aiosqlite import AsyncSqliteSaver from langgraph.checkpoint.memory import MemorySaver from langgraph.graph import END, Graph, StateGraph from langgraph.graph.message import MessageGraph -from langgraph.prebuilt.chat_agent_executor import create_function_calling_executor +from langgraph.prebuilt.chat_agent_executor import create_function_calling_executor, create_tool_calling_executor from langgraph.prebuilt.tool_executor import ToolExecutor from langgraph.pregel import Channel, GraphRecursionError, Pregel from langgraph.pregel.reserved import ReservedChannels @@ -1111,6 +1112,417 @@ async def test_conditional_graph_state() -> None: ] +async def test_prebuilt_tool_chat() -> None: + from langchain.chat_models.fake import FakeMessagesListChatModel + from langchain_community.tools import tool + from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage + + 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={ + "tool_calls": [{ + "id": "tool_call123", + "type": "function", + "function":{ + "name": "search_api", + "arguments": json.dumps("query"), + } + }] + }, + ), + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call234", + "type": "function", + "function":{ + "name": "search_api", + "arguments": json.dumps("another"), + } + }] + }, + ), + AIMessage(content="answer"), + ] + ), + tools, + ) + + assert await app.ainvoke( + {"messages": [HumanMessage(content="what is weather in sf")]} + ) == { + "messages": [ + HumanMessage(content="what is weather in sf"), + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call123", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "query", + } + }] + }, + ), + FunctionMessage(content="result for query", name="search_api"), + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call234", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "another", + } + }] + }, + ), + FunctionMessage(content="result for another", name="search_api"), + AIMessage(content="answer"), + ] + } + + assert [ + c + async for c in app.astream( + {"messages": [HumanMessage(content="what is weather in sf")]} + ) + ] == [ + { + "agent": { + "messages": [ + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call123", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "query", + } + }] + }, + ) + ] + } + }, + { + "action": { + "messages": [ + FunctionMessage(content="result for query", name="search_api") + ] + } + }, + { + "agent": { + "messages": [ + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call234", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "another", + } + }] + }, + ) + ] + } + }, + { + "action": { + "messages": [ + FunctionMessage(content="result for another", name="search_api") + ] + } + }, + {"agent": {"messages": [AIMessage(content="answer")]}}, + { + "__end__": { + "messages": [ + HumanMessage(content="what is weather in sf"), + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call123", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "query", + } + }] + }, + ), + FunctionMessage(content="result for query", name="search_api"), + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call234", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "another", + } + }] + }, + ), + FunctionMessage(content="result for another", name="search_api"), + AIMessage(content="answer"), + ] + } + }, + ] + + +async def test_message_tool_graph() -> None: + from langchain.chat_models.fake import FakeMessagesListChatModel + from langchain_community.tools import tool + from langchain_core.agents import AgentAction + from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage + + 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] + + model = FakeFuntionChatModel( + responses=[ + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call123", + "type": "function", + "function":{ + "name": "search_api", + "arguments": json.dumps("query"), + } + }] + }, + ), + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call234", + "type": "function", + "function":{ + "name": "search_api", + "arguments": json.dumps("another"), + } + }] + }, + ), + AIMessage(content="answer"), + ] + ) + + tool_executor = ToolExecutor(tools) + + # Define the function that determines whether to continue or not + def should_continue(messages): + last_message = messages[-1] + # If there is no function call, then we finish + if "tool_calls" not in last_message.additional_kwargs: + return "end" + # Otherwise if there is, we continue + else: + return "continue" + + async def call_tool(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 + action = AgentAction( + tool=last_message.additional_kwargs["tool_calls"][0]["fcuntion"]["name"], + tool_input=json.loads( + last_message.additional_kwargs["tool_calls"][0]["function"]["arguments"] + ), + log="", + ) + # We call the tool_executor and get back a response + response = await tool_executor.ainvoke(action) + # We use the response to create a FunctionMessage + return FunctionMessage(content=str(response), name=action.tool) + + # Define a new graph + workflow = MessageGraph() + + # Define the two nodes we will cycle between + workflow.add_node("agent", model) + workflow.add_node("action", call_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": "action", + # 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("action", "agent") + + # Finally, we compile it! + # This compiles it into a LangChain Runnable, + # meaning you can use it as you would any other runnable + app = workflow.compile() + + assert await app.ainvoke(HumanMessage(content="what is weather in sf")) == [ + HumanMessage(content="what is weather in sf"), + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call123", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "query", + } + }] + }, + ), + FunctionMessage(content="result for query", name="search_api"), + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call234", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "another", + } + }] + }, + ), + FunctionMessage(content="result for another", name="search_api"), + AIMessage(content="answer"), + ] + + assert [ + c async for c in app.astream([HumanMessage(content="what is weather in sf")]) + ] == [ + { + "agent": AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call123", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "query", + } + }] + }, + ) + }, + {"action": FunctionMessage(content="result for query", name="search_api")}, + { + "agent": AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call234", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "another", + } + }] + }, + ) + }, + {"action": FunctionMessage(content="result for another", name="search_api")}, + {"agent": AIMessage(content="answer")}, + { + "__end__": [ + HumanMessage(content="what is weather in sf"), + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call123", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "query", + } + }] + }, + ), + FunctionMessage(content="result for query", name="search_api"), + AIMessage( + content="", + additional_kwargs={ + "tool_calls": [{ + "id": "tool_call234", + "type": "function", + "function":{ + "name": "search_api", + "arguments": "another", + } + }] + }, + ), + FunctionMessage(content="result for another", name="search_api"), + AIMessage(content="answer"), + ] + }, + ] + +@deprecated("*") async def test_prebuilt_chat() -> None: from langchain.chat_models.fake import FakeMessagesListChatModel from langchain_community.tools import tool @@ -1258,7 +1670,7 @@ async def test_prebuilt_chat() -> None: }, ] - +@deprecated("*") async def test_message_graph() -> None: from langchain.chat_models.fake import FakeMessagesListChatModel from langchain_community.tools import tool From 7cdae56e7e9e4c81b767a4cccceb0478b16f2396 Mon Sep 17 00:00:00 2001 From: midas8181919 Date: Sun, 11 Feb 2024 02:19:29 +0000 Subject: [PATCH 04/13] test it --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0109f16e4..84ae06147 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ name: test on: push: - branches: [master] + branches: [tool_call] pull_request: env: From 41ff4c8c437c9f2f9ed351459eb739de02edb110 Mon Sep 17 00:00:00 2001 From: midas8181919 Date: Tue, 13 Feb 2024 15:41:27 +0000 Subject: [PATCH 05/13] fix some errors --- README.md | 8 +-- langgraph/prebuilt/chat_agent_executor.py | 14 ++--- tests/test_pregel.py | 63 ++++++++++----------- tests/test_pregel_async.py | 67 ++++++++++++----------- 4 files changed, 76 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 96f70c7db..7be0bb11f 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ def should_continue(state): 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: + if "tool_calls" not in last_message.additional_kwargs: return "end" # Otherwise if there is, we continue else: @@ -171,10 +171,10 @@ def call_tool(state): # Based on the continue condition # we know the last message involves a function call last_message = messages[-1] - # We construct an ToolInvocation from the function_call + # We construct an ToolInvocation from the tool_calls action = ToolInvocation( - tool=last_message.additional_kwargs["function_call"]["name"], - tool_input=json.loads(last_message.additional_kwargs["function_call"]["arguments"]), + tool=last_message.additional_kwargs["tool_calls"][0]["function"]["name"], + tool_input=json.loads(last_message.additional_kwargs["tool_calls"][0]["function"]["arguments"]), ) # We call the tool_executor and get back a response response = tool_executor.invoke(action) diff --git a/langgraph/prebuilt/chat_agent_executor.py b/langgraph/prebuilt/chat_agent_executor.py index 83e48a392..c99d56e65 100644 --- a/langgraph/prebuilt/chat_agent_executor.py +++ b/langgraph/prebuilt/chat_agent_executor.py @@ -3,7 +3,7 @@ import operator from typing import Annotated, Sequence, TypedDict from langchain_core.agents import AgentAction -from langchain_core.messages import BaseMessage, FunctionMessage +from langchain_core.messages import BaseMessage, FunctionMessage, ToolMessage from langchain_core.runnables import RunnableLambda from langchain_core.utils.function_calling import convert_to_openai_function, convert_to_openai_tool @@ -139,7 +139,7 @@ def create_tool_calling_executor(model, tools): messages = state["messages"] last_message = messages[-1] # If there is no function call, then we finish - if "tool_call" not in last_message.additional_kwargs: + if "tool_calls" not in last_message.additional_kwargs: return "end" # Otherwise if there is, we continue else: @@ -170,7 +170,7 @@ def create_tool_calling_executor(model, tools): tool_input=json.loads( last_message.additional_kwargs["tool_calls"][0]["function"]["arguments"] ), - log="", + log=last_message.additional_kwargs["tool_calls"][0]["id"], ) def call_tool(state): @@ -178,18 +178,18 @@ def create_tool_calling_executor(model, tools): # We call the tool_executor and get back a response response = tool_executor.invoke(action) # We use the response to create a FunctionMessage - function_message = FunctionMessage(content=str(response), name=action.tool) + tool_message = ToolMessage(content=str(response), tool_call_id=action.log) # We return a list, because this will get added to the existing list - return {"messages": [function_message]} + return {"messages": [tool_message]} async def acall_tool(state): action = _get_action(state) # We call the tool_executor and get back a response response = await tool_executor.ainvoke(action) # We use the response to create a FunctionMessage - function_message = FunctionMessage(content=str(response), name=action.tool) + tool_message = ToolMessage(content=str(response), tool_call_id=action.log) # We return a list, because this will get added to the existing list - return {"messages": [function_message]} + return {"messages": [tool_message]} # We create the AgentState that we will pass around # This simply involves a list of messages diff --git a/tests/test_pregel.py b/tests/test_pregel.py index 6f0e3aad8..447e99e12 100644 --- a/tests/test_pregel.py +++ b/tests/test_pregel.py @@ -26,7 +26,6 @@ from langgraph.prebuilt.tool_executor import ToolExecutor from langgraph.pregel import Channel, GraphRecursionError, Pregel from langgraph.pregel.reserved import ReservedChannels - def test_invoke_single_process_in_out(mocker: MockerFixture) -> None: add_one = mocker.Mock(side_effect=lambda x: x + 1) chain = Channel.subscribe_to("input") | add_one | Channel.write_to("output") @@ -1065,7 +1064,7 @@ def test_conditional_graph_state() -> None: def test_prebuilt_tool_chat() -> None: from langchain.chat_models.fake import FakeMessagesListChatModel from langchain_community.tools import tool - from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage + from langchain_core.messages import AIMessage, HumanMessage, ToolMessage class FakeFuntionChatModel(FakeMessagesListChatModel): def bind_functions(self, functions: list): @@ -1112,7 +1111,7 @@ def test_prebuilt_tool_chat() -> None: ), tools, ) - + assert app.invoke( {"messages": [HumanMessage(content="what is weather in sf")]} ) == { @@ -1126,12 +1125,12 @@ def test_prebuilt_tool_chat() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "query", + "arguments": "\"query\"", } }] }, ), - FunctionMessage(content="result for query", name="search_api"), + ToolMessage(content="result for query", tool_call_id="tool_call123"), AIMessage( content="", additional_kwargs={ @@ -1140,12 +1139,12 @@ def test_prebuilt_tool_chat() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "another", + "arguments": "\"another\"", } }] }, ), - FunctionMessage(content="result for another", name="search_api"), + ToolMessage(content="result for another", tool_call_id="tool_call234"), AIMessage(content="answer"), ] } @@ -1164,7 +1163,7 @@ def test_prebuilt_tool_chat() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "query", + "arguments": "\"query\"", } }] }, @@ -1175,7 +1174,7 @@ def test_prebuilt_tool_chat() -> None: { "action": { "messages": [ - FunctionMessage(content="result for query", name="search_api") + ToolMessage(content="result for query", tool_call_id="tool_call123") ] } }, @@ -1190,7 +1189,7 @@ def test_prebuilt_tool_chat() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "another", + "arguments": "\"another\"", } }] }, @@ -1201,7 +1200,7 @@ def test_prebuilt_tool_chat() -> None: { "action": { "messages": [ - FunctionMessage(content="result for another", name="search_api") + ToolMessage(content="result for another", tool_call_id="tool_call234") ] } }, @@ -1218,12 +1217,12 @@ def test_prebuilt_tool_chat() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "query", + "arguments": "\"query\"", } }] }, ), - FunctionMessage(content="result for query", name="search_api"), + ToolMessage(content="result for query", tool_call_id="tool_call123"), AIMessage( content="", additional_kwargs={ @@ -1232,12 +1231,12 @@ def test_prebuilt_tool_chat() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "another", + "arguments": "\"another\"", } }] }, ), - FunctionMessage(content="result for another", name="search_api"), + ToolMessage(content="result for another", tool_call_id="tool_call234"), AIMessage(content="answer"), ] } @@ -1248,7 +1247,7 @@ def test_tool_message_graph() -> None: from langchain.chat_models.fake import FakeMessagesListChatModel from langchain_community.tools import tool from langchain_core.agents import AgentAction - from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage + from langchain_core.messages import AIMessage, ToolMessage, HumanMessage class FakeFuntionChatModel(FakeMessagesListChatModel): def bind_functions(self, functions: list): @@ -1315,12 +1314,12 @@ def test_tool_message_graph() -> None: tool_input=json.loads( last_message.additional_kwargs["tool_calls"][0]["function"]["arguments"] ), - log="", + log=last_message.additional_kwargs["tool_calls"][0]["id"], ) # We call the tool_executor and get back a response response = tool_executor.invoke(action) - # We use the response to create a FunctionMessage - return FunctionMessage(content=str(response), name=action.tool) + # We use the response to create a ToolMessage + return ToolMessage(content=str(response), tool_call_id=action.log) # Define a new graph workflow = MessageGraph() @@ -1373,12 +1372,12 @@ def test_tool_message_graph() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "query", + "arguments": "\"query\"", } }] }, ), - FunctionMessage(content="result for query", name="search_api"), + ToolMessage(content="result for query", tool_call_id="tool_call123"), AIMessage( content="", additional_kwargs={ @@ -1387,12 +1386,12 @@ def test_tool_message_graph() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "another", + "arguments": "\"another\"", } }] }, ), - FunctionMessage(content="result for another", name="search_api"), + ToolMessage(content="result for another", tool_call_id="tool_call234"), AIMessage(content="answer"), ] @@ -1406,13 +1405,13 @@ def test_tool_message_graph() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "query", + "arguments": "\"query\"", } }] }, ) }, - {"action": FunctionMessage(content="result for query", name="search_api")}, + {"action": ToolMessage(content="result for query", tool_call_id="tool_call123")}, { "agent": AIMessage( content="", @@ -1422,13 +1421,13 @@ def test_tool_message_graph() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "another", + "arguments": "\"another\"", } }] }, ) }, - {"action": FunctionMessage(content="result for another", name="search_api")}, + {"action": ToolMessage(content="result for another", tool_call_id="tool_call234")}, {"agent": AIMessage(content="answer")}, { "__end__": [ @@ -1441,12 +1440,12 @@ def test_tool_message_graph() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "query", + "arguments": "\"query\"", } }] }, ), - FunctionMessage(content="result for query", name="search_api"), + ToolMessage(content="result for query", tool_call_id="tool_call123"), AIMessage( content="", additional_kwargs={ @@ -1455,18 +1454,17 @@ def test_tool_message_graph() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "another", + "arguments": "\"another\"", } }] }, ), - FunctionMessage(content="result for another", name="search_api"), + ToolMessage(content="result for another", tool_call_id="tool_call234"), AIMessage(content="answer"), ] }, ] -@deprecated("*") def test_prebuilt_chat() -> None: from langchain.chat_models.fake import FakeMessagesListChatModel from langchain_community.tools import tool @@ -1611,7 +1609,6 @@ def test_prebuilt_chat() -> None: }, ] -@deprecated("*") def test_message_graph() -> None: from langchain.chat_models.fake import FakeMessagesListChatModel from langchain_community.tools import tool diff --git a/tests/test_pregel_async.py b/tests/test_pregel_async.py index 24fa679b8..a51c36a94 100644 --- a/tests/test_pregel_async.py +++ b/tests/test_pregel_async.py @@ -1115,7 +1115,7 @@ async def test_conditional_graph_state() -> None: async def test_prebuilt_tool_chat() -> None: from langchain.chat_models.fake import FakeMessagesListChatModel from langchain_community.tools import tool - from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage + from langchain_core.messages import AIMessage, ToolMessage, HumanMessage class FakeFuntionChatModel(FakeMessagesListChatModel): def bind_functions(self, functions: list): @@ -1128,7 +1128,7 @@ async def test_prebuilt_tool_chat() -> None: tools = [search_api] - app = create_function_calling_executor( + app = create_tool_calling_executor( FakeFuntionChatModel( responses=[ AIMessage( @@ -1163,6 +1163,9 @@ async def test_prebuilt_tool_chat() -> None: tools, ) + res = await app.ainvoke( + {"messages": [HumanMessage(content="what is weather in sf")]} + ) assert await app.ainvoke( {"messages": [HumanMessage(content="what is weather in sf")]} ) == { @@ -1176,12 +1179,12 @@ async def test_prebuilt_tool_chat() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "query", + "arguments": "\"query\"", } }] }, ), - FunctionMessage(content="result for query", name="search_api"), + ToolMessage(content="result for query", tool_call_id="tool_call123"), AIMessage( content="", additional_kwargs={ @@ -1190,12 +1193,12 @@ async def test_prebuilt_tool_chat() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "another", + "arguments": "\"another\"", } }] }, ), - FunctionMessage(content="result for another", name="search_api"), + ToolMessage(content="result for another", tool_call_id="tool_call234"), AIMessage(content="answer"), ] } @@ -1217,7 +1220,7 @@ async def test_prebuilt_tool_chat() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "query", + "arguments": "\"query\"", } }] }, @@ -1228,7 +1231,7 @@ async def test_prebuilt_tool_chat() -> None: { "action": { "messages": [ - FunctionMessage(content="result for query", name="search_api") + ToolMessage(content="result for query", tool_call_id="tool_call123") ] } }, @@ -1243,7 +1246,7 @@ async def test_prebuilt_tool_chat() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "another", + "arguments": "\"another\"", } }] }, @@ -1254,7 +1257,7 @@ async def test_prebuilt_tool_chat() -> None: { "action": { "messages": [ - FunctionMessage(content="result for another", name="search_api") + ToolMessage(content="result for another", tool_call_id="tool_call234") ] } }, @@ -1271,12 +1274,12 @@ async def test_prebuilt_tool_chat() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "query", + "arguments": "\"query\"", } }] }, ), - FunctionMessage(content="result for query", name="search_api"), + ToolMessage(content="result for query", tool_call_id="tool_call123"), AIMessage( content="", additional_kwargs={ @@ -1285,12 +1288,12 @@ async def test_prebuilt_tool_chat() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "another", + "arguments": "\"another\"", } }] }, ), - FunctionMessage(content="result for another", name="search_api"), + ToolMessage(content="result for another", tool_call_id="tool_call234"), AIMessage(content="answer"), ] } @@ -1302,7 +1305,7 @@ async def test_message_tool_graph() -> None: from langchain.chat_models.fake import FakeMessagesListChatModel from langchain_community.tools import tool from langchain_core.agents import AgentAction - from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage + from langchain_core.messages import AIMessage, ToolMessage, HumanMessage class FakeFuntionChatModel(FakeMessagesListChatModel): def bind_functions(self, functions: list): @@ -1365,16 +1368,16 @@ async def test_message_tool_graph() -> None: last_message = messages[-1] # We construct an AgentAction from the function_call action = AgentAction( - tool=last_message.additional_kwargs["tool_calls"][0]["fcuntion"]["name"], + tool=last_message.additional_kwargs["tool_calls"][0]["function"]["name"], tool_input=json.loads( last_message.additional_kwargs["tool_calls"][0]["function"]["arguments"] ), - log="", + log=last_message.additional_kwargs["tool_calls"][0]["id"], ) # We call the tool_executor and get back a response response = await tool_executor.ainvoke(action) # We use the response to create a FunctionMessage - return FunctionMessage(content=str(response), name=action.tool) + return ToolMessage(content=str(response), tool_call_id=action.log) # Define a new graph workflow = MessageGraph() @@ -1427,12 +1430,12 @@ async def test_message_tool_graph() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "query", + "arguments": "\"query\"", } }] }, ), - FunctionMessage(content="result for query", name="search_api"), + ToolMessage(content="result for query", tool_call_id="tool_call123"), AIMessage( content="", additional_kwargs={ @@ -1441,12 +1444,12 @@ async def test_message_tool_graph() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "another", + "arguments": "\"another\"", } }] }, ), - FunctionMessage(content="result for another", name="search_api"), + ToolMessage(content="result for another", tool_call_id="tool_call234"), AIMessage(content="answer"), ] @@ -1462,13 +1465,13 @@ async def test_message_tool_graph() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "query", + "arguments": "\"query\"", } }] }, ) }, - {"action": FunctionMessage(content="result for query", name="search_api")}, + {"action": ToolMessage(content="result for query", tool_call_id="tool_call123")}, { "agent": AIMessage( content="", @@ -1478,13 +1481,13 @@ async def test_message_tool_graph() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "another", + "arguments": "\"another\"", } }] }, ) }, - {"action": FunctionMessage(content="result for another", name="search_api")}, + {"action": ToolMessage(content="result for another", tool_call_id="tool_call234")}, {"agent": AIMessage(content="answer")}, { "__end__": [ @@ -1497,12 +1500,12 @@ async def test_message_tool_graph() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "query", + "arguments": "\"query\"", } }] }, ), - FunctionMessage(content="result for query", name="search_api"), + ToolMessage(content="result for query", tool_call_id="tool_call123"), AIMessage( content="", additional_kwargs={ @@ -1511,18 +1514,18 @@ async def test_message_tool_graph() -> None: "type": "function", "function":{ "name": "search_api", - "arguments": "another", + "arguments": "\"another\"", } }] }, ), - FunctionMessage(content="result for another", name="search_api"), + ToolMessage(content="result for another", tool_call_id="tool_call234"), AIMessage(content="answer"), ] }, ] -@deprecated("*") + async def test_prebuilt_chat() -> None: from langchain.chat_models.fake import FakeMessagesListChatModel from langchain_community.tools import tool @@ -1670,7 +1673,7 @@ async def test_prebuilt_chat() -> None: }, ] -@deprecated("*") + async def test_message_graph() -> None: from langchain.chat_models.fake import FakeMessagesListChatModel from langchain_community.tools import tool From 794cc712549b57482850900ae817cdd6d34440ac Mon Sep 17 00:00:00 2001 From: midas8181919 Date: Tue, 13 Feb 2024 22:42:37 +0000 Subject: [PATCH 06/13] fixed --- tests/test_pregel.py | 1 - tests/test_pregel_async.py | 4 ---- 2 files changed, 5 deletions(-) diff --git a/tests/test_pregel.py b/tests/test_pregel.py index 447e99e12..4b1a78659 100644 --- a/tests/test_pregel.py +++ b/tests/test_pregel.py @@ -10,7 +10,6 @@ import pytest from langchain_core.runnables import RunnablePassthrough from pytest_mock import MockerFixture -from langchain_core._api import deprecated from langgraph.channels.base import InvalidUpdateError from langgraph.channels.binop import BinaryOperatorAggregate from langgraph.channels.context import Context diff --git a/tests/test_pregel_async.py b/tests/test_pregel_async.py index a51c36a94..4f09e372a 100644 --- a/tests/test_pregel_async.py +++ b/tests/test_pregel_async.py @@ -17,7 +17,6 @@ import pytest from langchain_core.runnables import RunnablePassthrough from pytest_mock import MockerFixture -from langchain_core._api import deprecated from langgraph.channels.base import InvalidUpdateError from langgraph.channels.binop import BinaryOperatorAggregate from langgraph.channels.context import Context @@ -1163,9 +1162,6 @@ async def test_prebuilt_tool_chat() -> None: tools, ) - res = await app.ainvoke( - {"messages": [HumanMessage(content="what is weather in sf")]} - ) assert await app.ainvoke( {"messages": [HumanMessage(content="what is weather in sf")]} ) == { From 52df544dac6a94603544915b351752432edad96d Mon Sep 17 00:00:00 2001 From: midas8181919 Date: Tue, 13 Feb 2024 22:58:38 +0000 Subject: [PATCH 07/13] fix --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 84ae06147..0109f16e4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ name: test on: push: - branches: [tool_call] + branches: [master] pull_request: env: From c0e075c08185d8ca9dddeedbc5ed921ce2366a1f Mon Sep 17 00:00:00 2001 From: Eugene Yurtsev Date: Fri, 16 Feb 2024 11:54:57 -0500 Subject: [PATCH 08/13] x --- langgraph/__init__.py | 3 +++ langgraph/version.py | 9 +++++++++ 2 files changed, 12 insertions(+) create mode 100644 langgraph/version.py diff --git a/langgraph/__init__.py b/langgraph/__init__.py index e69de29bb..959f4ab2e 100644 --- a/langgraph/__init__.py +++ b/langgraph/__init__.py @@ -0,0 +1,3 @@ +from langgraph.version import __version__ + +__all__ = ["__version__"] diff --git a/langgraph/version.py b/langgraph/version.py new file mode 100644 index 000000000..ac7aeef6f --- /dev/null +++ b/langgraph/version.py @@ -0,0 +1,9 @@ +"""Main entrypoint into package.""" +from importlib import metadata + +try: + __version__ = metadata.version(__package__) +except metadata.PackageNotFoundError: + # Case where package metadata is not available. + __version__ = "" +del metadata # optional, avoids polluting the results of dir(__package__) From ea26b42a7de5697b46e28c46a6f5957dc52b648c Mon Sep 17 00:00:00 2001 From: Repkit Date: Fri, 16 Feb 2024 22:18:29 +0200 Subject: [PATCH 09/13] fix base.ipynb to install langchainhub this is requiered to load the prompt from hub --- examples/agent_executor/base.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/agent_executor/base.ipynb b/examples/agent_executor/base.ipynb index 87fcbee9a..61ecd5b1a 100644 --- a/examples/agent_executor/base.ipynb +++ b/examples/agent_executor/base.ipynb @@ -26,7 +26,7 @@ "metadata": {}, "outputs": [], "source": [ - "!pip install --quiet -U langchain langchain_openai tavily-python" + "!pip install --quiet -U langchain langchain_openai langchainhub langgraph tavily-python" ] }, { From a21380dd267c5c0c436d9d2e8c731e20cc14da09 Mon Sep 17 00:00:00 2001 From: Karen Javadyan Date: Sun, 18 Feb 2024 22:40:15 +0400 Subject: [PATCH 10/13] fix typo --- examples/rag/langgraph_agentic_rag.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/rag/langgraph_agentic_rag.ipynb b/examples/rag/langgraph_agentic_rag.ipynb index 20b1e9132..07ca74be3 100644 --- a/examples/rag/langgraph_agentic_rag.ipynb +++ b/examples/rag/langgraph_agentic_rag.ipynb @@ -19,7 +19,7 @@ "\n", "[Retrieval Agents](https://python.langchain.com/docs/use_cases/question_answering/conversational_retrieval_agents) are useful when we want to make decisions about whether to retrieve from an index.\n", "\n", - "To implement a retrieval agent, we simple need to give an LLM access to a retrier tool.\n", + "To implement a retrieval agent, we simple need to give an LLM access to a retriever tool.\n", "\n", "We can incorperate this into [LangGraph](https://python.langchain.com/docs/langgraph).\n", "\n", From c444ef0a2cf790502cb36402c1f5d04966921e96 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 19 Feb 2024 08:39:21 -0800 Subject: [PATCH 11/13] Update base.ipynb --- examples/agent_executor/base.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/agent_executor/base.ipynb b/examples/agent_executor/base.ipynb index 61ecd5b1a..6a40577ed 100644 --- a/examples/agent_executor/base.ipynb +++ b/examples/agent_executor/base.ipynb @@ -26,7 +26,7 @@ "metadata": {}, "outputs": [], "source": [ - "!pip install --quiet -U langchain langchain_openai langchainhub langgraph tavily-python" + "!pip install --quiet -U langchain langchain_openai langchainhub tavily-python" ] }, { From e4bfa8603d602a184cb0a00135ff8ff6ca5ae11d Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 19 Feb 2024 09:12:08 -0800 Subject: [PATCH 12/13] Support multiple tool calls, Lint --- langgraph/prebuilt/chat_agent_executor.py | 120 +++--- tests/test_pregel.py | 408 +++++++-------------- tests/test_pregel_async.py | 423 +++++++--------------- 3 files changed, 328 insertions(+), 623 deletions(-) diff --git a/langgraph/prebuilt/chat_agent_executor.py b/langgraph/prebuilt/chat_agent_executor.py index c99d56e65..fcd4d6806 100644 --- a/langgraph/prebuilt/chat_agent_executor.py +++ b/langgraph/prebuilt/chat_agent_executor.py @@ -1,17 +1,23 @@ import json import operator -from typing import Annotated, Sequence, TypedDict +from typing import Annotated, Sequence, TypedDict, Union -from langchain_core.agents import AgentAction +from langchain_core.language_models import LanguageModelLike from langchain_core.messages import BaseMessage, FunctionMessage, ToolMessage from langchain_core.runnables import RunnableLambda -from langchain_core.utils.function_calling import convert_to_openai_function, convert_to_openai_tool +from langchain_core.tools import BaseTool +from langchain_core.utils.function_calling import ( + convert_to_openai_function, + convert_to_openai_tool, +) from langgraph.graph import END, StateGraph -from langgraph.prebuilt.tool_executor import ToolExecutor +from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation -def create_function_calling_executor(model, tools): +def create_function_calling_executor( + model: LanguageModelLike, tools: Union[ToolExecutor, Sequence[BaseTool]] +): if isinstance(tools, ToolExecutor): tool_executor = tools tool_classes = tools.tools @@ -20,8 +26,15 @@ def create_function_calling_executor(model, tools): tool_classes = tools model = model.bind(functions=[convert_to_openai_function(t) for t in tool_classes]) + # We create the AgentState that we will pass around + # This simply involves a list of messages + # We want steps to return messages to append to the list + # So we annotate the messages attribute with operator.add + class AgentState(TypedDict): + messages: Annotated[Sequence[BaseMessage], operator.add] + # Define the function that determines whether to continue or not - def should_continue(state): + def should_continue(state: AgentState): messages = state["messages"] last_message = messages[-1] # If there is no function call, then we finish @@ -32,34 +45,33 @@ def create_function_calling_executor(model, tools): return "continue" # Define the function that calls the model - def call_model(state): + def call_model(state: AgentState): messages = state["messages"] response = model.invoke(messages) # We return a list, because this will get added to the existing list return {"messages": [response]} - async def acall_model(state): + async def acall_model(state: AgentState): messages = state["messages"] response = await model.ainvoke(messages) # 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): + 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 AgentAction( + return ToolInvocation( tool=last_message.additional_kwargs["function_call"]["name"], tool_input=json.loads( last_message.additional_kwargs["function_call"]["arguments"] ), - log="", ) - def call_tool(state): + def call_tool(state: AgentState): action = _get_action(state) # We call the tool_executor and get back a response response = tool_executor.invoke(action) @@ -68,7 +80,7 @@ def create_function_calling_executor(model, tools): # We return a list, because this will get added to the existing list return {"messages": [function_message]} - async def acall_tool(state): + async def acall_tool(state: AgentState): action = _get_action(state) # We call the tool_executor and get back a response response = await tool_executor.ainvoke(action) @@ -77,13 +89,6 @@ def create_function_calling_executor(model, tools): # We return a list, because this will get added to the existing list return {"messages": [function_message]} - # We create the AgentState that we will pass around - # This simply involves a list of messages - # We want steps to return messages to append to the list - # So we annotate the messages attribute with operator.add - class AgentState(TypedDict): - messages: Annotated[Sequence[BaseMessage], operator.add] - # Define a new graph workflow = StateGraph(AgentState) @@ -125,17 +130,27 @@ def create_function_calling_executor(model, tools): # meaning you can use it as you would any other runnable return workflow.compile() -def create_tool_calling_executor(model, tools): + +def create_tool_calling_executor( + model: LanguageModelLike, tools: Union[ToolExecutor, Sequence[BaseTool]] +): 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_tool(t) for t in tool_classes]) + model = model.bind(tools=[convert_to_openai_tool(t) for t in tool_classes]) + + # We create the AgentState that we will pass around + # This simply involves a list of messages + # We want steps to return messages to append to the list + # So we annotate the messages attribute with operator.add + class AgentState(TypedDict): + messages: Annotated[Sequence[BaseMessage], operator.add] # Define the function that determines whether to continue or not - def should_continue(state): + def should_continue(state: AgentState): messages = state["messages"] last_message = messages[-1] # If there is no function call, then we finish @@ -146,57 +161,62 @@ def create_tool_calling_executor(model, tools): return "continue" # Define the function that calls the model - def call_model(state): + def call_model(state: AgentState): messages = state["messages"] response = model.invoke(messages) # We return a list, because this will get added to the existing list return {"messages": [response]} - async def acall_model(state): + async def acall_model(state: AgentState): messages = state["messages"] response = await model.ainvoke(messages) # 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): + def _get_actions(state: AgentState): messages = state["messages"] # Based on the continue condition # we know the last message involves a tool call last_message = messages[-1] - # We construct an AgentAction from the tool_calls - return AgentAction( - tool=last_message.additional_kwargs["tool_calls"][0]["function"]["name"], - tool_input=json.loads( - last_message.additional_kwargs["tool_calls"][0]["function"]["arguments"] - ), - log=last_message.additional_kwargs["tool_calls"][0]["id"], + # We construct an AgentAction from each of the tool_calls + return ( + [ + ToolInvocation( + tool=tool_call["function"]["name"], + tool_input=json.loads(tool_call["function"]["arguments"]), + ) + for tool_call in last_message.additional_kwargs["tool_calls"] + ], + [ + tool_call["id"] + for tool_call in last_message.additional_kwargs["tool_calls"] + ], ) - def call_tool(state): - action = _get_action(state) + def call_tool(state: AgentState): + actions, ids = _get_actions(state) # We call the tool_executor and get back a response - response = tool_executor.invoke(action) + responses = tool_executor.batch(actions) # We use the response to create a FunctionMessage - tool_message = ToolMessage(content=str(response), tool_call_id=action.log) + tool_messages = [ + ToolMessage(content=str(response), tool_call_id=id) + for response, id in zip(responses, ids) + ] # We return a list, because this will get added to the existing list - return {"messages": [tool_message]} + return {"messages": tool_messages} - async def acall_tool(state): - action = _get_action(state) + async def acall_tool(state: AgentState): + actions, ids = _get_actions(state) # We call the tool_executor and get back a response - response = await tool_executor.ainvoke(action) + responses = await tool_executor.abatch(actions) # We use the response to create a FunctionMessage - tool_message = ToolMessage(content=str(response), tool_call_id=action.log) + tool_messages = [ + ToolMessage(content=str(response), tool_call_id=id) + for response, id in zip(responses, ids) + ] # We return a list, because this will get added to the existing list - return {"messages": [tool_message]} - - # We create the AgentState that we will pass around - # This simply involves a list of messages - # We want steps to return messages to append to the list - # So we annotate the messages attribute with operator.add - class AgentState(TypedDict): - messages: Annotated[Sequence[BaseMessage], operator.add] + return {"messages": tool_messages} # Define a new graph workflow = StateGraph(AgentState) diff --git a/tests/test_pregel.py b/tests/test_pregel.py index 4b1a78659..13427b9c6 100644 --- a/tests/test_pregel.py +++ b/tests/test_pregel.py @@ -20,11 +20,15 @@ from langgraph.checkpoint.sqlite import SqliteSaver from langgraph.graph import END, Graph from langgraph.graph.message import MessageGraph from langgraph.graph.state import StateGraph -from langgraph.prebuilt.chat_agent_executor import create_function_calling_executor, create_tool_calling_executor +from langgraph.prebuilt.chat_agent_executor import ( + create_function_calling_executor, + create_tool_calling_executor, +) from langgraph.prebuilt.tool_executor import ToolExecutor from langgraph.pregel import Channel, GraphRecursionError, Pregel from langgraph.pregel.reserved import ReservedChannels + def test_invoke_single_process_in_out(mocker: MockerFixture) -> None: add_one = mocker.Mock(side_effect=lambda x: x + 1) chain = Channel.subscribe_to("input") | add_one | Channel.write_to("output") @@ -1060,6 +1064,7 @@ def test_conditional_graph_state() -> None: }, ] + def test_prebuilt_tool_chat() -> None: from langchain.chat_models.fake import FakeMessagesListChatModel from langchain_community.tools import tool @@ -1082,27 +1087,39 @@ def test_prebuilt_tool_chat() -> None: AIMessage( content="", additional_kwargs={ - "tool_calls": [{ - "id": "tool_call123", - "type": "function", - "function":{ - "name": "search_api", - "arguments": json.dumps("query"), + "tool_calls": [ + { + "id": "tool_call123", + "type": "function", + "function": { + "name": "search_api", + "arguments": json.dumps("query"), + }, } - }] + ] }, ), AIMessage( content="", additional_kwargs={ - "tool_calls": [{ - "id": "tool_call234", - "type": "function", - "function":{ - "name": "search_api", - "arguments": json.dumps("another"), - } - }] + "tool_calls": [ + { + "id": "tool_call234", + "type": "function", + "function": { + "name": "search_api", + "arguments": json.dumps("another"), + }, + }, + { + "id": "tool_call567", + "type": "function", + "function": { + "name": "search_api", + "arguments": '"a third one"', + }, + }, + ] }, ), AIMessage(content="answer"), @@ -1110,7 +1127,7 @@ def test_prebuilt_tool_chat() -> None: ), tools, ) - + assert app.invoke( {"messages": [HumanMessage(content="what is weather in sf")]} ) == { @@ -1119,31 +1136,44 @@ def test_prebuilt_tool_chat() -> None: AIMessage( content="", additional_kwargs={ - "tool_calls": [{ - "id": "tool_call123", - "type": "function", - "function":{ - "name": "search_api", - "arguments": "\"query\"", + "tool_calls": [ + { + "id": "tool_call123", + "type": "function", + "function": { + "name": "search_api", + "arguments": '"query"', + }, } - }] + ] }, ), ToolMessage(content="result for query", tool_call_id="tool_call123"), AIMessage( content="", additional_kwargs={ - "tool_calls": [{ - "id": "tool_call234", - "type": "function", - "function":{ - "name": "search_api", - "arguments": "\"another\"", - } - }] - }, + "tool_calls": [ + { + "id": "tool_call234", + "type": "function", + "function": { + "name": "search_api", + "arguments": '"another"', + }, + }, + { + "id": "tool_call567", + "type": "function", + "function": { + "name": "search_api", + "arguments": '"a third one"', + }, + }, + ] + }, ), ToolMessage(content="result for another", tool_call_id="tool_call234"), + ToolMessage(content="result for a third one", tool_call_id="tool_call567"), AIMessage(content="answer"), ] } @@ -1157,14 +1187,16 @@ def test_prebuilt_tool_chat() -> None: AIMessage( content="", additional_kwargs={ - "tool_calls": [{ + "tool_calls": [ + { "id": "tool_call123", "type": "function", - "function":{ + "function": { "name": "search_api", - "arguments": "\"query\"", - } - }] + "arguments": '"query"', + }, + } + ] }, ) ] @@ -1182,16 +1214,26 @@ def test_prebuilt_tool_chat() -> None: "messages": [ AIMessage( content="", - additional_kwargs={ - "tool_calls": [{ + additional_kwargs={ + "tool_calls": [ + { "id": "tool_call234", "type": "function", - "function":{ + "function": { "name": "search_api", - "arguments": "\"another\"", - } - }] - }, + "arguments": '"another"', + }, + }, + { + "id": "tool_call567", + "type": "function", + "function": { + "name": "search_api", + "arguments": '"a third one"', + }, + }, + ] + }, ) ] } @@ -1199,7 +1241,12 @@ def test_prebuilt_tool_chat() -> None: { "action": { "messages": [ - ToolMessage(content="result for another", tool_call_id="tool_call234") + ToolMessage( + content="result for another", tool_call_id="tool_call234" + ), + ToolMessage( + content="result for a third one", tool_call_id="tool_call567" + ), ] } }, @@ -1211,258 +1258,56 @@ def test_prebuilt_tool_chat() -> None: AIMessage( content="", additional_kwargs={ - "tool_calls": [{ + "tool_calls": [ + { "id": "tool_call123", "type": "function", - "function":{ + "function": { "name": "search_api", - "arguments": "\"query\"", - } - }] - }, + "arguments": '"query"', + }, + } + ] + }, + ), + ToolMessage( + content="result for query", tool_call_id="tool_call123" ), - ToolMessage(content="result for query", tool_call_id="tool_call123"), AIMessage( content="", additional_kwargs={ - "tool_calls": [{ + "tool_calls": [ + { "id": "tool_call234", "type": "function", - "function":{ + "function": { "name": "search_api", - "arguments": "\"another\"", - } - }] + "arguments": '"another"', + }, + }, + { + "id": "tool_call567", + "type": "function", + "function": { + "name": "search_api", + "arguments": '"a third one"', + }, + }, + ] }, ), - ToolMessage(content="result for another", tool_call_id="tool_call234"), + ToolMessage( + content="result for another", tool_call_id="tool_call234" + ), + ToolMessage( + content="result for a third one", tool_call_id="tool_call567" + ), AIMessage(content="answer"), ] } }, ] -def test_tool_message_graph() -> None: - from langchain.chat_models.fake import FakeMessagesListChatModel - from langchain_community.tools import tool - from langchain_core.agents import AgentAction - from langchain_core.messages import AIMessage, ToolMessage, HumanMessage - - 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] - - model = FakeFuntionChatModel( - responses=[ - AIMessage( - content="", - additional_kwargs={ - "tool_calls": [{ - "id": "tool_call123", - "type": "function", - "function":{ - "name": "search_api", - "arguments": json.dumps("query"), - } - }] - }, - ), - AIMessage( - content="", - additional_kwargs={ - "tool_calls": [{ - "id": "tool_call234", - "type": "function", - "function":{ - "name": "search_api", - "arguments": json.dumps("another"), - } - }] - }, - ), - AIMessage(content="answer"), - ] - ) - - tool_executor = ToolExecutor(tools) - - # Define the function that determines whether to continue or not - def should_continue(messages): - last_message = messages[-1] - # If there is no function call, then we finish - if "tool_calls" not in last_message.additional_kwargs: - return "end" - # Otherwise if there is, we continue - else: - return "continue" - - def call_tool(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 - action = AgentAction( - tool=last_message.additional_kwargs["tool_calls"][0]["function"]["name"], - tool_input=json.loads( - last_message.additional_kwargs["tool_calls"][0]["function"]["arguments"] - ), - log=last_message.additional_kwargs["tool_calls"][0]["id"], - ) - # We call the tool_executor and get back a response - response = tool_executor.invoke(action) - # We use the response to create a ToolMessage - return ToolMessage(content=str(response), tool_call_id=action.log) - - # Define a new graph - workflow = MessageGraph() - - # Define the two nodes we will cycle between - workflow.add_node("agent", model) - workflow.add_node("action", call_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": "action", - # 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("action", "agent") - - # Finally, we compile it! - # This compiles it into a LangChain Runnable, - # meaning you can use it as you would any other runnable - app = workflow.compile() - - assert app.invoke(HumanMessage(content="what is weather in sf")) == [ - HumanMessage(content="what is weather in sf"), - AIMessage( - content="", - additional_kwargs={ - "tool_calls": [{ - "id": "tool_call123", - "type": "function", - "function":{ - "name": "search_api", - "arguments": "\"query\"", - } - }] - }, - ), - ToolMessage(content="result for query", tool_call_id="tool_call123"), - AIMessage( - content="", - additional_kwargs={ - "tool_calls": [{ - "id": "tool_call234", - "type": "function", - "function":{ - "name": "search_api", - "arguments": "\"another\"", - } - }] - }, - ), - ToolMessage(content="result for another", tool_call_id="tool_call234"), - AIMessage(content="answer"), - ] - - assert [*app.stream([HumanMessage(content="what is weather in sf")])] == [ - { - "agent": AIMessage( - content="", - additional_kwargs={ - "tool_calls": [{ - "id": "tool_call123", - "type": "function", - "function":{ - "name": "search_api", - "arguments": "\"query\"", - } - }] - }, - ) - }, - {"action": ToolMessage(content="result for query", tool_call_id="tool_call123")}, - { - "agent": AIMessage( - content="", - additional_kwargs={ - "tool_calls": [{ - "id": "tool_call234", - "type": "function", - "function":{ - "name": "search_api", - "arguments": "\"another\"", - } - }] - }, - ) - }, - {"action": ToolMessage(content="result for another", tool_call_id="tool_call234")}, - {"agent": AIMessage(content="answer")}, - { - "__end__": [ - HumanMessage(content="what is weather in sf"), - AIMessage( - content="", - additional_kwargs={ - "tool_calls": [{ - "id": "tool_call123", - "type": "function", - "function":{ - "name": "search_api", - "arguments": "\"query\"", - } - }] - }, - ), - ToolMessage(content="result for query", tool_call_id="tool_call123"), - AIMessage( - content="", - additional_kwargs={ - "tool_calls": [{ - "id": "tool_call234", - "type": "function", - "function":{ - "name": "search_api", - "arguments": "\"another\"", - } - }] - }, - ), - ToolMessage(content="result for another", tool_call_id="tool_call234"), - AIMessage(content="answer"), - ] - }, - ] def test_prebuilt_chat() -> None: from langchain.chat_models.fake import FakeMessagesListChatModel @@ -1608,6 +1453,7 @@ def test_prebuilt_chat() -> None: }, ] + def test_message_graph() -> None: from langchain.chat_models.fake import FakeMessagesListChatModel from langchain_community.tools import tool @@ -1781,4 +1627,4 @@ def test_message_graph() -> None: AIMessage(content="answer"), ] }, - ] \ No newline at end of file + ] diff --git a/tests/test_pregel_async.py b/tests/test_pregel_async.py index 4f09e372a..30a344a95 100644 --- a/tests/test_pregel_async.py +++ b/tests/test_pregel_async.py @@ -26,7 +26,10 @@ from langgraph.checkpoint.aiosqlite import AsyncSqliteSaver from langgraph.checkpoint.memory import MemorySaver from langgraph.graph import END, Graph, StateGraph from langgraph.graph.message import MessageGraph -from langgraph.prebuilt.chat_agent_executor import create_function_calling_executor, create_tool_calling_executor +from langgraph.prebuilt.chat_agent_executor import ( + create_function_calling_executor, + create_tool_calling_executor, +) from langgraph.prebuilt.tool_executor import ToolExecutor from langgraph.pregel import Channel, GraphRecursionError, Pregel from langgraph.pregel.reserved import ReservedChannels @@ -1114,7 +1117,7 @@ async def test_conditional_graph_state() -> None: async def test_prebuilt_tool_chat() -> None: from langchain.chat_models.fake import FakeMessagesListChatModel from langchain_community.tools import tool - from langchain_core.messages import AIMessage, ToolMessage, HumanMessage + from langchain_core.messages import AIMessage, HumanMessage, ToolMessage class FakeFuntionChatModel(FakeMessagesListChatModel): def bind_functions(self, functions: list): @@ -1133,27 +1136,39 @@ async def test_prebuilt_tool_chat() -> None: AIMessage( content="", additional_kwargs={ - "tool_calls": [{ - "id": "tool_call123", - "type": "function", - "function":{ - "name": "search_api", - "arguments": json.dumps("query"), + "tool_calls": [ + { + "id": "tool_call123", + "type": "function", + "function": { + "name": "search_api", + "arguments": json.dumps("query"), + }, } - }] + ] }, ), AIMessage( content="", additional_kwargs={ - "tool_calls": [{ - "id": "tool_call234", - "type": "function", - "function":{ - "name": "search_api", - "arguments": json.dumps("another"), - } - }] + "tool_calls": [ + { + "id": "tool_call234", + "type": "function", + "function": { + "name": "search_api", + "arguments": json.dumps("another"), + }, + }, + { + "id": "tool_call567", + "type": "function", + "function": { + "name": "search_api", + "arguments": '"a third one"', + }, + }, + ] }, ), AIMessage(content="answer"), @@ -1170,31 +1185,44 @@ async def test_prebuilt_tool_chat() -> None: AIMessage( content="", additional_kwargs={ - "tool_calls": [{ - "id": "tool_call123", - "type": "function", - "function":{ - "name": "search_api", - "arguments": "\"query\"", + "tool_calls": [ + { + "id": "tool_call123", + "type": "function", + "function": { + "name": "search_api", + "arguments": '"query"', + }, } - }] - }, + ] + }, ), ToolMessage(content="result for query", tool_call_id="tool_call123"), AIMessage( content="", additional_kwargs={ - "tool_calls": [{ - "id": "tool_call234", - "type": "function", - "function":{ - "name": "search_api", - "arguments": "\"another\"", - } - }] + "tool_calls": [ + { + "id": "tool_call234", + "type": "function", + "function": { + "name": "search_api", + "arguments": '"another"', + }, + }, + { + "id": "tool_call567", + "type": "function", + "function": { + "name": "search_api", + "arguments": '"a third one"', + }, + }, + ] }, ), ToolMessage(content="result for another", tool_call_id="tool_call234"), + ToolMessage(content="result for a third one", tool_call_id="tool_call567"), AIMessage(content="answer"), ] } @@ -1211,15 +1239,17 @@ async def test_prebuilt_tool_chat() -> None: AIMessage( content="", additional_kwargs={ - "tool_calls": [{ - "id": "tool_call123", - "type": "function", - "function":{ - "name": "search_api", - "arguments": "\"query\"", + "tool_calls": [ + { + "id": "tool_call123", + "type": "function", + "function": { + "name": "search_api", + "arguments": '"query"', + }, } - }] - }, + ] + }, ) ] } @@ -1237,14 +1267,24 @@ async def test_prebuilt_tool_chat() -> None: AIMessage( content="", additional_kwargs={ - "tool_calls": [{ - "id": "tool_call234", - "type": "function", - "function":{ - "name": "search_api", - "arguments": "\"another\"", - } - }] + "tool_calls": [ + { + "id": "tool_call234", + "type": "function", + "function": { + "name": "search_api", + "arguments": '"another"', + }, + }, + { + "id": "tool_call567", + "type": "function", + "function": { + "name": "search_api", + "arguments": '"a third one"', + }, + }, + ] }, ) ] @@ -1253,7 +1293,12 @@ async def test_prebuilt_tool_chat() -> None: { "action": { "messages": [ - ToolMessage(content="result for another", tool_call_id="tool_call234") + ToolMessage( + content="result for another", tool_call_id="tool_call234" + ), + ToolMessage( + content="result for a third one", tool_call_id="tool_call567" + ), ] } }, @@ -1265,31 +1310,50 @@ async def test_prebuilt_tool_chat() -> None: AIMessage( content="", additional_kwargs={ - "tool_calls": [{ - "id": "tool_call123", - "type": "function", - "function":{ - "name": "search_api", - "arguments": "\"query\"", + "tool_calls": [ + { + "id": "tool_call123", + "type": "function", + "function": { + "name": "search_api", + "arguments": '"query"', + }, } - }] + ] }, ), - ToolMessage(content="result for query", tool_call_id="tool_call123"), + ToolMessage( + content="result for query", tool_call_id="tool_call123" + ), AIMessage( content="", additional_kwargs={ - "tool_calls": [{ - "id": "tool_call234", - "type": "function", - "function":{ - "name": "search_api", - "arguments": "\"another\"", - } - }] + "tool_calls": [ + { + "id": "tool_call234", + "type": "function", + "function": { + "name": "search_api", + "arguments": '"another"', + }, + }, + { + "id": "tool_call567", + "type": "function", + "function": { + "name": "search_api", + "arguments": '"a third one"', + }, + }, + ] }, ), - ToolMessage(content="result for another", tool_call_id="tool_call234"), + ToolMessage( + content="result for another", tool_call_id="tool_call234" + ), + ToolMessage( + content="result for a third one", tool_call_id="tool_call567" + ), AIMessage(content="answer"), ] } @@ -1297,231 +1361,6 @@ async def test_prebuilt_tool_chat() -> None: ] -async def test_message_tool_graph() -> None: - from langchain.chat_models.fake import FakeMessagesListChatModel - from langchain_community.tools import tool - from langchain_core.agents import AgentAction - from langchain_core.messages import AIMessage, ToolMessage, HumanMessage - - 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] - - model = FakeFuntionChatModel( - responses=[ - AIMessage( - content="", - additional_kwargs={ - "tool_calls": [{ - "id": "tool_call123", - "type": "function", - "function":{ - "name": "search_api", - "arguments": json.dumps("query"), - } - }] - }, - ), - AIMessage( - content="", - additional_kwargs={ - "tool_calls": [{ - "id": "tool_call234", - "type": "function", - "function":{ - "name": "search_api", - "arguments": json.dumps("another"), - } - }] - }, - ), - AIMessage(content="answer"), - ] - ) - - tool_executor = ToolExecutor(tools) - - # Define the function that determines whether to continue or not - def should_continue(messages): - last_message = messages[-1] - # If there is no function call, then we finish - if "tool_calls" not in last_message.additional_kwargs: - return "end" - # Otherwise if there is, we continue - else: - return "continue" - - async def call_tool(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 - action = AgentAction( - tool=last_message.additional_kwargs["tool_calls"][0]["function"]["name"], - tool_input=json.loads( - last_message.additional_kwargs["tool_calls"][0]["function"]["arguments"] - ), - log=last_message.additional_kwargs["tool_calls"][0]["id"], - ) - # We call the tool_executor and get back a response - response = await tool_executor.ainvoke(action) - # We use the response to create a FunctionMessage - return ToolMessage(content=str(response), tool_call_id=action.log) - - # Define a new graph - workflow = MessageGraph() - - # Define the two nodes we will cycle between - workflow.add_node("agent", model) - workflow.add_node("action", call_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": "action", - # 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("action", "agent") - - # Finally, we compile it! - # This compiles it into a LangChain Runnable, - # meaning you can use it as you would any other runnable - app = workflow.compile() - - assert await app.ainvoke(HumanMessage(content="what is weather in sf")) == [ - HumanMessage(content="what is weather in sf"), - AIMessage( - content="", - additional_kwargs={ - "tool_calls": [{ - "id": "tool_call123", - "type": "function", - "function":{ - "name": "search_api", - "arguments": "\"query\"", - } - }] - }, - ), - ToolMessage(content="result for query", tool_call_id="tool_call123"), - AIMessage( - content="", - additional_kwargs={ - "tool_calls": [{ - "id": "tool_call234", - "type": "function", - "function":{ - "name": "search_api", - "arguments": "\"another\"", - } - }] - }, - ), - ToolMessage(content="result for another", tool_call_id="tool_call234"), - AIMessage(content="answer"), - ] - - assert [ - c async for c in app.astream([HumanMessage(content="what is weather in sf")]) - ] == [ - { - "agent": AIMessage( - content="", - additional_kwargs={ - "tool_calls": [{ - "id": "tool_call123", - "type": "function", - "function":{ - "name": "search_api", - "arguments": "\"query\"", - } - }] - }, - ) - }, - {"action": ToolMessage(content="result for query", tool_call_id="tool_call123")}, - { - "agent": AIMessage( - content="", - additional_kwargs={ - "tool_calls": [{ - "id": "tool_call234", - "type": "function", - "function":{ - "name": "search_api", - "arguments": "\"another\"", - } - }] - }, - ) - }, - {"action": ToolMessage(content="result for another", tool_call_id="tool_call234")}, - {"agent": AIMessage(content="answer")}, - { - "__end__": [ - HumanMessage(content="what is weather in sf"), - AIMessage( - content="", - additional_kwargs={ - "tool_calls": [{ - "id": "tool_call123", - "type": "function", - "function":{ - "name": "search_api", - "arguments": "\"query\"", - } - }] - }, - ), - ToolMessage(content="result for query", tool_call_id="tool_call123"), - AIMessage( - content="", - additional_kwargs={ - "tool_calls": [{ - "id": "tool_call234", - "type": "function", - "function":{ - "name": "search_api", - "arguments": "\"another\"", - } - }] - }, - ), - ToolMessage(content="result for another", tool_call_id="tool_call234"), - AIMessage(content="answer"), - ] - }, - ] - - async def test_prebuilt_chat() -> None: from langchain.chat_models.fake import FakeMessagesListChatModel from langchain_community.tools import tool From cadb852086c61e4c06adcee1da82612174898570 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 19 Feb 2024 09:20:39 -0800 Subject: [PATCH 13/13] Undo change to readme --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f60d7dd15..d4990cd1f 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ def should_continue(state): messages = state['messages'] last_message = messages[-1] # If there is no function call, then we finish - if "tool_calls" not in last_message.additional_kwargs: + if "function_call" not in last_message.additional_kwargs: return "end" # Otherwise if there is, we continue else: @@ -171,10 +171,10 @@ def call_tool(state): # Based on the continue condition # we know the last message involves a function call last_message = messages[-1] - # We construct an ToolInvocation from the tool_calls + # We construct an ToolInvocation from the function_call action = ToolInvocation( - tool=last_message.additional_kwargs["tool_calls"][0]["function"]["name"], - tool_input=json.loads(last_message.additional_kwargs["tool_calls"][0]["function"]["arguments"]), + tool=last_message.additional_kwargs["function_call"]["name"], + tool_input=json.loads(last_message.additional_kwargs["function_call"]["arguments"]), ) # We call the tool_executor and get back a response response = tool_executor.invoke(action) @@ -474,7 +474,6 @@ For a walkthrough on how to do that, see [this documentation](https://github.com LangGraph comes with built-in support for human-in-the-loop workflows. This is useful when you want to have a human review the current state before proceeding to a particular node. For a walkthrough on how to do that, see [this documentation](https://github.com/langchain-ai/langgraph/blob/main/examples/human-in-the-loop.ipynb) - ### Planning Agent Examples The following notebooks implement agent architectures prototypical of the "plan-and-execute" style, where an LLM planner decomposes a user request into a program, an executor executes the program, and an LLM synthesizes a response (and/or dynamically replans) based on the program outputs. @@ -483,7 +482,6 @@ The following notebooks implement agent architectures prototypical of the "plan- - [Reasoning without Observation](https://github.com/langchain-ai/langgraph/blob/main/examples/rewoo/rewoo.ipynb): planner generates a task list whose observations are saved as **variables**. Variables can be used in subsequent tasks to reduce the need for further re-planning. Based on the [ReWOO](https://arxiv.org/abs/2305.18323) paper by Xu, et. al. - [LLMCompiler](https://github.com/langchain-ai/langgraph/blob/main/examples/llm-compiler/LLMCompiler.ipynb): planner generates a **DAG** of tasks with variable responses. Tasks are **streamed** and executed eagerly to minimize tool execution runtime. Based on the [paper](https://arxiv.org/abs/2312.04511) by Kim, et. al. - ### Multi-agent Examples - [Multi-agent collaboration](https://github.com/langchain-ai/langgraph/blob/main/examples/multi_agent/multi-agent-collaboration.ipynb): how to create two agents that work together to accomplish a task