From 333ad65cac9500b8784f15e99fc3981fb2fec382 Mon Sep 17 00:00:00 2001 From: Bagatur Date: Tue, 23 Jan 2024 13:29:23 -0800 Subject: [PATCH 1/4] patch: make conditional_edge_mapping optional --- langgraph/graph/graph.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/langgraph/graph/graph.py b/langgraph/graph/graph.py index 4ebd8144e..7fc239b43 100644 --- a/langgraph/graph/graph.py +++ b/langgraph/graph/graph.py @@ -17,11 +17,14 @@ END = "__end__" class Branch(NamedTuple): condition: Callable[..., str] - ends: dict[str, str] + ends: Optional[dict[str, str]] def runnable(self, input: Any) -> Runnable: result = self.condition(input) - destination = self.ends[result] + if self.ends: + destination = self.ends[result] + else: + destination = result return Channel.write_to(f"{destination}:inbox" if destination != END else END) @@ -59,15 +62,21 @@ class Graph: self, start_key: str, condition: Callable[..., str], - conditional_edge_mapping: Dict[str, str], + conditional_edge_mapping: Optional[Dict[str, str]] = None, ) -> None: if start_key not in self.nodes: raise ValueError(f"Need to add_node `{start_key}` first") if iscoroutinefunction(condition): raise ValueError("Condition cannot be a coroutine function") - for destination in conditional_edge_mapping.values(): - if destination not in self.nodes and destination != END: - raise ValueError(f"Need to add_node `{destination}` first") + if conditional_edge_mapping and set( + conditional_edge_mapping.values() + ).difference(self.nodes): + raise ValueError( + f"Missing nodes which are in conditional edge mapping. Mapping " + f"contains possible destinations: " + f"{list(conditional_edge_mapping.values())}. Possible nodes are " + f"{list(self.nodes.keys())}." + ) self.branches[start_key].append(Branch(condition, conditional_edge_mapping)) From 936b2637ce564602b7e1249d8a840d426c830fa3 Mon Sep 17 00:00:00 2001 From: Bagatur Date: Tue, 23 Jan 2024 16:38:11 -0800 Subject: [PATCH 2/4] fmt --- examples/streaming-tokens.ipynb | 141 +++++++++++++------------------- langgraph/graph/graph.py | 33 +++++--- 2 files changed, 76 insertions(+), 98 deletions(-) diff --git a/examples/streaming-tokens.ipynb b/examples/streaming-tokens.ipynb index d1dfed602..7293f2394 100644 --- a/examples/streaming-tokens.ipynb +++ b/examples/streaming-tokens.ipynb @@ -109,14 +109,26 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 26, "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], "source": [ "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langchain_core.tools import tool\n", "\n", - "tools = [TavilySearchResults(max_results=1)]" + "@tool\n", + "def multiply(x: int, y: int) -> int:\n", + " \"\"\"Multiply two ints\"\"\"\n", + " return x * y\n", + "\n", + "@tool\n", + "def add(x: int, y: int) -> int:\n", + " \"\"\"Add two ints\"\"\"\n", + " return x + y\n", + "\n", + "\n", + "tools = [multiply, add]" ] }, { @@ -131,7 +143,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 27, "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], @@ -163,7 +175,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 28, "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], @@ -187,7 +199,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 29, "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], @@ -218,7 +230,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 30, "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], @@ -265,7 +277,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 31, "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], @@ -283,6 +295,8 @@ " return \"end\"\n", " # Otherwise if there is, we continue\n", " else:\n", + " if last_message.additional_kwargs[\"function_call\"][\"name\"] == \"add\":\n", + " return \"add\"\n", " return \"continue\"\n", "\n", "# Define the function that calls the model\n", @@ -323,18 +337,21 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 32, "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], "source": [ "from langgraph.graph import StateGraph, END\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", "# Define a new graph\n", "workflow = StateGraph(AgentState)\n", "\n", "# Define the two nodes we will cycle between\n", "workflow.add_node(\"agent\", call_model)\n", "workflow.add_node(\"action\", call_tool)\n", + "workflow.add_node(\"add\", call_tool)\n", "\n", "# Set the entrypoint as `agent`\n", "# This means that this node is the first one called\n", @@ -356,6 +373,7 @@ " {\n", " # If `tools`, then we call the tool node.\n", " \"continue\": \"action\",\n", + " \"add\": \"add\",\n", " # Otherwise we finish.\n", " \"end\": END\n", " }\n", @@ -364,11 +382,14 @@ "# We now add a normal edge from `tools` to `agent`.\n", "# This means that after `tools` is called, `agent` node is called next.\n", "workflow.add_edge('action', 'agent')\n", + "workflow.add_edge('add', END)\n", "\n", "# Finally, we compile it!\n", "# This compiles it into a LangChain Runnable,\n", "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()" + "app = workflow.compile()\n", + "app.interrupt=[\"agent\"]\n", + "app.checkpointer=MemorySaver()" ] }, { @@ -385,90 +406,40 @@ }, { "cell_type": "code", - "execution_count": 10, - "id": "cfd140f0-a5a6-4697-8115-322242f197b5", + "execution_count": 34, + "id": "81633bc1-b136-40e9-b8be-9961adb38183", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "content='' additional_kwargs={'function_call': {'arguments': '', 'name': 'tavily_search_results_json'}}\n", - "content='' additional_kwargs={'function_call': {'arguments': '{\\n', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': ' ', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': ' \"', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': 'query', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': '\":', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': ' \"', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': 'weather', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': ' in', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': ' San', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': ' Francisco', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': '\"\\n', 'name': ''}}\n", - "content='' additional_kwargs={'function_call': {'arguments': '}', 'name': ''}}\n", - "content=''\n", - "content=''\n", - "content='I'\n", - "content=\"'m\"\n", - "content=' sorry'\n", - "content=','\n", - "content=' but'\n", - "content=' I'\n", - "content=' couldn'\n", - "content=\"'t\"\n", - "content=' find'\n", - "content=' the'\n", - "content=' current'\n", - "content=' weather'\n", - "content=' in'\n", - "content=' San'\n", - "content=' Francisco'\n", - "content='.'\n", - "content=' However'\n", - "content=','\n", - "content=' you'\n", - "content=' can'\n", - "content=' check'\n", - "content=' the'\n", - "content=' weather'\n", - "content=' forecast'\n", - "content=' for'\n", - "content=' San'\n", - "content=' Francisco'\n", - "content=' on'\n", - "content=' websites'\n", - "content=' like'\n", - "content=' Weather'\n", - "content='.com'\n", - "content=' or'\n", - "content=' Acc'\n", - "content='u'\n", - "content='Weather'\n", - "content='.'\n", - "content=''\n" - ] + "data": { + "text/plain": [ + "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"x\": 5,\\n \"y\": 4\\n}', 'name': 'multiply'}})]}" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "from langchain_core.messages import HumanMessage\n", - "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", - "async for output in app.astream_log(inputs, include_types=[\"llm\"]):\n", - " # astream_log() yields the requested logs (here LLMs) in JSONPatch format\n", - " for op in output.ops:\n", - " if op[\"path\"] == \"/streamed_output/-\":\n", - " # this is the output from .stream()\n", - " ...\n", - " elif op[\"path\"].startswith(\"/logs/\") and op[\"path\"].endswith(\n", - " \"/streamed_output/-\"\n", - " ):\n", - " # because we chose to only include LLMs, these are LLM tokens\n", - " print(op[\"value\"])" + "inputs = {\"messages\": [HumanMessage(content=\"what is 5 times 4\")]}\n", + "await app.ainvoke(inputs, output_keys=\"agent\", config={\"configurable\": {\"thread_id\": \"foo\"}})" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "08ae8246-11d5-40e1-8567-361e5bef8917", + "metadata": {}, + "outputs": [], + "source": [ + "await app.ainvoke({\"messages\": [HumanMessage(content=\"how about the sum of those two numbers\")]},config={\"configurable\": {\"thread_id\": \"foo\"}})" ] }, { "cell_type": "code", "execution_count": null, - "id": "08ae8246-11d5-40e1-8567-361e5bef8917", + "id": "371cb452-2508-46c8-878f-2249a0dae93a", "metadata": {}, "outputs": [], "source": [] @@ -476,9 +447,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "langgraph", "language": "python", - "name": "python3" + "name": "langgraph" }, "language_info": { "codemirror_mode": { @@ -490,7 +461,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/langgraph/graph/graph.py b/langgraph/graph/graph.py index 7fc239b43..adce5dcef 100644 --- a/langgraph/graph/graph.py +++ b/langgraph/graph/graph.py @@ -90,23 +90,30 @@ class Graph: def validate(self) -> None: all_starts = {src for src, _ in self.edges} | {src for src in self.branches} - all_ends = ( - {end for _, end in self.edges} - | { - end - for branch_list in self.branches.values() - for branch in branch_list - for end in branch.ends.values() - } - | {self.entry_point} - ) - for node in self.nodes: - if node not in all_ends: - raise ValueError(f"Node `{node}` is not reachable") if node not in all_starts: raise ValueError(f"Node `{node}` is a dead-end") + if all( + branch.ends is not None + for branch_list in self.branches.values() + for branch in branch_list + ): + all_ends = ( + {end for _, end in self.edges} + | { + end + for branch_list in self.branches.values() + for branch in branch_list + for end in branch.ends.values() + } + | {self.entry_point} + ) + + for node in self.nodes: + if node not in all_ends: + raise ValueError(f"Node `{node}` is not reachable") + def compile(self, checkpointer: Optional[BaseCheckpointSaver] = None) -> Pregel: self.validate() From 9cbc67fecc86d31360953cbb6671719a0c81bfa0 Mon Sep 17 00:00:00 2001 From: Bagatur Date: Tue, 23 Jan 2024 16:38:26 -0800 Subject: [PATCH 3/4] undo --- examples/streaming-tokens.ipynb | 141 +++++++++++++++++++------------- 1 file changed, 85 insertions(+), 56 deletions(-) diff --git a/examples/streaming-tokens.ipynb b/examples/streaming-tokens.ipynb index 7293f2394..d1dfed602 100644 --- a/examples/streaming-tokens.ipynb +++ b/examples/streaming-tokens.ipynb @@ -109,26 +109,14 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 1, "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], "source": [ "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from langchain_core.tools import tool\n", "\n", - "@tool\n", - "def multiply(x: int, y: int) -> int:\n", - " \"\"\"Multiply two ints\"\"\"\n", - " return x * y\n", - "\n", - "@tool\n", - "def add(x: int, y: int) -> int:\n", - " \"\"\"Add two ints\"\"\"\n", - " return x + y\n", - "\n", - "\n", - "tools = [multiply, add]" + "tools = [TavilySearchResults(max_results=1)]" ] }, { @@ -143,7 +131,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 2, "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], @@ -175,7 +163,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 3, "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], @@ -199,7 +187,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 4, "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], @@ -230,7 +218,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 5, "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], @@ -277,7 +265,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 6, "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], @@ -295,8 +283,6 @@ " return \"end\"\n", " # Otherwise if there is, we continue\n", " else:\n", - " if last_message.additional_kwargs[\"function_call\"][\"name\"] == \"add\":\n", - " return \"add\"\n", " return \"continue\"\n", "\n", "# Define the function that calls the model\n", @@ -337,21 +323,18 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 7, "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], "source": [ "from langgraph.graph import StateGraph, END\n", - "from langgraph.checkpoint.memory import MemorySaver\n", - "\n", "# Define a new graph\n", "workflow = StateGraph(AgentState)\n", "\n", "# Define the two nodes we will cycle between\n", "workflow.add_node(\"agent\", call_model)\n", "workflow.add_node(\"action\", call_tool)\n", - "workflow.add_node(\"add\", call_tool)\n", "\n", "# Set the entrypoint as `agent`\n", "# This means that this node is the first one called\n", @@ -373,7 +356,6 @@ " {\n", " # If `tools`, then we call the tool node.\n", " \"continue\": \"action\",\n", - " \"add\": \"add\",\n", " # Otherwise we finish.\n", " \"end\": END\n", " }\n", @@ -382,14 +364,11 @@ "# We now add a normal edge from `tools` to `agent`.\n", "# This means that after `tools` is called, `agent` node is called next.\n", "workflow.add_edge('action', 'agent')\n", - "workflow.add_edge('add', END)\n", "\n", "# Finally, we compile it!\n", "# This compiles it into a LangChain Runnable,\n", "# meaning you can use it as you would any other runnable\n", - "app = workflow.compile()\n", - "app.interrupt=[\"agent\"]\n", - "app.checkpointer=MemorySaver()" + "app = workflow.compile()" ] }, { @@ -406,40 +385,90 @@ }, { "cell_type": "code", - "execution_count": 34, - "id": "81633bc1-b136-40e9-b8be-9961adb38183", + "execution_count": 10, + "id": "cfd140f0-a5a6-4697-8115-322242f197b5", "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n \"x\": 5,\\n \"y\": 4\\n}', 'name': 'multiply'}})]}" - ] - }, - "execution_count": 34, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "content='' additional_kwargs={'function_call': {'arguments': '', 'name': 'tavily_search_results_json'}}\n", + "content='' additional_kwargs={'function_call': {'arguments': '{\\n', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' ', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' \"', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': 'query', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': '\":', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' \"', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': 'weather', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' in', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' San', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': ' Francisco', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': '\"\\n', 'name': ''}}\n", + "content='' additional_kwargs={'function_call': {'arguments': '}', 'name': ''}}\n", + "content=''\n", + "content=''\n", + "content='I'\n", + "content=\"'m\"\n", + "content=' sorry'\n", + "content=','\n", + "content=' but'\n", + "content=' I'\n", + "content=' couldn'\n", + "content=\"'t\"\n", + "content=' find'\n", + "content=' the'\n", + "content=' current'\n", + "content=' weather'\n", + "content=' in'\n", + "content=' San'\n", + "content=' Francisco'\n", + "content='.'\n", + "content=' However'\n", + "content=','\n", + "content=' you'\n", + "content=' can'\n", + "content=' check'\n", + "content=' the'\n", + "content=' weather'\n", + "content=' forecast'\n", + "content=' for'\n", + "content=' San'\n", + "content=' Francisco'\n", + "content=' on'\n", + "content=' websites'\n", + "content=' like'\n", + "content=' Weather'\n", + "content='.com'\n", + "content=' or'\n", + "content=' Acc'\n", + "content='u'\n", + "content='Weather'\n", + "content='.'\n", + "content=''\n" + ] } ], "source": [ - "inputs = {\"messages\": [HumanMessage(content=\"what is 5 times 4\")]}\n", - "await app.ainvoke(inputs, output_keys=\"agent\", config={\"configurable\": {\"thread_id\": \"foo\"}})" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "id": "08ae8246-11d5-40e1-8567-361e5bef8917", - "metadata": {}, - "outputs": [], - "source": [ - "await app.ainvoke({\"messages\": [HumanMessage(content=\"how about the sum of those two numbers\")]},config={\"configurable\": {\"thread_id\": \"foo\"}})" + "from langchain_core.messages import HumanMessage\n", + "inputs = {\"messages\": [HumanMessage(content=\"what is the weather in sf\")]}\n", + "async for output in app.astream_log(inputs, include_types=[\"llm\"]):\n", + " # astream_log() yields the requested logs (here LLMs) in JSONPatch format\n", + " for op in output.ops:\n", + " if op[\"path\"] == \"/streamed_output/-\":\n", + " # this is the output from .stream()\n", + " ...\n", + " elif op[\"path\"].startswith(\"/logs/\") and op[\"path\"].endswith(\n", + " \"/streamed_output/-\"\n", + " ):\n", + " # because we chose to only include LLMs, these are LLM tokens\n", + " print(op[\"value\"])" ] }, { "cell_type": "code", "execution_count": null, - "id": "371cb452-2508-46c8-878f-2249a0dae93a", + "id": "08ae8246-11d5-40e1-8567-361e5bef8917", "metadata": {}, "outputs": [], "source": [] @@ -447,9 +476,9 @@ ], "metadata": { "kernelspec": { - "display_name": "langgraph", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "langgraph" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -461,7 +490,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.3" + "version": "3.11.1" } }, "nbformat": 4, From 40e4b10689159249d504edfa71cfcb09a1585579 Mon Sep 17 00:00:00 2001 From: Bagatur Date: Fri, 26 Jan 2024 18:47:54 -0800 Subject: [PATCH 4/4] fmt --- langgraph/graph/graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langgraph/graph/graph.py b/langgraph/graph/graph.py index adce5dcef..a0ac4a172 100644 --- a/langgraph/graph/graph.py +++ b/langgraph/graph/graph.py @@ -70,7 +70,7 @@ class Graph: raise ValueError("Condition cannot be a coroutine function") if conditional_edge_mapping and set( conditional_edge_mapping.values() - ).difference(self.nodes): + ).difference([END]).difference(self.nodes): raise ValueError( f"Missing nodes which are in conditional edge mapping. Mapping " f"contains possible destinations: "