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()