{
  "cells": [
    {
      "attachments": {
        "71516aef-9c00-4730-a676-a54e90cb6472.png": {
          "image/png": "[encoded data, 132216 characters]"
        }
      },
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# How to create subgraphs\n",
        "\n",
        "For more complex systems, sub-graphs are a useful design principle. Sub-graphs allow you to create and manage different states in different parts of your graph. This allows you build things like [multi-agent teams](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/hierarchical_agent_teams/), where each team can track its own separate state.\n",
        "\n",
        "![Screenshot 2024-07-11 at 1.01.28 PM.png](attachment:71516aef-9c00-4730-a676-a54e90cb6472.png)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "metadata": {},
      "outputs": [],
      "source": [
        "%%capture --no-stderr\n",
        "%pip install -U langgraph"
      ]
    },
    {
      "attachments": {
        "9145adc1-ce9d-4a22-8183-e13796d4a388.png": {
          "image/png": "[encoded data, 174144 characters]"
        }
      },
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Simple example\n",
        "\n",
        "Let's consider a toy example: I have a system that accepts logs and perform two separate sub-tasks. First, it will summarize them. Second, it will summarize any failure modes captured in the logs. I want to perform these two operations in two different sub-graphs.\n",
        "\n",
        "The most important thing to recognize is the information transfer between the graphs. `Entry Graph` is the parent, and each of the two sub-graphs are defined as nodes in `Entry Graph`. Both subgraphs inherit state from the parent `Entry Graph`; I can access `docs` in each of the sub-graphs simply by specifying it in the sub-graph state (see diagram). Each subgraph can have its own private state. And any values that I want propagated back to the parent `Entry Graph` (for final reporting) simply need to be defined in my `Entry Graph` state (e.g., `summary report` and `failure report`).\n",
        "\n",
        "![Screenshot 2024-07-12 at 10.35.41 AM.png](attachment:9145adc1-ce9d-4a22-8183-e13796d4a388.png)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "metadata": {},
      "outputs": [],
      "source": [
        "from operator import add\n",
        "from typing import List, TypedDict, Optional, Annotated, Dict\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",
        "    docs: Optional[List]\n",
        "    answer: str\n",
        "    grade: Optional[int]\n",
        "    grader: Optional[str]\n",
        "    feedback: Optional[str]\n",
        "\n",
        "\n",
        "# Failure Analysis Sub-graph\n",
        "class FailureAnalysisState(TypedDict):\n",
        "    docs: List[Logs]\n",
        "    failures: List[Logs]\n",
        "    fa_summary: str\n",
        "\n",
        "\n",
        "def get_failures(state):\n",
        "    docs = state[\"docs\"]\n",
        "    failures = [doc for doc in docs if \"grade\" in doc]\n",
        "    return {\"failures\": failures}\n",
        "\n",
        "\n",
        "def generate_summary(state):\n",
        "    failures = state[\"failures\"]\n",
        "    # Add fxn: fa_summary = summarize(failures)\n",
        "    fa_summary = \"Poor quality retrieval of Chroma documentation.\"\n",
        "    return {\"fa_summary\": 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",
        "    docs: List[Logs]\n",
        "    qs_summary: str\n",
        "    report: str\n",
        "\n",
        "\n",
        "def generate_summary(state):\n",
        "    docs = state[\"docs\"]\n",
        "    # Add fxn: summary = summarize(docs)\n",
        "    summary = \"Questions focused on usage of ChatOllama and Chroma vector store.\"\n",
        "    return {\"qs_summary\": summary}\n",
        "\n",
        "\n",
        "def send_to_slack(state):\n",
        "    qs_summary = state[\"qs_summary\"]\n",
        "    # Add fxn: report = report_generation(qs_summary)\n",
        "    report = \"foo bar baz\"\n",
        "    return {\"report\": report}\n",
        "\n",
        "\n",
        "def format_report_for_slack(state):\n",
        "    report = state[\"report\"]\n",
        "    # Add fxn: formatted_report = report_format(report)\n",
        "    formatted_report = \"foo bar\"\n",
        "    return {\"report\": formatted_report}\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_node(\"format_report_for_slack\", format_report_for_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\", \"format_report_for_slack\")\n",
        "qs_builder.add_edge(\"format_report_for_slack\", END)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Note that each sub-graph has its own state, `QuestionSummarizationState` and `FailureAnalysisState`.\n",
        " \n",
        "After defining each sub-graph, we put everything together."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "metadata": {},
      "outputs": [
        {
          "data": {
            "image/jpeg": "[encoded data, 41452 characters]",
            "text/plain": [
              "<IPython.core.display.Image object>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "# Dummy logs\n",
        "question_answer = Logs(\n",
        "    id=\"1\",\n",
        "    question=\"How can I import ChatOllama?\",\n",
        "    answer=\"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\",\n",
        ")\n",
        "\n",
        "question_answer_feedback = 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",
        "    grader=\"Document Relevance Recall\",\n",
        "    feedback=\"The retrieved documents discuss vector stores in general, but not Chroma specifically\",\n",
        ")\n",
        "\n",
        "\n",
        "# Entry Graph\n",
        "class EntryGraphState(TypedDict):\n",
        "    raw_logs: Annotated[List[Dict], add]\n",
        "    docs: Annotated[List[Logs], add]  # This will be used in sub-graphs\n",
        "    fa_summary: str  # This will be generated in the FA sub-graph\n",
        "    report: str  # This will be generated in the QS sub-graph\n",
        "\n",
        "\n",
        "def convert_logs_to_docs(state):\n",
        "    # Get logs\n",
        "    raw_logs = state[\"raw_logs\"]\n",
        "    docs = [question_answer, question_answer_feedback]\n",
        "    return {\"docs\": docs}\n",
        "\n",
        "\n",
        "entry_builder = StateGraph(EntryGraphState)\n",
        "entry_builder.add_node(\"convert_logs_to_docs\", convert_logs_to_docs)\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, \"convert_logs_to_docs\")\n",
        "entry_builder.add_edge(\"convert_logs_to_docs\", \"failure_analysis\")\n",
        "entry_builder.add_edge(\"convert_logs_to_docs\", \"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": "code",
      "execution_count": 3,
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "{'raw_logs': [{'foo': 'bar'}, {'foo': 'baz'}],\n",
              " 'docs': [{'id': '1',\n",
              "   'question': 'How can I import ChatOllama?',\n",
              "   'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"},\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",
              "   'grader': 'Document Relevance Recall',\n",
              "   'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'},\n",
              "  {'id': '1',\n",
              "   'question': 'How can I import ChatOllama?',\n",
              "   'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"},\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",
              "   'grader': 'Document Relevance Recall',\n",
              "   'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'},\n",
              "  {'id': '1',\n",
              "   'question': 'How can I import ChatOllama?',\n",
              "   'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"},\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",
              "   'grader': 'Document Relevance Recall',\n",
              "   'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'}],\n",
              " 'fa_summary': 'Poor quality retrieval of Chroma documentation.',\n",
              " 'report': 'foo bar'}"
            ]
          },
          "execution_count": 3,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "raw_logs = [{\"foo\": \"bar\"}, {\"foo\": \"baz\"}]\n",
        "graph.invoke({\"raw_logs\": raw_logs}, debug=False)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Custom reducer functions to manage state\n",
        "\n",
        "Now, let's highlight a possible stumbling block when we use the same `State` across multiple sub-graphs.\n",
        " \n",
        "We will create two graphs: a parent graph with a few nodes and a child graph that is added as a node in the parent.\n",
        "\n",
        "We define a custom [reducer](https://langchain-ai.github.io/langgraph/concepts/low_level/#reducers) function for our state."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 15,
      "metadata": {},
      "outputs": [],
      "source": [
        "from typing import Annotated\n",
        "\n",
        "from typing_extensions import TypedDict\n",
        "\n",
        "\n",
        "def reduce_list(left: list | None, right: list | None) -> list:\n",
        "    if not left:\n",
        "        left = []\n",
        "    if not right:\n",
        "        right = []\n",
        "    return left + right\n",
        "\n",
        "\n",
        "class ChildState(TypedDict):\n",
        "    name: str\n",
        "    path: Annotated[list[str], reduce_list]\n",
        "\n",
        "\n",
        "class ParentState(TypedDict):\n",
        "    name: str\n",
        "    path: Annotated[list[str], reduce_list]\n",
        "\n",
        "\n",
        "child_builder = StateGraph(ChildState)\n",
        "\n",
        "child_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\n",
        "child_builder.add_edge(START, \"child_start\")\n",
        "child_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\n",
        "child_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\n",
        "child_builder.add_edge(\"child_start\", \"child_middle\")\n",
        "child_builder.add_edge(\"child_middle\", \"child_end\")\n",
        "child_builder.add_edge(\"child_end\", END)\n",
        "\n",
        "builder = StateGraph(ParentState)\n",
        "\n",
        "builder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\n",
        "builder.add_edge(START, \"grandparent\")\n",
        "builder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\n",
        "builder.add_node(\"child\", child_builder.compile())\n",
        "builder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\n",
        "builder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n",
        "\n",
        "# Add connections\n",
        "builder.add_edge(\"grandparent\", \"parent\")\n",
        "builder.add_edge(\"parent\", \"child\")\n",
        "builder.add_edge(\"parent\", \"sibling\")\n",
        "builder.add_edge(\"child\", \"fin\")\n",
        "builder.add_edge(\"sibling\", \"fin\")\n",
        "builder.add_edge(\"fin\", END)\n",
        "graph = builder.compile()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "metadata": {},
      "outputs": [
        {
          "data": {
            "image/jpeg": "[encoded data, 26608 characters]",
            "text/plain": [
              "<IPython.core.display.Image object>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "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": "code",
      "execution_count": 17,
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\u001b[36;1m\u001b[1;3m[0:tasks]\u001b[0m \u001b[1mStarting step 0 with 1 task:\n",
            "\u001b[0m- \u001b[32;1m\u001b[1;3m__start__\u001b[0m -> {'name': 'test'}\n",
            "\u001b[36;1m\u001b[1;3m[0:writes]\u001b[0m \u001b[1mFinished step 0 with writes to 1 channel:\n",
            "\u001b[0m- \u001b[33;1m\u001b[1;3mname\u001b[0m -> 'test'\n",
            "\u001b[36;1m\u001b[1;3m[0:checkpoint]\u001b[0m \u001b[1mState at the end of step 0:\n",
            "\u001b[0m{'name': 'test', 'path': []}\n",
            "\u001b[36;1m\u001b[1;3m[1:tasks]\u001b[0m \u001b[1mStarting step 1 with 1 task:\n",
            "\u001b[0m- \u001b[32;1m\u001b[1;3mgrandparent\u001b[0m -> {'name': 'test', 'path': []}\n",
            "\u001b[36;1m\u001b[1;3m[1:writes]\u001b[0m \u001b[1mFinished step 1 with writes to 1 channel:\n",
            "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['grandparent']\n",
            "\u001b[36;1m\u001b[1;3m[1:checkpoint]\u001b[0m \u001b[1mState at the end of step 1:\n",
            "\u001b[0m{'name': 'test', 'path': ['grandparent']}\n",
            "\u001b[36;1m\u001b[1;3m[2:tasks]\u001b[0m \u001b[1mStarting step 2 with 1 task:\n",
            "\u001b[0m- \u001b[32;1m\u001b[1;3mparent\u001b[0m -> {'name': 'test', 'path': ['grandparent']}\n",
            "\u001b[36;1m\u001b[1;3m[2:writes]\u001b[0m \u001b[1mFinished step 2 with writes to 1 channel:\n",
            "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['parent']\n",
            "\u001b[36;1m\u001b[1;3m[2:checkpoint]\u001b[0m \u001b[1mState at the end of step 2:\n",
            "\u001b[0m{'name': 'test', 'path': ['grandparent', 'parent']}\n",
            "\u001b[36;1m\u001b[1;3m[3:tasks]\u001b[0m \u001b[1mStarting step 3 with 2 tasks:\n",
            "\u001b[0m- \u001b[32;1m\u001b[1;3mchild\u001b[0m -> {'name': 'test', 'path': ['grandparent', 'parent']}\n",
            "- \u001b[32;1m\u001b[1;3msibling\u001b[0m -> {'name': 'test', 'path': ['grandparent', 'parent']}\n",
            "\u001b[36;1m\u001b[1;3m[3:writes]\u001b[0m \u001b[1mFinished step 3 with writes to 2 channels:\n",
            "\u001b[0m- \u001b[33;1m\u001b[1;3mname\u001b[0m -> 'test'\n",
            "- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['grandparent', 'parent', 'child_start', 'child_middle', 'child_end'], ['sibling']\n",
            "\u001b[36;1m\u001b[1;3m[3:checkpoint]\u001b[0m \u001b[1mState at the end of step 3:\n",
            "\u001b[0m{'name': 'test',\n",
            " 'path': ['grandparent',\n",
            "          'parent',\n",
            "          'grandparent',\n",
            "          'parent',\n",
            "          'child_start',\n",
            "          'child_middle',\n",
            "          'child_end',\n",
            "          'sibling']}\n",
            "\u001b[36;1m\u001b[1;3m[4:tasks]\u001b[0m \u001b[1mStarting step 4 with 1 task:\n",
            "\u001b[0m- \u001b[32;1m\u001b[1;3mfin\u001b[0m -> {'name': 'test',\n",
            " 'path': ['grandparent',\n",
            "          'parent',\n",
            "          'grandparent',\n",
            "          'parent',\n",
            "          'child_start',\n",
            "          'child_middle',\n",
            "          'child_end',\n",
            "          'sibling']}\n",
            "\u001b[36;1m\u001b[1;3m[4:writes]\u001b[0m \u001b[1mFinished step 4 with writes to 1 channel:\n",
            "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['fin']\n",
            "\u001b[36;1m\u001b[1;3m[4:checkpoint]\u001b[0m \u001b[1mState at the end of step 4:\n",
            "\u001b[0m{'name': 'test',\n",
            " 'path': ['grandparent',\n",
            "          'parent',\n",
            "          'grandparent',\n",
            "          'parent',\n",
            "          'child_start',\n",
            "          'child_middle',\n",
            "          'child_end',\n",
            "          'sibling',\n",
            "          'fin']}\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "{'name': 'test',\n",
              " 'path': ['grandparent',\n",
              "  'parent',\n",
              "  'grandparent',\n",
              "  'parent',\n",
              "  'child_start',\n",
              "  'child_middle',\n",
              "  'child_end',\n",
              "  'sibling',\n",
              "  'fin']}"
            ]
          },
          "execution_count": 17,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "graph.invoke({\"name\": \"test\"}, debug=True)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Notice here that the `[\"grandparent\", \"parent\"]` sequence is duplicated! \n",
        "\n",
        "This is because our child state has received the full parent state and returns the full parent state once it terminates. \n",
        "\n",
        "To avoid duplication or conflicts in state, you typically would do one or more of the following:\n",
        "\n",
        "1. Handle duplicates in your `reducer` function.\n",
        "2. Call the child graph from within a python function. In that function, handle the state as needed.  \n",
        "3. Update the child graph keys to avoid conflicts. You would still need to ensure the output can be interpreted by the parent, however.\n",
        "\n",
        "Let's re-implement the graph using technique (1) and add unique IDs for every value in the list. This is what is done in [`MessageGraph`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.MessageGraph)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 23,
      "metadata": {},
      "outputs": [],
      "source": [
        "import uuid\n",
        "\n",
        "\n",
        "def reduce_list(left: list | None, right: list | None) -> list:\n",
        "    \"\"\"Append the right-hand list, replacing any elements with the same id in the left-hand list.\"\"\"\n",
        "    if not left:\n",
        "        left = []\n",
        "    if not right:\n",
        "        right = []\n",
        "    left_, right_ = [], []\n",
        "    for orig, new in [(left, left_), (right, right_)]:\n",
        "        for val in orig:\n",
        "            if not isinstance(val, dict):\n",
        "                val = {\"val\": val}\n",
        "            if \"id\" not in val:\n",
        "                val[\"id\"] = str(uuid.uuid4())\n",
        "            new.append(val)\n",
        "    # Merge the two lists\n",
        "    left_idx_by_id = {val[\"id\"]: i for i, val in enumerate(left_)}\n",
        "    merged = left_.copy()\n",
        "    for val in right_:\n",
        "        if (existing_idx := left_idx_by_id.get(val[\"id\"])) is not None:\n",
        "            merged[existing_idx] = val\n",
        "        else:\n",
        "            merged.append(val)\n",
        "    return merged\n",
        "\n",
        "\n",
        "class ChildState(TypedDict):\n",
        "    name: str\n",
        "    path: Annotated[list[str], reduce_list]\n",
        "\n",
        "\n",
        "class ParentState(TypedDict):\n",
        "    name: str\n",
        "    path: Annotated[list[str], reduce_list]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 24,
      "metadata": {},
      "outputs": [],
      "source": [
        "child_builder = StateGraph(ChildState)\n",
        "\n",
        "child_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\n",
        "child_builder.add_edge(START, \"child_start\")\n",
        "child_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\n",
        "child_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\n",
        "child_builder.add_edge(\"child_start\", \"child_middle\")\n",
        "child_builder.add_edge(\"child_middle\", \"child_end\")\n",
        "child_builder.add_edge(\"child_end\", END)\n",
        "\n",
        "builder = StateGraph(ParentState)\n",
        "\n",
        "builder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\n",
        "builder.add_edge(START, \"grandparent\")\n",
        "builder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\n",
        "builder.add_node(\"child\", child_builder.compile())\n",
        "builder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\n",
        "builder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n",
        "\n",
        "# Add connections\n",
        "builder.add_edge(\"grandparent\", \"parent\")\n",
        "builder.add_edge(\"parent\", \"child\")\n",
        "builder.add_edge(\"parent\", \"sibling\")\n",
        "builder.add_edge(\"child\", \"fin\")\n",
        "builder.add_edge(\"sibling\", \"fin\")\n",
        "builder.add_edge(\"fin\", END)\n",
        "graph = builder.compile()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 25,
      "metadata": {},
      "outputs": [
        {
          "data": {
            "image/jpeg": "[encoded data, 30724 characters]",
            "text/plain": [
              "<IPython.core.display.Image object>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "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": "code",
      "execution_count": 26,
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\u001b[36;1m\u001b[1;3m[0:tasks]\u001b[0m \u001b[1mStarting step 0 with 1 task:\n",
            "\u001b[0m- \u001b[32;1m\u001b[1;3m__start__\u001b[0m -> {'name': 'test'}\n",
            "\u001b[36;1m\u001b[1;3m[0:writes]\u001b[0m \u001b[1mFinished step 0 with writes to 1 channel:\n",
            "\u001b[0m- \u001b[33;1m\u001b[1;3mname\u001b[0m -> 'test'\n",
            "\u001b[36;1m\u001b[1;3m[0:checkpoint]\u001b[0m \u001b[1mState at the end of step 0:\n",
            "\u001b[0m{'name': 'test', 'path': []}\n",
            "\u001b[36;1m\u001b[1;3m[1:tasks]\u001b[0m \u001b[1mStarting step 1 with 1 task:\n",
            "\u001b[0m- \u001b[32;1m\u001b[1;3mgrandparent\u001b[0m -> {'name': 'test', 'path': []}\n",
            "\u001b[36;1m\u001b[1;3m[1:writes]\u001b[0m \u001b[1mFinished step 1 with writes to 1 channel:\n",
            "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['grandparent']\n",
            "\u001b[36;1m\u001b[1;3m[1:checkpoint]\u001b[0m \u001b[1mState at the end of step 1:\n",
            "\u001b[0m{'name': 'test',\n",
            " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'}]}\n",
            "\u001b[36;1m\u001b[1;3m[2:tasks]\u001b[0m \u001b[1mStarting step 2 with 1 task:\n",
            "\u001b[0m- \u001b[32;1m\u001b[1;3mparent\u001b[0m -> {'name': 'test',\n",
            " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'}]}\n",
            "\u001b[36;1m\u001b[1;3m[2:writes]\u001b[0m \u001b[1mFinished step 2 with writes to 1 channel:\n",
            "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['parent']\n",
            "\u001b[36;1m\u001b[1;3m[2:checkpoint]\u001b[0m \u001b[1mState at the end of step 2:\n",
            "\u001b[0m{'name': 'test',\n",
            " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n",
            "          {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'}]}\n",
            "\u001b[36;1m\u001b[1;3m[3:tasks]\u001b[0m \u001b[1mStarting step 3 with 2 tasks:\n",
            "\u001b[0m- \u001b[32;1m\u001b[1;3mchild\u001b[0m -> {'name': 'test',\n",
            " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n",
            "          {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'}]}\n",
            "- \u001b[32;1m\u001b[1;3msibling\u001b[0m -> {'name': 'test',\n",
            " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n",
            "          {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'}]}\n",
            "\u001b[36;1m\u001b[1;3m[3:writes]\u001b[0m \u001b[1mFinished step 3 with writes to 2 channels:\n",
            "\u001b[0m- \u001b[33;1m\u001b[1;3mname\u001b[0m -> 'test'\n",
            "- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n",
            " {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'},\n",
            " {'id': 'd1c1bab0-6e19-4846-a470-e9cc2eb85088', 'val': 'child_start'},\n",
            " {'id': 'e0fcb647-1e9e-4ae4-b560-0046515d5783', 'val': 'child_middle'},\n",
            " {'id': '669dd810-360f-4694-a9f3-49597f23376a', 'val': 'child_end'}], ['sibling']\n",
            "\u001b[36;1m\u001b[1;3m[3:checkpoint]\u001b[0m \u001b[1mState at the end of step 3:\n",
            "\u001b[0m{'name': 'test',\n",
            " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n",
            "          {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'},\n",
            "          {'id': 'd1c1bab0-6e19-4846-a470-e9cc2eb85088', 'val': 'child_start'},\n",
            "          {'id': 'e0fcb647-1e9e-4ae4-b560-0046515d5783', 'val': 'child_middle'},\n",
            "          {'id': '669dd810-360f-4694-a9f3-49597f23376a', 'val': 'child_end'},\n",
            "          {'id': '137dbc2f-b33c-4ea4-8b04-a62215ba9718', 'val': 'sibling'}]}\n",
            "\u001b[36;1m\u001b[1;3m[4:tasks]\u001b[0m \u001b[1mStarting step 4 with 1 task:\n",
            "\u001b[0m- \u001b[32;1m\u001b[1;3mfin\u001b[0m -> {'name': 'test',\n",
            " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n",
            "          {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'},\n",
            "          {'id': 'd1c1bab0-6e19-4846-a470-e9cc2eb85088', 'val': 'child_start'},\n",
            "          {'id': 'e0fcb647-1e9e-4ae4-b560-0046515d5783', 'val': 'child_middle'},\n",
            "          {'id': '669dd810-360f-4694-a9f3-49597f23376a', 'val': 'child_end'},\n",
            "          {'id': '137dbc2f-b33c-4ea4-8b04-a62215ba9718', 'val': 'sibling'}]}\n",
            "\u001b[36;1m\u001b[1;3m[4:writes]\u001b[0m \u001b[1mFinished step 4 with writes to 1 channel:\n",
            "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['fin']\n",
            "\u001b[36;1m\u001b[1;3m[4:checkpoint]\u001b[0m \u001b[1mState at the end of step 4:\n",
            "\u001b[0m{'name': 'test',\n",
            " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n",
            "          {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'},\n",
            "          {'id': 'd1c1bab0-6e19-4846-a470-e9cc2eb85088', 'val': 'child_start'},\n",
            "          {'id': 'e0fcb647-1e9e-4ae4-b560-0046515d5783', 'val': 'child_middle'},\n",
            "          {'id': '669dd810-360f-4694-a9f3-49597f23376a', 'val': 'child_end'},\n",
            "          {'id': '137dbc2f-b33c-4ea4-8b04-a62215ba9718', 'val': 'sibling'},\n",
            "          {'id': 'a4328c5f-845a-43de-b3d7-53a39208e316', 'val': 'fin'}]}\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "{'name': 'test',\n",
              " 'path': [{'val': 'grandparent', 'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49'},\n",
              "  {'val': 'parent', 'id': '2a6f0263-3949-4e47-a210-57f817e6097d'},\n",
              "  {'val': 'child_start', 'id': 'd1c1bab0-6e19-4846-a470-e9cc2eb85088'},\n",
              "  {'val': 'child_middle', 'id': 'e0fcb647-1e9e-4ae4-b560-0046515d5783'},\n",
              "  {'val': 'child_end', 'id': '669dd810-360f-4694-a9f3-49597f23376a'},\n",
              "  {'val': 'sibling', 'id': '137dbc2f-b33c-4ea4-8b04-a62215ba9718'},\n",
              "  {'val': 'fin', 'id': 'a4328c5f-845a-43de-b3d7-53a39208e316'}]}"
            ]
          },
          "execution_count": 26,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "graph.invoke({\"name\": \"test\"}, debug=True)"
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3 (ipykernel)",
      "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.8"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 4
}
