From 8fc0eecac118c2ae2b082cfb543114baa9c0e9b2 Mon Sep 17 00:00:00 2001 From: Isaac Francisco <78627776+isahers1@users.noreply.github.com> Date: Thu, 5 Sep 2024 20:37:47 -0700 Subject: [PATCH] [docs]: add subgraph streaming how-to (and some small cleanups) (#1606) * wip * spelling * edit format_namespace --- docs/_scripts/copy_notebooks.py | 1 + docs/docs/cloud/faq/studio.md | 6 +- docs/docs/how-tos/index.md | 1 + docs/mkdocs.yml | 1 + examples/streaming-from-final-node.ipynb | 8 +- examples/streaming-subgraphs.ipynb | 364 +++++++++++++++++++++++ 6 files changed, 376 insertions(+), 5 deletions(-) create mode 100644 examples/streaming-subgraphs.ipynb diff --git a/docs/_scripts/copy_notebooks.py b/docs/_scripts/copy_notebooks.py index 382edbb96..28bd89111 100644 --- a/docs/_scripts/copy_notebooks.py +++ b/docs/_scripts/copy_notebooks.py @@ -26,6 +26,7 @@ "streaming-events-from-within-tools.ipynb", "streaming-events-from-within-tools-without-langchain.ipynb", "streaming-from-final-node.ipynb", + "streaming-subgraphs.ipynb", "persistence.ipynb", "input_output_schema.ipynb", "pass_private_state.ipynb", diff --git a/docs/docs/cloud/faq/studio.md b/docs/docs/cloud/faq/studio.md index 070014ce7..232699315 100644 --- a/docs/docs/cloud/faq/studio.md +++ b/docs/docs/cloud/faq/studio.md @@ -54,7 +54,7 @@ The first way to solve this is to add path maps to your conditional edges. A pat === "Javascript" ```ts - graph.addConditionalEdges("node_a", routingFunction, ["node_b", "node_c"]); + graph.addConditionalEdges("node_a", routingFunction, { true: "node_b", false: "node_c" }); ``` In this case, the routing function returns either True or False, which map to `node_b` and `node_c` respectively. @@ -66,8 +66,8 @@ Instead of passing a path map, you can also be explicit about the typing of your ```python def routing_function(state: GraphState) -> Literal["node_b","node_c"]: if state['some_condition'] == True: - return "node_a" - else: return "node_b" + else: + return "node_c" ``` diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md index 171294b3d..966b77221 100644 --- a/docs/docs/how-tos/index.md +++ b/docs/docs/how-tos/index.md @@ -55,6 +55,7 @@ These guides show how to use different streaming modes. - [How to stream events from within a tool](streaming-events-from-within-tools.ipynb) - [How to stream events from within a tool without LangChain models](streaming-events-from-within-tools-without-langchain.ipynb) - [How to stream events from the final node](streaming-from-final-node.ipynb) +- [How to stream from subgraphs](streaming-subgraphs.ipynb) ## Tool calling diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 9291a309f..2c02a66cb 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -155,6 +155,7 @@ nav: - Stream events from within tools: how-tos/streaming-events-from-within-tools.ipynb - Stream events from within tools without LangChain models: how-tos/streaming-events-from-within-tools-without-langchain.ipynb - Stream events from the final node: how-tos/streaming-from-final-node.ipynb + - Stream from subgraphs: how-tos/streaming-subgraphs.ipynb - Tool calling: - Call tools using ToolNode: how-tos/tool-calling.ipynb - Handle tool calling errors: how-tos/tool-calling-errors.ipynb diff --git a/examples/streaming-from-final-node.ipynb b/examples/streaming-from-final-node.ipynb index 30496a3a1..f7a8d8c66 100644 --- a/examples/streaming-from-final-node.ipynb +++ b/examples/streaming-from-final-node.ipynb @@ -13,7 +13,11 @@ "id": "964686a6-8fed-4360-84d2-958c48186008", "metadata": {}, "source": [ - "A common use case is streaming from an agent is to stream LLM tokens from inside the final node. This guide demonstrates how you can do this." + "A common use case is streaming from an agent is to stream LLM tokens from inside the final node. This guide demonstrates how you can do this.\n", + "\n", + "## Setup\n", + "\n", + "First let's install our required packages and set our environment variables." ] }, { @@ -34,7 +38,7 @@ "metadata": {}, "outputs": [ { - "name": "stdin", + "name": "stdout", "output_type": "stream", "text": [ "OPENAI_API_KEY: ········\n" diff --git a/examples/streaming-subgraphs.ipynb b/examples/streaming-subgraphs.ipynb new file mode 100644 index 000000000..ca7f0808c --- /dev/null +++ b/examples/streaming-subgraphs.ipynb @@ -0,0 +1,364 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to stream from subgraphs\n", + "\n", + "If you have created a graph with subgraphs you may wish to stream things occurring inside those subgraphs (or you may not!). This guide will walk through how you can control the information that is streamed back from subgraphs.\n", + "\n", + "## Setup\n", + "\n", + "First let's download the required packages and set our OpenAI API key since we will need that to run the models " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%capture --no-stderr\n", + "%pip install -U langgraph langchain-openai" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "_set_env(\"OPENAI_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define subgraphs\n", + "\n", + "We are going to use the same subgraph from [this how-to](https://langchain-ai.github.io/langgraph/how-tos/subgraph/)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Optional, Annotated\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "from langgraph.graph import StateGraph, START, END\n", + "\n", + "\n", + "# The structure of the logs\n", + "class Logs(TypedDict):\n", + " id: str\n", + " question: str\n", + " answer: str\n", + " grade: Optional[int]\n", + " feedback: Optional[str]\n", + "\n", + "\n", + "# Define custom reducer (see more on this in the \"Custom reducer\" section below)\n", + "def add_logs(left: list[Logs], right: list[Logs]) -> list[Logs]:\n", + " if not left:\n", + " left = []\n", + " \n", + " if not right:\n", + " right = []\n", + "\n", + " logs = left.copy()\n", + " left_id_to_idx = {log[\"id\"]: idx for idx, log in enumerate(logs)}\n", + " # update if the new logs are already in the state, otherwise append\n", + " for log in right:\n", + " idx = left_id_to_idx.get(log[\"id\"])\n", + " if idx is not None:\n", + " logs[idx] = log\n", + " else:\n", + " logs.append(log)\n", + " return logs\n", + "\n", + "\n", + "# Failure Analysis Subgraph\n", + "class FailureAnalysisState(TypedDict):\n", + " # keys shared with the parent graph (EntryGraphState)\n", + " logs: Annotated[list[Logs], add_logs]\n", + " failure_report: str\n", + " # subgraph key\n", + " failures: list[Logs]\n", + "\n", + "\n", + "def get_failures(state: FailureAnalysisState):\n", + " failures = [log for log in state[\"logs\"] if log[\"grade\"] == 0]\n", + " return {\"failures\": failures}\n", + "\n", + "\n", + "def generate_summary(state: FailureAnalysisState):\n", + " failures = state[\"failures\"]\n", + " # NOTE: you can implement custom summarization logic here\n", + " failure_ids = [log[\"id\"] for log in failures]\n", + " fa_summary = f\"Poor quality of retrieval for document IDs: {', '.join(failure_ids)}\"\n", + " return {\"failure_report\": fa_summary}\n", + "\n", + "\n", + "fa_builder = StateGraph(FailureAnalysisState)\n", + "fa_builder.add_node(\"get_failures\", get_failures)\n", + "fa_builder.add_node(\"generate_summary\", generate_summary)\n", + "fa_builder.add_edge(START, \"get_failures\")\n", + "fa_builder.add_edge(\"get_failures\", \"generate_summary\")\n", + "fa_builder.add_edge(\"generate_summary\", END)\n", + "\n", + "\n", + "# Summarization subgraph\n", + "class QuestionSummarizationState(TypedDict):\n", + " # keys that are shared with the parent graph (EntryGraphState)\n", + " summary_report: str\n", + " logs: Annotated[list[Logs], add_logs]\n", + " # subgraph keys\n", + " summary: str\n", + "\n", + "def generate_summary(state: QuestionSummarizationState):\n", + " docs = state[\"logs\"]\n", + " # NOTE: you can implement custom summarization logic here\n", + " summary = \"Questions focused on usage of ChatOllama and Chroma vector store.\"\n", + " return {\"summary\": summary}\n", + "\n", + "\n", + "def send_to_slack(state: QuestionSummarizationState):\n", + " summary = state[\"summary\"]\n", + " # NOTE: you can implement custom logic here, for example sending the summary generated in the previous step to Slack\n", + " return {\"summary_report\": summary}\n", + "\n", + "\n", + "qs_builder = StateGraph(QuestionSummarizationState)\n", + "qs_builder.add_node(\"generate_summary\", generate_summary)\n", + "qs_builder.add_node(\"send_to_slack\", send_to_slack)\n", + "qs_builder.add_edge(START, \"generate_summary\")\n", + "qs_builder.add_edge(\"generate_summary\", \"send_to_slack\")\n", + "qs_builder.add_edge(\"send_to_slack\", END)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define parent graph" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Entry Graph\n", + "class EntryGraphState(TypedDict):\n", + " raw_logs: Annotated[list[Logs], add_logs]\n", + " logs: Annotated[list[Logs], add_logs] # This will be used in subgraphs\n", + " failure_report: str # This will be generated in the FA subgraph\n", + " summary_report: str # This will be generated in the QS subgraph\n", + "\n", + "\n", + "def select_logs(state):\n", + " return {\"logs\": [log for log in state[\"raw_logs\"] if \"grade\" in log]}\n", + "\n", + "\n", + "entry_builder = StateGraph(EntryGraphState)\n", + "entry_builder.add_node(\"select_logs\", select_logs)\n", + "entry_builder.add_node(\"question_summarization\", qs_builder.compile())\n", + "entry_builder.add_node(\"failure_analysis\", fa_builder.compile())\n", + "\n", + "entry_builder.add_edge(START, \"select_logs\")\n", + "entry_builder.add_edge(\"select_logs\", \"failure_analysis\")\n", + "entry_builder.add_edge(\"select_logs\", \"question_summarization\")\n", + "entry_builder.add_edge(\"failure_analysis\", END)\n", + "entry_builder.add_edge(\"question_summarization\", END)\n", + "\n", + "graph = entry_builder.compile()\n", + "\n", + "from IPython.display import Image, display\n", + "\n", + "# Setting xray to 1 will show the internal structure of the nested graph\n", + "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Stream\n", + "\n", + "Now let's see how we can stream from our graph!\n", + "\n", + "### Define input\n", + "\n", + "First, let's define the input we will use for the rest of the notebook:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# Dummy logs\n", + "dummy_logs = [\n", + " Logs(\n", + " id=\"1\",\n", + " question=\"How can I import ChatOllama?\",\n", + " grade=1,\n", + " answer=\"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\",\n", + " ),\n", + " Logs(\n", + " id=\"2\",\n", + " question=\"How can I use Chroma vector store?\",\n", + " answer=\"To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).\",\n", + " grade=0,\n", + " feedback=\"The retrieved documents discuss vector stores in general, but not Chroma specifically\",\n", + " ),\n", + " Logs(\n", + " id=\"3\",\n", + " question=\"How do I create react agent in langgraph?\",\n", + " answer=\"from langgraph.prebuilt import create_react_agent\",\n", + " )\n", + "]\n", + "\n", + "input = {\"raw_logs\": dummy_logs}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Stream normally\n", + "\n", + "First let us examine the output of streaming normally:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- Update from node select_logs ---------\n", + "{'logs': [{'id': '1', 'question': 'How can I import ChatOllama?', 'grade': 1, 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"}, {'id': '2', 'question': 'How can I use Chroma vector store?', 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).', 'grade': 0, 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'}]}\n", + "---------- Update from node failure_analysis ---------\n", + "{'logs': [{'id': '1', 'question': 'How can I import ChatOllama?', 'grade': 1, 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"}, {'id': '2', 'question': 'How can I use Chroma vector store?', 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).', 'grade': 0, 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'}], 'failure_report': 'Poor quality of retrieval for document IDs: 2'}\n", + "---------- Update from node question_summarization ---------\n", + "{'logs': [{'id': '1', 'question': 'How can I import ChatOllama?', 'grade': 1, 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"}, {'id': '2', 'question': 'How can I use Chroma vector store?', 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).', 'grade': 0, 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'}], 'summary_report': 'Questions focused on usage of ChatOllama and Chroma vector store.'}\n" + ] + } + ], + "source": [ + "for chunk in graph.stream(input, stream_mode=\"updates\"):\n", + " node_name = list(chunk.keys())[0]\n", + " print(f\"---------- Update from node {node_name} ---------\")\n", + " print(chunk[node_name])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see there are only 3 updates made to our overall graph state. The first one is by the `select_logs` node, and then we receive one update from each subgraph (note if you don't want to see the `log` update from each subgraph that you can set the [output schema](https://langchain-ai.github.io/langgraph/how-tos/input_output_schema/) to exclude it). What we do not see however, is the updates occurring *inside* each subgraph. The next section will explain how to do that.\n", + "\n", + "### Stream subgraph \n", + "\n", + "To show the updates occurring inside of each subgraph, we can simply set `subgraphs=True` to the streaming call:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------- Update from node select_logs in parent graph ---------\n", + "{'logs': [{'id': '1', 'question': 'How can I import ChatOllama?', 'grade': 1, 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"}, {'id': '2', 'question': 'How can I use Chroma vector store?', 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).', 'grade': 0, 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'}]}\n", + "---------- Update from node get_failures in failure_analysis subgraph ---------\n", + "{'failures': [{'id': '2', 'question': 'How can I use Chroma vector store?', 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).', 'grade': 0, 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'}]}\n", + "---------- Update from node generate_summary in failure_analysis subgraph ---------\n", + "{'failure_report': 'Poor quality of retrieval for document IDs: 2'}\n", + "---------- Update from node failure_analysis in parent graph ---------\n", + "{'logs': [{'id': '1', 'question': 'How can I import ChatOllama?', 'grade': 1, 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"}, {'id': '2', 'question': 'How can I use Chroma vector store?', 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).', 'grade': 0, 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'}], 'failure_report': 'Poor quality of retrieval for document IDs: 2'}\n", + "---------- Update from node generate_summary in question_summarization subgraph ---------\n", + "{'summary': 'Questions focused on usage of ChatOllama and Chroma vector store.'}\n", + "---------- Update from node send_to_slack in question_summarization subgraph ---------\n", + "{'summary_report': 'Questions focused on usage of ChatOllama and Chroma vector store.'}\n", + "---------- Update from node question_summarization in parent graph ---------\n", + "{'logs': [{'id': '1', 'question': 'How can I import ChatOllama?', 'grade': 1, 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"}, {'id': '2', 'question': 'How can I use Chroma vector store?', 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).', 'grade': 0, 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'}], 'summary_report': 'Questions focused on usage of ChatOllama and Chroma vector store.'}\n" + ] + } + ], + "source": [ + "# Format the namespace slightly nicer\n", + "def format_namespace(namespace):\n", + " return namespace[-1].split(':')[0]+' subgraph' if len(namespace) > 0 else 'parent graph'\n", + "\n", + "for namespace, chunk in graph.stream(input, stream_mode=\"updates\", subgraphs=True):\n", + " node_name = list(chunk.keys())[0]\n", + " print(f\"---------- Update from node {node_name} in {format_namespace(namespace)} ---------\")\n", + " print(chunk[node_name])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The first thing you will notice as different is that we are no longer just receiving chunks, but we also receive namespaces which tell us what subgraph we are currently inside of.\n", + "\n", + "If you look carefully at the logs you can see we are now receiving the updates made by nodes inside of each subgraph, for instance we now see updates to the `summary_report` state channel from the `get_failure` node which lives in the `failure_analysis` subgraph. When we didn't set `subgraphs=True` all we saw was the overall update made by the subgraph `failure_analysis`." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}