Compare commits

...
56 Commits
Author SHA1 Message Date
Vadym BardaandGitHub 86c556d01e cli: release 0.1.43 (#836) 2024-06-26 13:45:35 -04:00
Vadym BardaandGitHub b7580abf45 cli: update CAT -> RUN in the dockerfile (#816) 2024-06-26 13:43:34 -04:00
Vadym BardaandGitHub 72ac58d1c2 docs: update persistence in the how-tos (#833) 2024-06-26 13:43:23 -04:00
Vadym BardaandGitHub 000a5c5b83 docs: update graph state in readme (#835) 2024-06-26 13:43:14 -04:00
Isaac FranciscoandGitHub 6caea79fba webhook (#810) 2024-06-26 10:41:29 -07:00
Nuno Campos fae97d9fba cli0.1.42 2024-06-26 09:37:25 -07:00
Nuno CamposandGitHub d16ec4f436 cli: Make base image configurable in build command (#831)
* cli: Make base image configurable in build command

* Lint

* Fix
2024-06-26 09:36:09 -07:00
William FHandGitHub 66b728e83a [Docs] Cleanup links in nb (#828) 2024-06-26 00:20:20 -07:00
William FHandGitHub feca5e1970 [Docs] Restore Linkcheck (#824)
Plus:
1. Improve docstrings of add_node
2. Update crosslinking of sqlite and aiosqlite docstrings
3. Fix a bunch of links so we can turn on strict validation
2024-06-26 00:04:13 -07:00
Jacob LeeandGitHub c217e4a58d Update intro to same thread example, update conceptual docs to include checkpoint (#827) 2024-06-25 22:20:50 -07:00
Harrison ChaseandGitHub fcdf7a8ced add links from concepts guide (#825) 2024-06-25 22:07:06 -07:00
Jacob LeeandGitHub 720ea986d3 Fix misaligned code block (#823) 2024-06-25 21:25:01 -07:00
ab54ae2c23 Harrison/tutorial (#820)
* first draft edits (#818)

Co-authored-by: Harrison Chase <hw.chase.17@gmail.com>

* cr

* cr

---------

Co-authored-by: Isaac Francisco <78627776+isahers1@users.noreply.github.com>
2024-06-25 20:46:01 -07:00
Jacob LeeandGitHub a5650e1d88 Style nits in quickstart (#819)
* Style nits in quickstart

CC @andrewnguonly

* Missing quote

* Update quick_start.md
2024-06-25 19:44:19 -07:00
Jacob LeeandGitHub e49f3f5434 Fix typo (#817)
CC @andrewnguonly
2024-06-25 19:24:20 -07:00
William FHandGitHub f9c720f25a Rm git committers (#815) 2024-06-25 17:09:26 -07:00
David DuongandGitHub 5db7a2dc6a cli: release 0.1.42 (#813) 2024-06-26 00:35:28 +01:00
David DuongandGitHub ac34a4724f chore(cli): use different container port for debugger (#811)
* chore(cli): use different container port for debugger

* Fix test
2024-06-26 00:16:50 +01:00
Vadym BardaandGitHub c13e755c15 docs: add streaming from within a tool (#807) 2024-06-25 15:23:18 -04:00
Andrew NguonlyandGitHub 11bf3bde54 docs: Various updates to LangGraph Cloud docs (#805)
* Change LangGraph Deploy to LangGraph Cloud in CLI reference.

* Update main README to link to Cloud docs. Update How-to Guide link in Cloud index page.

* Create Environments Variable reference page.

* Add Authentication to Conceptual Guide.

* Update setup how-to to refer back to CompiledGraph variable name.

* Update how-to notebooks for double texting.

* Add warning about setting top-level variable for CompiledGraph.
2024-06-25 10:52:23 -07:00
Vadym BardaandGitHub 7490b2b38b docs: fix concepts page format/links (#806) 2024-06-25 13:36:45 -04:00
Vadym BardaandGitHub 97ccdd92b9 docs: add more streaming how-tos (#803) 2024-06-25 13:15:53 -04:00
Andrew NguonlyandGitHub 09b0af0630 docs: Add how-to page for LangGraph Studio (#802)
* Add how-to page for LangGraph Studio.

* Fix spelling error.

* Fix grammar typo.
2024-06-25 01:23:58 -07:00
Andrew NguonlyandGitHub 6a2a81be1c docs: Create how-to for setting up LangGraph app and how-to for deploying to LangGraph Cloud (#801)
* Create how-to for setting up LangGraph app and how-to for deploying to LangGraph Cloud.

* Fix spelling errors.
2024-06-25 00:16:44 -07:00
Andrew NguonlyandGitHub 4fb297471b Update API docs include link to Scalar API site. (#798) 2024-06-24 21:11:40 -07:00
Andrew NguonlyandGitHub 317664be63 Create notebooks for creating cron jobs and stateless runs. (#797) 2024-06-24 17:03:38 -07:00
Andrew NguonlyandGitHub 265f5012c1 docs: Breakup LangGraph Cloud human-in-the-loop notebook (#794)
* Breakup how-to pages for human-in-the-loop.

* Update indentation of Python code in notebooks.
2024-06-24 16:30:18 -07:00
Lance MartinandGitHub 93faaea7bd Update local CRAG ntbk (#795)
* Update local CRAG ntbk

* fmt
2024-06-24 16:24:20 -07:00
Andrew NguonlyandGitHub f867315c2d Add notebook for configuring multiple streaming modes. Update stream events notebook to show token-by-token streaming. (#792) 2024-06-24 15:26:59 -07:00
Vadym BardaandGitHub e80ddab3a5 cli: add analytics (#754) 2024-06-24 18:14:56 -04:00
Lance MartinandGitHub 5e8d4e584b Add CoLab link (#791) 2024-06-24 14:52:18 -07:00
Andrew NguonlyandGitHub ebc9cfb694 docs: Update LangGraph Cloud quickstart (#788)
* Update Cloud quick start.

* Fix spelling errors.
2024-06-24 13:43:18 -07:00
Andrew NguonlyandGitHub b3907b33b2 Add JS SDK how-to page. (#787) 2024-06-24 13:27:36 -07:00
Andrew NguonlyandGitHub 3e7d32cedb Update Cloud index page. (#786) 2024-06-24 13:12:50 -07:00
11cf3b0510 Add ReAct vs Custom agent tutorial (#765)
* Add agent tutorial ntbk

* Update and clean

* Update / finalize

* spelling

* fmt

* unused

* Wfh/craggycrag (#781)

* tmp

* rethought

* Update, finalize analysis

* Update figures

* Remove subdirs

* Fmt

---------

Co-authored-by: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com>
2024-06-24 13:08:34 -07:00
Vadym BardaandGitHub 66ae1ff9b0 CI: run only on file changes (#784) 2024-06-24 14:44:55 -04:00
David DuongandGitHub 393a0e741c feat(js): add ability to specify custom fetch (#783)
* feat(js): add ability to specify custom fetch

* Bump to 0.0.1-rc.14

* Fix format
2024-06-24 13:21:58 +01:00
William FHandGitHub d9b4d021bd [Docs] Update docstrings (#780)
To add more color on the meaning of some arguments.
2024-06-23 17:38:26 -07:00
Andrew NguonlyandGitHub 20f84f2041 docs: Create JS/TS SDK reference page (#779)
* Create JS/TS SDK reference docs.

* Remove index.mts from typedoc entry point.
2024-06-23 17:24:30 -07:00
Harrison ChaseandGitHub 070b339b67 Harrison/human in the loop (#776)
* add human in the loop examples

* cr

* cr

* cr

* cr

* cr

* cr

* cr
2024-06-23 14:35:43 -07:00
Andrew NguonlyandGitHub f39a22098d docs: Add Python SDK reference page (#773)
* Add Python SDK client.

* Update dependencies for docs group.
2024-06-23 12:46:01 -07:00
73e39fcba9 docs: add an example comparing LangGraph & LangGraph API (#764)
* docs: add an example comparing LangGraph & LangGraph API

* stash

* add cloud docs

* cr

* cr

* cr

* cr

---------

Co-authored-by: Harrison Chase <hw.chase.17@gmail.com>
2024-06-22 18:33:17 -07:00
Harrison ChaseandGitHub 06fe906719 minor changes to cloud docs (#769)
* stash

* add cloud docs
2024-06-22 18:15:00 -07:00
Harrison ChaseandGitHub 616c343034 update tutorials and how to guides for langgraph (#768)
* stash

* stash

* stash

* cr

* cr

* cr
2024-06-22 17:30:52 -07:00
Harrison ChaseandGitHub a5aef5c450 langgraph conceptual docs (#767)
* stash

* stash

* cr

* cr
2024-06-22 15:23:44 -07:00
53fd0321cb Fix typo in ReWOO notebook (#755)
Co-authored-by: Vadym Barda <vadym@langchain.dev>
2024-06-22 14:36:21 -07:00
William FHandGitHub a514ad41d4 Format docs (#752) 2024-06-22 14:35:41 -07:00
Nuno CamposandGitHub b4d815e32a Add example for context key (#762) 2024-06-21 18:15:56 -07:00
Nuno Campos cf2bda49a0 0.1.1 2024-06-21 18:15:39 -07:00
Nuno CamposandGitHub 99fa57666e Add support for using Context channel in StateGraph (#761)
* Add support for using Context channel in StateGraph

* Fix handling of asynccontextmanager funcs
2024-06-21 17:49:52 -07:00
Vadym BardaandGitHub 571f1bac27 docs: minor typos (#760) 2024-06-21 20:07:28 -04:00
Vadym BardaandGitHub 44510532ee docs: add link to api references for create_react_agent (#759) 2024-06-21 19:24:14 -04:00
Andrew NguonlyandGitHub 6abcbd5a74 docs: Update API Concepts page, add notebooks for streaming events and streaming debug (#756)
* Update API Concepts page. Add notebooks for streaming events and streaming debug events.

* Remove localhost URL from notebook example.
2024-06-21 15:38:14 -07:00
Vadym BardaandGitHub a29f354d9b docs: update home page (#753) 2024-06-21 17:17:32 -04:00
Vadym BardaandGitHub 19dd293c3f docs: disable TOC on index pages (#751) 2024-06-21 15:57:42 -04:00
Vadym BardaandGitHub c99b63b9d8 docs: update cloud how-tos (#747) 2024-06-21 14:04:25 -04:00
132 changed files with 19562 additions and 4844 deletions
+16 -2
View File
@@ -19,50 +19,64 @@ jobs:
working-directory: libs/cli
steps:
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: Ana06/get-changed-files@v2.2.0
with:
filter: "libs/cli/**"
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
if: steps.changed-files.outputs.all
uses: "./.github/actions/poetry_setup"
with:
python-version: ${{ matrix.python-version }}
poetry-version: ${{ env.POETRY_VERSION }}
working-directory: libs/cli
cache-key: integration-test-cli
- name: Setup env
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples
run: cat .env.example > .env
- name: Install cli globally
if: steps.changed-files.outputs.all
run: pip install -e .
- name: Start service A
if: steps.changed-files.outputs.all
run: |
langgraph up -c examples/langgraph.json --wait --verbose
- name: Stop service A
if: steps.changed-files.outputs.all
run: |
langgraph down -c examples/langgraph.json
sudo rm -rf .langgraph-data
- name: Start service B
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graphs
run: |
langgraph up --wait --verbose
- name: Stop service B
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graphs
run: |
langgraph down
sudo rm -rf .langgraph-data
- name: Start service C
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graphs_reqs_a
run: |
langgraph up --wait -d compose.yml --verbose
- name: Stop service C
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graphs_reqs_a
run: |
langgraph down
sudo rm -rf .langgraph-data
- name: Start service D
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graphs_reqs_b
run: |
langgraph up --wait -d compose.yml --verbose
- name: Stop service D
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graphs_reqs_b
run: |
langgraph down
+14 -1
View File
@@ -32,8 +32,13 @@ jobs:
name: "lint #${{ matrix.python-version }}"
steps:
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: Ana06/get-changed-files@v2.2.0
with:
filter: "${{ inputs.working-directory }}/**"
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
if: steps.changed-files.outputs.all
uses: "./.github/actions/poetry_setup"
with:
python-version: ${{ matrix.python-version }}
@@ -42,16 +47,19 @@ jobs:
cache-key: lint-with-extras
- name: Check Poetry File
if: steps.changed-files.outputs.all
shell: bash
working-directory: ${{ inputs.working-directory }}
run: poetry check
- name: Check lock file
if: steps.changed-files.outputs.all
shell: bash
working-directory: ${{ inputs.working-directory }}
run: poetry lock --check
- name: Install dependencies
if: steps.changed-files.outputs.all
# Also installs dev/lint/test/typing dependencies, to ensure we have
# type hints for as many of our libraries as possible.
# This helps catch errors that require dependencies to be spotted, for example:
@@ -64,6 +72,7 @@ jobs:
run: poetry install --with dev
- name: Get .mypy_cache to speed up mypy
if: steps.changed-files.outputs.all
uses: actions/cache@v3
env:
SEGMENT_DOWNLOAD_TIMEOUT_MIN: "2"
@@ -73,6 +82,7 @@ jobs:
key: mypy-lint-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-${{ inputs.working-directory }}-${{ hashFiles(format('{0}/poetry.lock', inputs.working-directory)) }}
- name: Analysing package code with our lint
if: steps.changed-files.outputs.all
working-directory: ${{ inputs.working-directory }}
run: |
if make lint_package > /dev/null 2>&1; then
@@ -83,6 +93,7 @@ jobs:
fi
- name: Install test dependencies
if: steps.changed-files.outputs.all
# Also installs dev/lint/test/typing dependencies, to ensure we have
# type hints for as many of our libraries as possible.
# This helps catch errors that require dependencies to be spotted, for example:
@@ -96,6 +107,7 @@ jobs:
poetry install --with dev
- name: Get .mypy_cache_test to speed up mypy
if: steps.changed-files.outputs.all
uses: actions/cache@v3
env:
SEGMENT_DOWNLOAD_TIMEOUT_MIN: "2"
@@ -105,6 +117,7 @@ jobs:
key: mypy-test-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-${{ inputs.working-directory }}-${{ hashFiles(format('{0}/poetry.lock', inputs.working-directory)) }}
- name: Analysing tests with our lint
if: steps.changed-files.outputs.all
working-directory: ${{ inputs.working-directory }}
run: |
if make lint_tests > /dev/null 2>&1; then
+9 -1
View File
@@ -24,8 +24,13 @@ jobs:
name: "test #${{ matrix.python-version }}"
steps:
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files
uses: Ana06/get-changed-files@v2.2.0
with:
filter: "${{ inputs.working-directory }}/**"
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
if: steps.changed-files.outputs.all
uses: "./.github/actions/poetry_setup"
with:
python-version: ${{ matrix.python-version }}
@@ -34,17 +39,20 @@ jobs:
cache-key: core
- name: Install dependencies
if: steps.changed-files.outputs.all
shell: bash
working-directory: ${{ inputs.working-directory }}
run: poetry install --with dev
- name: Run core tests
if: steps.changed-files.outputs.all
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
make test
- name: Ensure the tests did not create any additional files
if: steps.changed-files.outputs.all
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
+29 -18
View File
@@ -26,10 +26,10 @@ jobs:
- name: Check links in Markdown files
uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
folder-path: 'examples/'
folder-path: "examples/"
check-modified-files-only: ${{ github.event_name != 'schedule' }}
file-path: './README.md'
config-file: './.markdown-link-check.config.json'
file-path: "./README.md"
config-file: "./.markdown-link-check.config.json"
notebook-link-check:
runs-on: ubuntu-latest
@@ -49,18 +49,29 @@ jobs:
poetry install --with docs
poetry run pip install -U pytest pytest-check-links langsmith langchain GitPython
# - name: Check links in notebooks
# env:
# LANGCHAIN_API_KEY: test
# run: |
# if [ "${{ github.event_name }}" != "schedule" ]; then
# git fetch origin main
# CHANGED_FILES=$(git diff --name-only origin/main | grep '\.ipynb$')
# if [ -n "$CHANGED_FILES" ]; then
# poetry run pytest -o python_files=non_python_only --check-links --check-links-ignore "https://(api|web)\.smith\.langchain\.com/.*" --check-links-ignore "https://x.com/.*" $CHANGED_FILES
# else
# echo "No notebook files changed."
# fi
# else
# poetry run pytest -o python_files=non_python_only --check-links --ignore="*.py" -k .ipynb --check-links-ignore "https://(api|web)\.smith\.langchain\.com/.*" --check-links-ignore "https://x.com/.*" ./examples
# fi
- name: Check links in notebooks
env:
LANGCHAIN_API_KEY: test
run: |
if [ "${{ github.event_name }}" == "schedule" ] || [ "${{ github.event_name }}" == "workflow_dispatch" ] || ([ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == "refs/heads/main" ]); then
echo "Running link check on all notebooks in examples directory..."
poetry run pytest -v --check-links-ignore "https://(api|web)\.smith\.langchain\.com/.*" --check-links-ignore "https://x.com/.*" --check-links examples
else
echo "Fetching changes from origin/main..."
git fetch origin main
echo "Checking for changed notebook files..."
CHANGED_FILES=$(git diff --name-only origin/main | grep '\.ipynb$' || true)
echo "Changed files: ${CHANGED_FILES}"
if [ -n "${CHANGED_FILES}" ]; then
echo "Running link check on changed notebook files..."
poetry run pytest -v --check-links-ignore "https://(api|web)\.smith\.langchain\.com/.*" --check-links-ignore "https://x.com/.*" --check-links ${CHANGED_FILES}
PYTEST_EXIT_CODE=$?
echo "pytest exit code: ${PYTEST_EXIT_CODE}"
if [ ${PYTEST_EXIT_CODE} -ne 0 ]; then
echo "pytest failed with exit code ${PYTEST_EXIT_CODE}"
exit ${PYTEST_EXIT_CODE}
fi
else
echo "No notebook files changed."
fi
fi
+19 -7
View File
@@ -8,6 +8,9 @@
⚡ Building language agents as graphs ⚡
> [!NOTE]
> Looking for the JS version? Click [here](https://github.com/langchain-ai/langgraphjs) ([JS docs](https://langchain-ai.github.io/langgraphjs/)).
## Overview
[LangGraph](https://langchain-ai.github.io/langgraph/) is a library for building stateful, multi-actor applications with LLMs, used to create agent and multi-agent workflows. Compared to other LLM frameworks, it offers these core benefits: cycles, controllability, and persistence. LangGraph allows you to define flows that involve cycles, essential for most agentic architectures, differentiating it from DAG-based solutions. As a very low-level framework, it provides fine-grained control over both the flow and state of your application, crucial for creating reliable agents. Additionally, LangGraph includes built-in persistence, enabling advanced human-in-the-loop and memory features.
@@ -69,7 +72,7 @@ tool_node = ToolNode(tools)
model = ChatOpenAI(temperature=0).bind_tools(tools)
# Define the function that determines whether to continue or not
def should_continue(state: AgentState) -> Literal["tools", END]:
def should_continue(state: MessagesState) -> Literal["tools", END]:
messages = state['messages']
last_message = messages[-1]
# If the LLM makes a tool call, then we route to the "tools" node
@@ -80,7 +83,7 @@ def should_continue(state: AgentState) -> Literal["tools", END]:
# Define the function that calls the model
def call_model(state: AgentState):
def call_model(state: MessagesState):
messages = state['messages']
response = model.invoke(messages)
# We return a list, because this will get added to the existing list
@@ -146,27 +149,31 @@ final_state["messages"][-1].content
'The current weather in New York is as follows:\n- Temperature: 20.3°C (68.5°F)\n- Condition: Overcast\n- Wind: 2.2 mph from the north\n- Humidity: 65%\n- Cloud Cover: 100%\n- UV Index: 5.0\n\nFor more details, you can visit [Weather API](https://www.weatherapi.com/).'
```
### Step-by-step Breakdown:
### Step-by-step Breakdown
1. <details>
<summary>Initialize the model and tools.</summary>
- we use `ChatOpenAI` as our LLM. **NOTE:** we need make sure the model knows that it has these tools available to call. We can do this by converting the LangChain tools into the format for OpenAI tool calling using the `.bind_tools()` method.
- we define the tools we want to use -- a web search tool in our case. It is really easy to create your own tools - see documentation here on how to do that [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools).
- we define the tools we want to use - a web search tool in our case. It is really easy to create your own tools - see documentation here on how to do that [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools).
</details>
2. <details>
<summary>Initialize graph with state.</summary>
- we initialize graph (`StateGraph`) by passing state schema (in our case `MessagesState`)
- `MessagesState` is a prebuilt state schema that has one attribute -- a list of LangChain `Message` objects, as well as logic for merging the updates from each node into the state
</details>
3. <details>
<summary>Define graph nodes.</summary>
There are two main nodes we need:
- The `agent` node: responsible for deciding what (if any) actions to take.
- The `tools` node that invokes tools: if the agent decides to take an action, this node will then execute that action.
</details>
4. <details>
<summary>Define entry point and graph edges.</summary>
@@ -179,12 +186,14 @@ final_state["messages"][-1].content
- b. Finish (respond to the user) if the agent did not ask to run tools
- Normal edge: after the tools are invoked, the graph should always return to the agent to decide what to do next
</details>
5. <details>
<summary>Compile the graph.</summary>
- When we compile the graph, we turn it into a LangChain [Runnable](https://python.langchain.com/v0.2/docs/concepts/#runnable-interface), which automatically enables calling `.invoke()`, `.stream()` and `.batch()` with your inputs
- We can also optionally pass checkpointer object for persisting state between graph runs, and enabling memory, human-in-the-loop workflows, time travel and more. In our case we use `MemorySaver` - a simple in-memory checkpointer
</details>
6. <details>
<summary>Execute the graph.</summary>
@@ -192,8 +201,10 @@ final_state["messages"][-1].content
2. The `"agent"` node executes, invoking the chat model.
3. The chat model returns an `AIMessage`. LangGraph adds this to the state.
4. Graph cycles the following steps until there are no more `tool_calls` on `AIMessage`:
- If `AIMessage` has `tool_calls`, `"tools"` node executes
- The `"agent"` node executes again and returns `AIMessage`
- If `AIMessage` has `tool_calls`, `"tools"` node executes
- The `"agent"` node executes again and returns `AIMessage`
5. Execution progresses to the special `END` value and outputs the final state.
And as a result, we get a list of all our chat messages as output.
</details>
@@ -204,4 +215,5 @@ final_state["messages"][-1].content
* [Tutorials](https://langchain-ai.github.io/langgraph/tutorials/): Learn to build with LangGraph through guided examples.
* [How-to Guides](https://langchain-ai.github.io/langgraph/how-tos/): Accomplish specific things within LangGraph, from streaming, to adding memory & persistence, to common design patterns (branching, subgraphs, etc.), these are the place to go if you want to copy and run a specific code snippet.
* [Conceptual Guides](https://langchain-ai.github.io/langgraph/concepts/): In-depth explanations of the key concepts and principles behind LangGraph, such as nodes, edges, state and more.
* [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Review important classes and methods, simple examples of how to use the graph and checkpointing APIs, higher-level prebuilt components and more.
* [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Review important classes and methods, simple examples of how to use the graph and checkpointing APIs, higher-level prebuilt components and more.
* [Cloud (alpha)](https://langchain-ai.github.io/langgraph/cloud/): With one click, deploy LangGraph applications to LangGraph Cloud.
+71 -15
View File
@@ -1,4 +1,6 @@
import json
import os
import re
import shutil
from pathlib import Path
@@ -14,10 +16,13 @@ cloud_sdk_dir = docs_dir / "cloud"
_MANUAL = {
"how-tos": [
"async.ipynb",
"stream-values.ipynb",
"stream-updates.ipynb",
"stream-multiple.ipynb",
"streaming-tokens.ipynb",
"human-in-the-loop.ipynb",
"streaming-content.ipynb",
"streaming-events-from-within-tools.ipynb",
"persistence.ipynb",
"time-travel.ipynb",
"visualization.ipynb",
"state-model.ipynb",
"subgraph.ipynb",
@@ -30,14 +35,20 @@ _MANUAL = {
"dynamically-returning-directly.ipynb",
"configuration.ipynb",
"map-reduce.ipynb",
"extraction/retries.ipynb",
"create-react-agent.ipynb",
"create-react-agent-system-prompt.ipynb",
"create-react-agent-memory.ipynb",
"create-react-agent-hitl.ipynb",
"human_in_the_loop/breakpoints.ipynb",
"human_in_the_loop/time-travel.ipynb",
"human_in_the_loop/edit-graph-state.ipynb",
"human_in_the_loop/wait-user-input.ipynb",
],
"tutorials": [
"introduction.ipynb",
"customer-support/customer-support.ipynb",
"tutorials/tnt-llm/tnt-llm.ipynb",
"tutorials/sql-agent.ipynb"
"tutorials/sql-agent.ipynb",
],
}
_MANUAL_INVERSE = {v: docs_dir / k for k, vs in _MANUAL.items() for v in vs}
@@ -68,6 +79,20 @@ _HIDE = set(
"rag/langgraph_rag_agent_llama3_local.ipynb",
"rag/langgraph_self_rag_pinecone_movies.ipynb",
"rag/langgraph_adaptive_rag_cohere.ipynb",
"dynamically-returning-directly.ipynb",
"force-calling-a-tool-first.ipynb",
"managing-agent-steps.ipynb",
"pass-run-time-values-to-tools.ipynb",
"respond-in-format.ipynb",
"quickstart.ipynb",
"human-in-the-loop.ipynb",
"learning.ipynb",
"managing-conversation-history.ipynb",
"docs/quickstart.ipynb",
"tutorials/rag-agent-testing.ipynb",
"state-context-key.ipynb",
"time-travel.ipynb",
"code_assistant/langgraph_code_assistant_mistral.ipynb",
]
)
@@ -89,6 +114,44 @@ def clean_notebooks():
os.rmdir(root)
def update_notebook_links(notebook_path):
with open(notebook_path, "r", encoding="utf-8") as f:
notebook = json.load(f)
for cell in notebook["cells"]:
if cell["cell_type"] == "markdown":
for i, source in enumerate(cell["source"]):
# Update relative notebook links
cell["source"][i] = re.sub(
r"\[([^\]]+)\]\(([^:)]+\.ipynb)\)",
lambda m: transform_link(m.group(1), m.group(2)),
source,
)
with open(notebook_path, "w", encoding="utf-8") as f:
json.dump(notebook, f, indent=2)
def transform_link(text, link):
dir_path, filename = os.path.split(link)
# Remove the .ipynb extension
filename_without_ext = os.path.splitext(filename)[0]
# If it's a local link (starts with ./)
if link.startswith("./"):
# Change to parent directory and remove ./ prefix
new_link = f"../{filename_without_ext}/"
elif dir_path:
# If there's a directory path, keep it and add one more level up
new_link = f"../{dir_path}/{filename_without_ext}/"
else:
# If it's just a filename, simply go one level up
new_link = f"../{filename_without_ext}/"
return f"[{text}]({new_link})"
def copy_notebooks():
# Nested ones are mostly tutorials rn
for root, dirs, files in os.walk(examples_dir):
@@ -98,9 +161,9 @@ def copy_notebooks():
continue
if any(path in _HOW_TOS for path in root.split(os.sep)):
dst_dir = how_tos_dir
elif 'sdk' in root.split(os.sep):
elif "sdk" in root.split(os.sep):
dst_dir = cloud_sdk_dir
elif 'cloud_examples' in root.split(os.sep):
elif "cloud_examples" in root.split(os.sep):
dst_dir = cloud_how_tos_dir
else:
dst_dir = tutorials_dir
@@ -139,16 +202,9 @@ def copy_notebooks():
content = content.replace('src=\\"./img/', 'src=\\"../img/')
with open(dst_path, "w") as f:
f.write(content)
update_notebook_links(dst_path)
dst_dir = dst_dir_
# Top level notebooks are "how-to's"
# for file in examples_dir.iterdir():
# if file.suffix.endswith(".ipynb") and not os.path.isdir(
# os.path.join(examples_dir, file)
# ):
# src_path = os.path.join(examples_dir, file)
# dst_path = os.path.join(docs_dir, "how-tos", file.name)
# shutil.copy(src_path, dst_path)
if __name__ == "__main__":
clean_notebooks()
-24
View File
@@ -1,24 +0,0 @@
# API Concepts
This page discusses high-level concepts of the LangGraph Cloud.
## Assistant
An assistant is a configured instance of a [`CompiledGraph`](../reference/graphs/#compiledgraph). It abstracts the cognitive architecture of the graph and contains instance specific configuration and metadata. Multiple assistants can reference the same graph but can contain different configuration and metadata, which may differentiate the behavior of the assistants.
An assistant (i.e. the graph) is invoked as part of a [run](#run).
## Thread
A thread contains the accumulated state of a group of [runs](#run). If a run is executed on a thread, then the [state](../concepts/#state-management) of the underlying graph of the [assistant](#assistant) will be persisted to the thread. A thread's current and historical state can be retrieved.
To persist state, a thread must be created prior to executing a run.
## Run
A run is an invocation of an [assistant](#assistant). Each run may have its own input, configuration, and metadata, which may affect execution and output of the underlying graph. A run can optionally be executed on a [thread](#thread).
## Streaming
Streaming is critical in making applications based on LLMs feel responsive to end-users. There are three different ways to stream with graphs: by [values](../how_tos/cloud_examples/stream_values/), by [messages](../how_tos/cloud_examples/stream_messages/), and by [updates](../how_tos/cloud_examples/stream_updates/).
## Human-in-the-Loop
There are many occasions where the graph cannot run completely autonomously. For instance, the user might need to input some additional arguments to a function call, or select the next edge for the graph to continue on. In these instances, we need to insert some human in the loop interaction, which you can learn about in [this how-to](../how_tos/cloud_examples/human-in-the-loop_cloud).
## Multi-Tasking
Many times users might interact with your graph in unintended ways. For instance, a user interacting with a graph that has chat output could send one message and before the graph has finished running send a second message. To solve this issue of "double-texting" (i.e. prompting the graph a second time before the first run has finished), Langgraph has provided four different solutions, all of which are covered in the [Double Texting how-tos](../how_tos/cloud_examples/interrupt_concurrent/).
+97
View File
@@ -0,0 +1,97 @@
# API Concepts
This page describes the high-level concepts of the LangGraph Cloud API. The conceptual guide of LangGraph (Python library) is [here](../../concepts/index.md).
## Data Models
The LangGraph Cloud API consists of a few core data models: [Assistants](#assistants), [Threads](#threads), [Runs](#runs), and [Cron Jobs](#cron-jobs).
### Assistants
An assistant is a configured instance of a [`CompiledGraph`][compiledgraph]. It abstracts the cognitive architecture of the graph and contains instance specific configuration and metadata. Multiple assistants can reference the same graph but can contain different configuration and metadata, which may differentiate the behavior of the assistants. An assistant (i.e. the graph) is invoked as part of a run.
The LangGraph Cloud API provides several endpoints for creating and managing assistants. See the <a href="../reference/api/api_ref.html#tag/assistantscreate" target="_blank">API reference</a> for more details.
### Threads
A thread contains the accumulated state of a group of runs. If a run is executed on a thread, then the [state][state] of the underlying graph of the assistant will be persisted to the thread. A thread's current and historical state can be retrieved. To persist state, a thread must be created prior to executing a run.
The state of a thread at a particular point in time is called a checkpoint.
For more on threads and checkpoints, see this section of the [LangGraph conceptual guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#checkpointer).
The LangGraph Cloud API provides several endpoints for creating and managing threads and thread state. See the <a href="../reference/api/api_ref.html#tag/threadscreate" target="_blank">API reference</a> for more details.
### Runs
A run is an invocation of an assistant. Each run may have its own input, configuration, and metadata, which may affect execution and output of the underlying graph. A run can optionally be executed on a thread.
The LangGraph Cloud API provides several endpoints for creating and managing runs. See the <a href="../reference/api/api_ref.html#tag/runscreate" target="_blank">API reference</a> for more details.
### Cron Jobs
It's often useful to run graphs on some schedule. LangGraph Cloud supports cron jobs, which run on a user defined schedule. The user specifies a schedule, an assistant, and some input. After than, on the specified schedule LangGraph cloud will:
- Create a new thread with the specified assistant
- Send the specified input to that thread
Note that this sends the same input to the thread every time. See the [how-to guide](../how-tos/cloud_examples/cron_jobs.ipynb) for creating cron jobs.
The LangGraph Cloud API provides several endpoints for creating and managing cron jobs. See the <a href="../reference/api/api_ref.html#tag/runscreate/POST/threads/{thread_id}/runs/crons" target="_blank">API reference</a> for more details.
## Features
The LangGraph Cloud API offers several features to support complex agent architectures.
### Streaming
Streaming is critical for making LLM applications feel responsive to end users. When creating a streaming run, the streaming mode determines what data is streamed back to the API client. The LangGraph Cloud API supports five streaming modes.
- `values`: Stream the full state of the graph after each node is executed. See the [how-to guide](../how-tos/cloud_examples/stream_values.ipynb) for streaming values.
- `messages`: Stream complete messages (at the end of node execution) as well as tokens for any messages generated inside a node. This mode is primarily meant for powering chat applications. This is only an option if your graph contains a `messages` key. See the [how-to guide](../how-tos/cloud_examples/stream_messages.ipynb) for streaming messages.
- `updates`: Streams updates to the state of the graph after each node is executed. See the [how-to guide](../how-tos/cloud_examples/stream_updates.ipynb) for streaming updates.
- `events`: Stream all events (including the state of the graph) after each node is executed. See the [how-to guide](../how-tos/cloud_examples/stream_events.ipynb) for streaming events. This can be used to do token-by-token streaming for LLMs.
- `debug`: Stream debug events after each node is executed. See the [how-to guide](../how-tos/cloud_examples/stream_debug.ipynb) for streaming debug events.
You can also specify multiple streaming modes at the same time. See the [how-to guide](../how-tos/cloud_examples/stream_multiple.ipynb) for configuring multiple streaming modes at the same time.
See the <a href="../reference/api/api_ref.html#tag/runscreate/POST/threads/{thread_id}/runs/stream" target="_blank">API reference</a> for how to create streaming runs.
### Human-in-the-Loop
There are many occasions where the graph cannot run completely autonomously. For instance, the user might need to input some additional arguments to a function call, or select the next edge for the graph to continue on. In these instances, we need to insert some human in the loop interaction, which you can learn about in the [human in the loop how-tos](../how-tos/cloud_examples/human_in_the_loop_breakpoint.ipynb).
### Double Texting
Many times users might interact with your graph in unintended ways. For instance, a user may send one message and before the graph has finished running send a second message. To solve this issue of "double-texting" (i.e. prompting the graph a second time before the first run has finished), Langgraph has provided four different solutions, all of which are covered in the [Double Texting how-tos](../how-tos/cloud_examples/interrupt_concurrent.ipynb). These options are:
- `reject`: This is the simplest option, this just rejects any follow up runs and does not allow double texting. See the [how-to guide](../how-tos/cloud_examples/reject_concurrent.ipynb) for configuring the reject double text option.
- `enqueue`: This is a relatively simple option which continues the first run until it completes the whole run, then sends the new input as a separate run. See the [how-to guide](../how-tos/cloud_examples/enqueue_concurrent.ipynb) for configuring the enqueue double text option.
- `interrupt`: This option interrupts the current execution but saves all the work done up until that point. It then inserts the user input and continues from there. If you enable this option, your graph should be able to handle weird edge cases that may arise. See the [how-to guide](../how-tos/cloud_examples/interrupt_concurrent.ipynb) for configuring the interrupt double text option.
- `rollback`: This option rolls back all work done up until that point. It then sends the user input in, basically as if it just followed the original run input. See the [how-to guide](../how-tos/cloud_examples/rollback_concurrent.ipynb) for configuring the rollback double text option.
### Stateless Runs
All runs use the built-in checkpointer to store checkpoints for runs. However, it can often be useful to just kick off a run without worrying about explicitly creating a thread and without wanting to keep those checkpointers around. Stateless runs allow you to do this by exposing an endpoint that:
- Takes in user input
- Under the hood, creates a thread
- Runs the agent but skips all checkpointing steps
- Cleans up the thread afterwards
Stateless runs are still retried as regular retries are per node, while everything still in memory, so doesn't use checkpoints.
The only difference is in stateless background runs, if the task worker dies halfway (not because the run itself failed, for some external reason) then the whole run will be retried like any background run, but
- whereas a stateful background run would retry from the last successful checkpoint
- a stateless background run would retry from the beginning
See the [how-to guide](../how-tos/cloud_examples/stateless_runs.ipynb) for creating stateless runs.
## Deployment
The LangGraph Cloud offers several features to support secure and robost deployments.
### Authentication
LangGraph applications deployed to LangGraph Cloud are automatically configured with LangSmith authentication. In order to call the API, a valid <a href="https://docs.smith.langchain.com/how_to_guides/setup/create_account_api_key#api-keys" target="_blank">LangSmith API key</a> is required.
+52
View File
@@ -0,0 +1,52 @@
# How to Deploy to LangGraph Cloud
LangGraph Cloud is available within <a href="https://www.langchain.com/langsmith" target="_blank">LangSmith</a>. To deploy a LangGraph Cloud API, navigate to the <a href="https://smith.langchain.com/" target="_blank">LangSmith UI</a>.
## Setup GitHub Repository
LangGraph Cloud applications are deployed from GitHub repositories. Configure and upload a LangGraph Cloud application to a GitHub repository in order to deploy it to LangGraph Cloud.
## Create New Deployment
Starting from the <a href="https://smith.langchain.com/" target="_blank">LangSmith UI</a>...
1. In the left-hand navigation panel, select `Deployments`. The `Deployments` view contains a list of existing LangGraph Cloud deployments.
1. In the top-right corner, select `+ New Deployment` to create a new deployment.
1. In the `Create New Deployment` panel, fill out the required fields.
1. `Deployment details`
1. Select `Import from GitHub` and follow the GitHub OAuth workflow to install and authorize LangChain's `hosted-langserve` GitHub app to access the selected repositories. After installation is complete, return to the `Create New Deployment` panel and select the GitHub repository to deploy from the dropdown menu.
1. Specify a name for the deployment.
1. Specify the full path to the [LangGraph API config file](../reference/cli.md#configuration-file) including the file name. For example, if the file `langgraph.json` is in the root of the repository, simply specify `langgraph.json`.
1. Specify the desired `git` reference (e.g. branch name). For example, different branches of the repository can be deployed.
1. Select the desired `Deployment Type`.
1. `Development` deployments are meant for non-production use cases and are provisioned with minimal resources.
1. `Production` deployments can serve up to 500 requests/second and are provisioned with highly available storage with automatic backups.
1. Specify `Environment Variables` and secrets. See the [Environment Variables reference](../reference/env_var.md) to configure additional variables for the deployment.
1. Sensitive values such as API keys (e.g. `OPENAI_API_KEY`) should be specified as secrets.
1. Additional non-secret environment variables can be specified as well.
1. A new LangSmith `Tracing Project` is automatically created with the same name as the deployment.
1. In the top-right corner, select `Submit`. After a few seconds, the `Deployment` view appears and the new deployment will be queued for provisioning.
## Create New Revision
When [creating a new deployment](#create-new-deployment), a new revision is created by default. Subsequent revisions can be created to deploy new code changes.
Starting from the <a href="https://smith.langchain.com/" target="_blank">LangSmith UI</a>...
1. In the left-hand navigation panel, select `Deployments`. The `Deployments` view contains a list of existing LangGraph Cloud deployments.
1. Select an existing deployment to create a new revision for.
1. In the `Deployment` view, in the top-right corner, select `+ New Revision`.
1. In the `New Revision` modal, fill out the required fields.
1. Specify the full path to the [LangGraph API config file](../reference/cli.md#configuration-file) including the file name. For example, if the file `langgraph.json` is in the root of the repository, simply specify `langgraph.json`.
1. Specify the desired `git` reference (e.g. branch name). For example, different branches of the repository can be deployed.
1. Specify `Environment Variables` and secrets. Existing secrets and environment variables are prepopulated. See the [Environment Variables reference](../reference/env_var.md) to configure additional variables for the revision.
1. Add new secrets or environment variables.
1. Remove existing secrets or environment variables.
1. Update the value of existing secrets or environment variables.
1. Select `Submit`. After a few seconds, the `New Revision` modal will close and the new revision will be queued for deployment.
## Asynchronous Deployment
New [deployments](#create-new-deployment) and [revisions](#create-new-revision) are provisioned and deployed asynchronously. They are not deployed immediately after submission. Currently, deployment can take up to several minutes.
The `Deployment` view continually updates the status of pending revisions.
Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 59 KiB

-157
View File
@@ -1,157 +0,0 @@
# Deploy custom LangGraph code with LangGraph Cloud (Python)
## Set up your application code
### Create a new application
To create a new app called <my-app> create a directory with the following structure
```
<my-app>/
|-- agent.py # code for your LangGraph agent
|-- requirements.txt # python packages required for your graph
|-- langgraph.json # configuration file for langgraph
|-- .env # environment files with API keys
```
### Agent File
In your agent file, you can define as many graphs (agents) as you would like. For our example we are going to create the simplest graph possible: a one node graph. You can read about adding more complexity to your graphs in the [docs](https://langchain-ai.github.io/langgraph/tutorials/introduction/).
Here is what our `agent.py` file looks like for this example:
```python
from langchain_openai import ChatOpenAI
from langgraph.graph import END, MessageGraph
model = ChatOpenAI(temperature=0)
graph_workflow = MessageGraph()
graph_workflow.add_node("oracle", model)
graph_workflow.add_edge("oracle", END)
graph_workflow.set_entry_point("oracle")
graph = graph_workflow.compile()
```
### Configuration file
- `langgraph.json` is a configuration file with three parts:
- `graphs`
- Pass in the graphs you want to host on your deployment, using the graph_id as the key and the path to the agent (a CompiledGraph) as the value. In our example we only use one graph, so the json looks like so:
```json
"graphs": {
"agent": "./agent.py:graph"
},
```
- `dependencies`
- Pass in a list of the dependencies you would like to be installed in order to host your app. In our case, we dont need any additional dependencies besides our `requirements.txt` file, but if we did we could append them to the dependencies list using the names of the additional packages we want installed.
```json
"dependencies": ["."],
```
- `env`
- This is simply a path to our environment file containing all variables/files to load.
```json
"env": ".env"
```
Putting it all together, our `langgraph.json` file should look like this:
```json
{
"dependencies": ["."],
"graphs": {
"agent": "./agent.py:agent"
},
"env": ".env"
}
```
### Environment and Package Requirement files
- The `.env` contains any environment variables that are needed to run your code. In our example the file looks like follows:
```python
OPENAI_API_KEY= ...
LANGCHAIN_API_KEY= ...
LANGCHAIN_TRACING_V2=true
```
- The `requirements.txt` file lists python package dependencies for your project (along with the associated versions if necessary). In our example the file looks like this:
```
langgraph
langchain_openai
```
### Push your code to GitHub
Create a git repo in the `<my-app>` directory, and verify its existence. You can use the GitHub CLI if you like, or just create a repo manually.
## Host your code on LangGraph Cloud
### Deploy from GitHub with LangGraph Cloud
Head to LangSmith and click on the 🚀 icon on the left navbar to create a new deployment. Click the `+ New Deployment` button.
***If you have not deployed to LangGraph Cloud before:*** there will be a button that shows up saying Import from GitHub. Youll need to follow that flow to connect LangGraph Cloud to GitHub.
***Once you have set up your GitHub connection:*** the new deployment page will look as follows
![Screenshot 2024-06-11 at 1.17.03PM.png](./img/deployment_page.png)
To deploy your application, you should do the following:
1. Select your GitHub username or organization from the selector
2. Search for your repo to deploy in the search bar and select it
3. Choose any name
4. In the `LangGraph API config file` field, enter the path to your `langgraph.json` file (if left blank langsmith will automatically search for it on deployment)
5. For Git Reference, you can select either the git branch for the code you want to deploy, or the exact commit SHA.
6. If your chain relies on environment variables (for example, an OPENAI_API_KEY), add those in. They will be propagated to the underlying server so your code can access them.
Putting this all together, you should have something as follows for your deployment details:
![Screenshot 2024-06-11 at 1.21.52PM.png](./img/deploy_filled_out.png)
Hit `Submit` and your application will start deploying!
## Inspect Traces + Monitor Service
### Deployments View
After your deployment is complete, your deployments page should look as follows:
![Screenshot 2024-06-11 at 2.03.34PM.png](./img/deployed_page.png)
You can see that by default, you get access to the `Trace Count` monitoring chart and `Recent Traces` run view. These are powered by LangSmith.
You can click on `All Charts` to view all monitoring info for your server, or click on `See tracing project` to get more information on an individual trace.
### Access the Docs
You can access the docs by clicking on the API DOCS link, which should send you to a page that looks like this:
![Screenshot 2024-06-19 at 2.27.24PM.png](./img/api_page.png)
You wont actually be able to test any of the API endpoints without authorizing first. To do so, click on the Authorize button in the top right corner, input your `LANGCHAIN_API_KEY` in the `API Key` box, and then click `Authorize` to finish the process. You should now be able to select any of the API endpoints, click `Try it out` , enter the parameters you would like to pass, and then click `Execute` to view the results of the API call.
## Interact with your deployment via LangGraph Studio
### Access Studio
If you click on your deployment you should see a blue button in the top right that says `LangGraph Studio`. Clicking on this button will take you to a page that looks like this:
![Screenshot 2024-06-11 at 2.51.51PM.png](./img/graph_visualiztion)
On this page you can test out your graph by passing in starting states and clicking `Start Run` (this should behave identically to calling `.invoke`). You will then be able to look into the execution thread for each run and explore the steps your graph is taking to produce its output.
## Deploy new code
To deploy new code that you push to GitHub, simply navigate to the deployments page, and hit `+ New Revision`. LangGraph Cloud releases what it calls a new “revision” every time you deploy code. Therefore, your first deployment automatically showed revisions. A Revision always corresponds to a new piece of code being deployed. A modal will pop up to enter new revision info. This can be thought of as a partial update on the last revision, so you do not need to enter any fields that didnt change (*note: environment variables are not saved between revisions, you must re-enter them for each new revision)*.
+2 -8
View File
@@ -1,9 +1,3 @@
### Run your server locally
# How to Self-Host LangGraph Cloud API
First, make sure that Docker is up and running. Test that your server works by running:
```python
langgraph up -c langgraph.json
```
This will bring up a local server with your graph! Access the auto-generated server for your playground to confirm everything works as planned at [http://localhost:8124](http://localhost:8124) .
Coming soon...
+104
View File
@@ -0,0 +1,104 @@
# How to Set Up a LangGraph Application for Deployment
A LangGraph application must be configured with a [LangGraph API configuration file](../reference/cli.md#configuration-file) in order to be deployed to LangGraph Cloud (or to be self-hosted). This how-to guide discusses the basic steps to setup a LangGraph application for deployment.
After each step, an example file directory is provided to demonstrate how code can be organized.
## Specify Dependencies
Dependencies can optionally be specified in one of the following files: `pyproject.toml`, `setup.py`, or `requirements.txt`. If neither of these files is created, then dependencies can be specified later in the [LangGraph API configuration file](#create-langgraph-api-config).
Example `requirements.txt` file:
```
langgraph
langchain_openai
```
Example file directory:
```
my-app/
|-- requirements.txt # Python packages required for your graph
```
## Specify Environment Variables
Environment variables can optionally be specified in a file (e.g. `.env`). See the [Environment Variables reference](../reference/env_var.md) to configure additional variables for a deployment.
Example `.env` file:
```
MY_ENV_VAR_1=foo
MY_ENV_VAR_2=bar
```
Example file directory:
```
my-app/
|-- requirements.txt
|-- .env # file with environment variables
```
## Define Graphs
Implement your graphs! Graphs can be defined in a single file or multiple files. Make note of the variable names of each [CompiledGraph][compiledgraph] to be included in the LangGraph application. The variable names will be used later when creating the [LangGraph API configuration file](../reference/cli.md#configuration-file).
Example `openai_agent.py` file:
```python
from langchain_openai import ChatOpenAI
from langgraph.graph import END, MessageGraph
model = ChatOpenAI(temperature=0)
graph_workflow = MessageGraph()
graph_workflow.add_node("agent", model)
graph_workflow.add_edge("agent", END)
graph_workflow.set_entry_point("agent")
agent = graph_workflow.compile()
```
!!! warning "Assign `CompiledGraph` to Variable"
The build process for LangGraph Cloud requires that the `CompiledGraph` object be assigned to a variable at the top-level of a Python module.
Example file directory:
```
my-app/
|-- requirements.txt
|-- .env
|-- openai_agent.py # code for your graph
|-- anthropic_agent.py # code for your graph
```
## Create LangGraph API Config
Create a [LangGraph API configuration file](../reference/cli.md#configuration-file) called `langgraph.json`. See the [LangGraph CLI reference](../reference/cli.md#configuration-file) for detailed explanations of each key in the JSON object of the configuration file.
Example `langgraph.json` file:
```json
{
"dependencies": [
"./my-app"
],
"graphs": {
"openai_agent": "./openai_agent.py:agent",
"anthropic_agent": "./anthropic_agent.py:agent"
},
"env": ".env"
}
```
Note that the variable name of the `CompiledGraph` appears at the end of the value of each subkey in the top-level `graphs` key (i.e. `:<variable_name>`).
Example file directory:
```
my-app/
|-- requirements.txt
|-- .env
|-- openai_agent.py
|-- anthropic_agent.py
|-- langgraph.json # configuration file for LangGraph
```
## Upload to GitHub
To deploy the LangGraph application to LangGraph Cloud, the code must be uploaded to a GitHub repository.
+44
View File
@@ -0,0 +1,44 @@
# How to Test with LangGraph Studio
LangGraph applications can be tested with LangGraph Studio. LangGraph Studio is a robust UI for testing and exercising functionality of the graphs in a LangGraph application. The LangGraph Studio UI connects directly to a LangGraph Cloud deployments or to a local LangGraph API instance.
The LangGraph Studio UI is available within <a href="https://www.langchain.com/langsmith" target="_blank">LangSmith</a>. To test a LangGraph application, navigate to the <a href="https://smith.langchain.com/" target="_blank">LangSmith UI</a>.
## Test Cloud Deployment
The LangGraph Studio UI connects directly to LangGraph Cloud deployments.
Starting from the <a href="https://smith.langchain.com/" target="_blank">LangSmith UI</a>...
1. In the left-hand navigation panel, select `Deployments`. The `Deployments` view contains a list of existing LangGraph Cloud deployments.
1. Select an existing deployment to test with LangGraph Studio.
1. In the top-right corner, select `Open LangGraph Studio`.
1. [Invoke an assistant](#invoke-assistant) or [view an existing thread](#view-thread).
## Test Local Instance
The LangGraph Studio UI connects directly to local LangGraph API instances. This is helpful for quickly iterating and testing during the development process.
Starting from the <a href="https://smith.langchain.com/" target="_blank">LangSmith UI</a>...
1. Find the hostname of the local LangGraph API instance. For example, `http://localhost:9123`.
1. Navigate to the URL `/studio/thread/` and set the `baseUrl` query parameter to the hostname. For example, `https://smith.langchain.com/studio/thread?baseUrl=http://localhost:9123`.
1. [Invoke an assistant](#invoke-assistant) or [view an existing thread](#view-thread).
## Invoke Assistant
1. The LangGraph Studio UI displays a visualization of the selected assistant.
1. In the top-right dropdown menu of the left-hand pane, select an assistant.
1. In the bottom of the left-hand pane, edit the `Input` and `Configure` the assistant.
1. Select `Submit` to invoke the selected assistant.
1. View output of the invocation in the right-hand pane.
## View Thread
1. In the top of the right-hand pane, select the `New Thread` dropdown menu to view existing threads.
1. View the state of the thread (i.e. the output) in the right-hand pane.
1. To create a new thread, select `+ New Thread`.
## Edit Thread State
The LangGraph Studio UI contains features for editing thread state. Explore these features in the right-hand pane. Select the `Edit` icon, modify the desired state, and then select `Fork` to invoke the assistant with the updated state.
Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

+23 -1
View File
@@ -6,6 +6,28 @@
!!! warning "Under Construction"
LangGraph Cloud documentation is under construction. Contents may change until general availability.
LangGraph Cloud is a managed service for deploying and hosting LangGraph applications. Deploying your application with LangGraph Cloud shortens the time-to-market for developers. With one click, start a production-ready HTTP microservice with built-in persistence for your LangGraph application.
## Overview
LangGraph Cloud is a managed service for deploying and hosting LangGraph applications. Deploying applications with LangGraph Cloud shortens the time-to-market for developers. With one click, deploy a production-ready API with built-in persistence for your LangGraph application. LangGraph Cloud APIs are horizontally scalable and deployed with durable storage.
The LangGraph Cloud API exposes functionality of your LangGraph application through [Assistants](./concepts/index.md#assistants). An assistant abstracts the cognitive architecture of your graph. Invoke an assistant by calling the pre-built [API endpoints](./reference/api/api_ref.md).
LangGraph Cloud is seamlessly integrated with [LangSmith](https://www.langchain.com/langsmith) and is accessible from within the LangSmith UI.
## Key Features
The LangGraph Cloud API supports key LangGraph features in addition to new functionality for enabling complex, agentic workflows.
- **Assistants and Threads**: Assistants abstract the cognitive architecture of graphs and threads track the state/history of graphs.
- **Streaming**: API support for [LangGraph streaming modes](../concepts/low_level.md#streaming) including setting multiple streaming modes at the same time.
- **Human-in-the-Loop**: API support for [LangGraph human-in-the-loop features](../concepts/agentic_concepts.md#human-in-the-loop).
- **Double Texting**: Configure how assistants respond when new input is received while processing a previous input. Interrupt, rollback, reject, or enqueue.
- **Background Runs/Cron Jobs**: A built-in task queue enables background runs and scheduled cron jobs.
- **Stateless Runs**: For simpler use cases, invoke an assistant without needing to create a thread.
## Documentation
- [Tutorials](./quick_start.md): Learn to build and deploy applications for LangGraph Cloud.
- [How-to Guides](./deployment/setup.md): Learn how to set up a LangGraph application for deployment and implement features of the LangGraph Cloud API such as streaming tokens, configuring double texting, and creating cron jobs. Go here if you want to copy and run a specific code snippet.
- [Conceptual Guides](./concepts/index.md): In-depth explanations of the core data models (e.g. assistants) and key features (e.g. double texting) of the LangGraph Cloud API.
- [Reference](./reference/api/api_ref.md): References for the LangGraph Cloud API, the corresponding Python and JS/TS SDKs, the LangGraph CLI, and deployment environment variables.
+159 -33
View File
@@ -1,9 +1,9 @@
# Quick Start
This quick start guide will cover how to develop an application for LangGraph Cloud, run it locally in Docker, and call the APIs to invoke a graph.
This quick start guide will cover how to build a simple chatbot with LangGraph, deploy it to LangGraph Cloud, use the LangGraph Studio to visualize and test it out, and use the LangGraph Cloud SDK to interact with it.
Alternatively, clone or fork the [`langgraph/example`](https://github.com/langchain-ai/langgraph-example) GitHub repository and follow the instructions in the `README`.
## Develop
## Set up local files
1. Create a new application with the following directory and files:
<my-app>/
@@ -12,29 +12,35 @@ Alternatively, clone or fork the [`langgraph/example`](https://github.com/langch
|-- langgraph.json # configuration file for LangGraph
|-- .env # environment files with API keys
2. The `agent.py` file should contain the following Python code for defining a simple graph:
2. The `agent.py` file should contain Python code for defining your graph. The following code is a simple example, the important thing is that at some point in your file you compile your graph and assign the compiled graph to a variable (in this case the `graph` variable).
```python
from langchain_openai import ChatOpenAI
from langgraph.graph import END, MessageGraph
model = ChatOpenAI(temperature=0)
graph_workflow = MessageGraph()
graph_workflow.add_node("agent", model)
from langchain_anthropic import ChatAnthropic
from langgraph.graph import END, StateGraph, MessagesState
model = ChatAnthropic(model="claude-3-5-sonnet-20240620")
graph_workflow = StateGraph(MessagesState)
def agent(state: MessagesState):
response = model.invoke(state["messages"])
return {"messages": [response]}
graph_workflow.add_node(agent)
graph_workflow.add_edge("agent", END)
graph_workflow.set_entry_point("agent")
graph = graph_workflow.compile()
```
3. The `requirements.txt` file should contain the following dependencies:
3. The `requirements.txt` file should contain any dependencies for your graph(s). In this case we only require two packages for our graph to run:
langgraph
langchain_openai
langchain_anthropic
4. The `langgraph.json` file should contain the following JSON object:
4. The `langgraph.json` file is a configuration file that describes what graph(s) you are going to host. In this case we only have one graph to host: the compiled `graph` object from `agent.py`.
```json
{
@@ -48,23 +54,143 @@ Alternatively, clone or fork the [`langgraph/example`](https://github.com/langch
Learn more about the LangGraph CLI configuration file [here](./reference/cli.md#configuration-file).
5. The `.env` file should contain the environment variables:
OPENAI_API_KEY=<add your key here>
LANGGRAPH_AUTH_TYPE=noop
!!! warning "Disable Authentication"
When testing locally, set `LANGGRAPH_AUTH_TYPE` to `noop` to disable authentication.
## Run Locally
1. Install the [LangGraph CLI](./reference/cli.md#installation).
2. Run the following command to start the API server in Docker:
langgraph up -c langgraph.json
3. The API server is now running at `http://localhost:8123`. Navigate to [`http://localhost:8123/docs`](http://localhost:8123/docs) to view the API docs.
Now that we have set everything up on our local file system, we are ready to host our graph.
## Deploy to Cloud
Follow [these instructions](./deployment/managed.md#deploy-from-github-with-hosted-langgraph) to deploy to LangGraph Cloud.
### Push your code to GitHub
Turn the `<my-app>` directory into a GitHub repo. You can use the GitHub CLI if you like, or just create a repo manually (if unfamiliar, instructions [here](https://docs.github.com/en/migrations/importing-source-code/using-the-command-line-to-import-source-code/adding-locally-hosted-code-to-github)).
### Deploy from GitHub with LangGraph Cloud
Once you have created your github repository with a Python file containing your compiled graph as well as a `langgraph.json` file containing the configuration for hosting your graph, you can head over to LangSmith and click on the 🚀 icon on the left navbar to create a new deployment. Then click the `+ New Deployment` button.
![Langsmith Workflow](./img/cloud_deployment.png)
***If you have not deployed to LangGraph Cloud before:*** there will be a button that shows up saying Import from GitHub. Youll need to follow that flow to connect LangGraph Cloud to GitHub.
***Once you have set up your GitHub connection:*** the new deployment page will look as follows:
![Deployment before being filled out](./deployment/img/deployment_page.png)
To deploy your application, you should do the following:
1. Select your GitHub username or organization from the selector
2. Search for your repo to deploy in the search bar and select it
3. Choose any name
4. In the `LangGraph API config file` field, enter the path to your `langgraph.json` file (if left blank langsmith will automatically search for it on deployment)
5. For Git Reference, you can select either the git branch for the code you want to deploy, or the exact commit SHA.
6. If your chain relies on environment variables (for example, an OPENAI_API_KEY), add those in. They will be propagated to the underlying server so your code can access them.
Putting this all together, you should have something as follows for your deployment details:
![Deployment filled out](./deployment/img/deploy_filled_out.png)
Hit `Submit` and your application will start deploying!
## Inspect Traces + Monitor Service
### Deployments View
After your deployment is complete, your deployments page should look as follows:
![Deployed page](./deployment/img/deployed_page.png)
You can see that by default, you get access to the `Trace Count` monitoring chart and `Recent Traces` run view. These are powered by LangSmith.
You can click on `All Charts` to view all monitoring info for your server, or click on `See tracing project` to get more information on an individual trace.
### Access the Docs
You can access the docs by clicking on the API docs link, which should send you to a page that looks like this:
![API Docs page](./deployment/img/api_page.png)
You wont actually be able to test any of the API endpoints without authorizing first. To do so, click on the Authorize button in the top right corner, input your `LANGCHAIN_API_KEY` in the `API Key` box, and then click `Authorize` to finish the process. You should now be able to select any of the API endpoints, click `Try it out`, enter the parameters you would like to pass, and then click `Execute` to view the results of the API call.
## Interact with your deployment via LangGraph Studio
If you click on your deployment you should see a blue button in the top right that says `LangGraph Studio`. Clicking on this button will take you to a page that looks like this:
![Studio UI before being run](./deployment/img/graph_visualization.png)
On this page you can test out your graph by passing in starting states and clicking `Start Run` (this should behave identically to calling `.invoke`). You will then be able to look into the execution thread for each run and explore the steps your graph is taking to produce its output.
![Studio UI once being run](./deployment/img/graph_run.png)
## Use with the SDK
Once you have tested that your hosted graph works as expected using LangGraph Studio, you can start using your hosted graph all over your organization by using the LangGraph SDK. Let's see how we can access our hosted graph and execute our run from a python file.
First, make sure you have the SDK installed by calling `pip install langgraph_sdk`.
Before using, you need to get the URL of your LangGraph deployment. You can find this on the auto generated documentation page here:
![Base URL of LangGraph deployment](./deployment/img/base_url.png)
You also need to make sure you have set up your API key properly so you can authenticate with LangGraph Cloud.
```shell
export LANGCHAIN_API_KEY=...
```
The first thing to do when using the SDK is to setup our client, access our assistant, and create a thread to execute a run on:
```python
from langgraph_sdk import get_client
# Replace this with the URL of your own deployed graph
URL = "https://chatbot-23a570f3210f52a7b167f09f6158e3b3-ffoprvkqsa-uc.a.run.app"
client = get_client(url=URL)
# Search all hosted graphs
assistants = await client.assistants.search()
# In this example we select the first assistant since we are only hosting a single graph
assistant = assistants[0]
# We create a thread for tracking the state of our run
thread = await client.threads.create()
```
We can then execute a run on the thread:
```python
input = {"messages":[{"role": "user", "content": "Hello! My name is Bagatur and I am 26 years old."}]}
async for chunk in client.runs.stream(
thread['thread_id'],
assistant["assistant_id"],
input=input,
stream_mode="updates",
):
if chunk.data and "run_id" not in chunk.data:
print(chunk.data)
```
{'agent': {'messages': [{'content': "Hi Bagatur! It's nice to meet you. How can I assist you today?", 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_9cb5d38cf7'}, 'type': 'ai', 'name': None, 'id': 'run-c89118b7-1b1e-42b9-a85d-c43fe99881cd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
You can learn more about the Python SDK in [this how-to guide](./sdk/python_sdk.ipynb), and read up on the Javascript SDK in [this how-to guide](./sdk/js_sdk.ipynb)
## What's Next
Congratulations! If you've worked your way through this tutorial you are well on your way to becoming a LangGraph Cloud expert. Here are some other resources to check out to help you out on the path to expertise:
### LangGraph Cloud How-tos
If you want to learn more about streaming from hosted graphs, check out the Streaming [how-to guides](https://langchain-ai.github.io/langgraph/cloud/how-tos/cloud_examples/stream_values/).
To learn more about double-texting and all the ways you can handle it in your application, read up on these [how-to guides](https://langchain-ai.github.io/langgraph/cloud/how-tos/cloud_examples/interrupt_concurrent/).
To learn about how to include different human-in-the-loop behavior in your graph, take a look at [these how-tos](https://langchain-ai.github.io/langgraph/cloud/how-tos/cloud_examples/human_in_the_loop_breakpoint/).
### LangGraph Tutorials
Before hosting, you have to write a graph to host. Here are some tutorials to get you more comfortable with writing LangGraph graphs and give you inspiration for the types of graphs you want to host.
[This tutorial](https://langchain-ai.github.io/langgraph/tutorials/customer-support/customer-support/) walks you through how to write a customer support bot using LangGraph.
If you are interested in writing a SQL agent, check out [this tutorial](https://langchain-ai.github.io/langgraph/tutorials/sql-agent/).
Check out the [LangGraph tutorials](https://langchain-ai.github.io/langgraph/tutorials/) page to read about more exciting use cases.
@@ -0,0 +1,19 @@
<!doctype html>
<html>
<head>
<title>LangGraph Cloud API Reference</title>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1" />
</head>
<body>
<script id="api-reference" data-url="./openapi.json"></script>
<script>
var configuration = {}
document.getElementById('api-reference').dataset.configuration =
JSON.stringify(configuration)
</script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
# API Reference
The LangGraph Cloud API reference is available with each deployment at the `/docs` URL path (e.g. `http://localhost:8124/docs`).
Click <a href="/langgraph/cloud/reference/api/api_ref.html" target="_blank">here</a> to view the API reference.
File diff suppressed because it is too large Load Diff
-2
View File
@@ -1,2 +0,0 @@
# API Reference
Coming soon
+10 -10
View File
@@ -1,17 +1,17 @@
# LangGraph CLI
The LangGraph CLI includes commands to build and run a LangGraph Cloud server locally in [Docker](https://www.docker.com/). For development and testing, use the CLI to deploy a local API server.
The LangGraph CLI includes commands to build and run a LangGraph Cloud API server locally in [Docker](https://www.docker.com/). For development and testing, use the CLI to deploy a local API server.
## Installation
1. Ensure that Docker is installed (e.g. `docker --version`).
1. Install the `langgraph-cli` Python package (e.g. `pip install langgraph-cli`).
1. Run the command `langgraph --help` to confirm that the CLI is installed.
2. Install the `langgraph-cli` Python package (e.g. `pip install langgraph-cli`).
3. Run the command `langgraph --help` to confirm that the CLI is installed.
## Configuration File
The LangGraph CLI requires a JSON configuration file with the following keys:
| Key | Description |
| --- | ----------- |
| `dependencies` | **Required**. Array of dependencies for LangGraph Deploy API server. Dependencies can be one of the following: (1) `"."`, which will look for local Python packages, (2) `pyproject.toml`, `setup.py` or `requirements.txt` in the app directory `"./local_package"`, or (3) a package name. |
| `dependencies` | **Required**. Array of dependencies for LangGraph Cloud API server. Dependencies can be one of the following: (1) `"."`, which will look for local Python packages, (2) `pyproject.toml`, `setup.py` or `requirements.txt` in the app directory `"./local_package"`, or (3) a package name. |
| `graphs` | **Required**. Mapping from graph ID to path where the compiled graph is defined. Example: `./your_package/your_file.py:variable`, where `variable` is an instance of `langgraph.graph.graph.CompiledGraph`. |
| `env` | Path to `.env` file or a mapping from environment variable to its value. |
| `python_version` | `3.11` or `3.12`. Defaults to `3.11`. |
@@ -65,7 +65,7 @@ langgraph [OPTIONS] COMMAND [ARGS]
```
### `build`
Build LangGraph Deploy API server Docker image.
Build LangGraph Cloud API server Docker image.
**Usage**
```
@@ -78,12 +78,12 @@ langgraph build [OPTIONS]
| ------ | ------- | ----------- |
| `--platform TEXT` | | Target platform(s) to build the Docker image for. Example: `langgraph build --platform linux/amd64,linux/arm64` |
| `-t, --tag TEXT` | | **Required**. Tag for the Docker image. Example: `langgraph build -t my-image` |
| `--pull / --no-pull` | `--pull` | Build with latest remote Docker image. Use `--no-pull` for running the LangGraph Deploy API server with locally built images. |
| `--pull / --no-pull` | `--pull` | Build with latest remote Docker image. Use `--no-pull` for running the LangGraph Cloud API server with locally built images. |
| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables. |
| `--help` | | Display command documentation. |
### `down`
Stop LangGraph Deploy API server.
Stop LangGraph Cloud API server.
**Usage**
```
@@ -102,7 +102,7 @@ langgraph down [OPTIONS]
| `--help` | | Display command documentation. |
### `logs`
Show LangGraph Deploy API server logs.
Show LangGraph Cloud API server logs.
**Usage**
```
@@ -119,7 +119,7 @@ langgraph logs [OPTIONS]
| `--help` | | Display command documentation. |
### `up`
Start LangGraph Deploy API server.
Start LangGraph Cloud API server.
**Usage**
```
@@ -137,6 +137,6 @@ langgraph up [OPTIONS]
| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables. |
| `-d, --docker-compose FILE` | | Advanced. Path to `docker-compose.yml` file with additional services to launch. |
| `-p, --port INTEGER` | `8123` | Port to expose. Example: `langgraph up --port 8000` |
| `--pull / --no-pull` | `--pull` | Build with latest remote Docker image. Use `--no-pull` for running the LangGraph Deploy API server with locally built images. |
| `--pull / --no-pull` | `--pull` | Build with latest remote Docker image. Use `--no-pull` for running the LangGraph Cloud API server with locally built images. |
| `--recreate / --no-recreate` | `--no-recreate` | Recreate containers even if their configuration and image haven't changed. |
| `--help` | | Display command documentation. |
+13
View File
@@ -0,0 +1,13 @@
# Environment Variables
The LangGraph Cloud API supports specific environment variables for configuring a deployment.
## `LANGGRAPH_AUTH_TYPE`
Type of authentication for the LangGraph Cloud API deployment. Valid values: `langsmith`, `noop`.
For deployments to LangGraph Cloud, this environment variable is set automatically. For local development or deployments where authentication is handled externally (e.g. self-hosted), set this environment variable to `noop`.
## `N_JOBS_PER_WORKER`
Number of jobs per worker for the LangGraph Cloud task queue. Defaults to `10`.
File diff suppressed because it is too large Load Diff
@@ -1,2 +1,79 @@
# Python SDK Reference
Coming soon
The Python SDK provides four underlying clients (`AssistantsClient`, `ThreadsClient`, `RunsClient`, `CronClient`) that correspond to each of the core API models and one top-level client (`LangGraphClient`) to access them.
## get_client()
The `get_client()` function returns the top-level `LangGraphClient` client.
```python
from langgraph_sdk import get_client
# get top-level LangGraphClient
client = get_client(url="http://localhost:8123")
# example usage: client.<model>.<method_name>()
assistants = await client.assistants.get(assistant_id="some_uuid")
```
::: langgraph_sdk.client.get_client
handler: python
## LangGraphClient
`LangGraphClient` is the top-level client for accessing `AssistantsClient`, `ThreadsClient`, `RunsClient`, and `CronClient`.
::: langgraph_sdk.client.LangGraphClient
handler: python
## AssistantsClient
Access the `AssistantsClient` via the `LangGraphClient.assistants` attribute.
```python
from langgraph_sdk import get_client
client = get_client(url="http://localhost:8123")
await client.assistants.<method_name>()
```
::: langgraph_sdk.client.AssistantsClient
handler: python
## ThreadsClient
Access the `ThreadsClient` via the `LangGraphClient.threads` attribute.
```python
from langgraph_sdk import get_client
client = get_client(url="http://localhost:8123")
await client.threads.<method_name>()
```
::: langgraph_sdk.client.ThreadsClient
handler: python
## RunsClient
Access the `RunsClient` via the `LangGraphClient.runs` attribute.
```python
from langgraph_sdk import get_client
client = get_client(url="http://localhost:8123")
await client.runs.<method_name>()
```
::: langgraph_sdk.client.RunsClient
handler: python
## CronClient
Access the `CronClient` via the `LangGraphClient.crons` attribute.
```python
from langgraph_sdk import get_client
client = get_client(url="http://localhost:8123")
await client.crons.<method_name>()
```
::: langgraph_sdk.client.CronClient
handler: python
+99
View File
@@ -0,0 +1,99 @@
# Common Agentic Patterns
## Structured Output
It's pretty common to want LLMs inside nodes to return structured output when building agents. This is because that structured output can often be used to route to the next step (e.g. choose between two different edges) or update specific keys of the state.
Since LangGraph nodes can be arbitrary Python functions, you can do this however you want. If you want to use LangChain, [this how-to guide](https://python.langchain.com/v0.2/docs/how_to/structured_output/) is a starting point.
## Tool calling
It's extremely common to want agents to do tool calling. Tool calling refers to choosing from several available tools, and specifying which ones to call and what the inputs should be. This is extremely common in agents, as you often want to let the LLM decide which tools to call and then call those tools.
Since LangGraph nodes can be arbitrary Python functions, you can do this however you want. If you want to use LangChain, [this how-to guide](https://python.langchain.com/v0.2/docs/how_to/tool_calling/) is a starting point.
## Memory
Memory is a key concept to agentic applications. Memory is important because end users often expect the application they are interacting with remember previous interactions. The most simple example of this is chatbots - they clearly need to remember previous messages in a conversation.
LangGraph is perfectly suited to give you full control over the memory of your application. With user defined [`State`](./low_level.md#state) you can specify the exact schema of the memory you want to retain. With [checkpointers](./low_level.md#checkpointer) you can store checkpoints of previous interactions and resume from there in follow up interactions.
See [this guide](../how-tos/persistence.ipynb) for how to add memory to your graph.
## Human-in-the-loop
Agentic systems often require some human-in-the-loop (or "on-the-loop") interaction patterns. This is because agentic systems are still not super reliable, so having a human involved is required for any sensitive tasks/actions. These are all easily enabled in LangGraph, largely due to [checkpointers](./low_level.md#checkpointer). The reason a checkpointer is necessary is that a lot of these interaction patterns involve running a graph up until a certain point, waiting for some sort of human feedback, and then continuing. When you want to "continue" you will need to access the state of the graph previous to getting interrupted, and checkpointers are a built in, highly convenient way to do that.
There are a few common human-in-the-loop interaction patterns we see emerging.
### Approval
A basic one is to have the agent wait for approval before executing certain tools. This may be all tools, or just a subset of tools. This is generally recommend for more sensitive actions (like writing to a database). This can easily be done in LangGraph by setting a [breakpoint](./low_level.md#breakpoints) before specific nodes.
See [this guide](../how-tos/human_in_the_loop/breakpoints.ipynb) for how do this in LangGraph.
### Wait for input
A similar one is to have the agent wait for human input. This can be done by:
1. Create a node specifically for human input
2. Add a breakpoint before the node
3. Get user input
4. Update the state with that user input, acting as that node
5. Resume execution
See [this guide](../how-tos/human_in_the_loop/wait-user-input.ipynb) for how do this in LangGraph.
### Edit agent actions
This is a more advanced interaction pattern. In this interaction pattern the human can actually edit some of the agent's previous decisions. This can be done either during the flow (after a [breakpoint](./low_level.md#breakpoints), part of the [approval](#approval) flow) or after the fact (as part of [time-travel](#time-travel))
See [this guide](../how-tos/human_in_the_loop/edit-graph-state.ipynb) for how do this in LangGraph.
### Time travel
This is a pretty advanced interaction pattern. In this interaction pattern, the human can look back at the list of previous checkpoints, find one they like, optionally [edit it](#edit-agent-actions), and then resume execution from there.
See [this guide](../how-tos/human_in_the_loop/time-travel.ipynb) for how to do this in LangGraph.
## Map-Reduce
A common pattern in agents is to generate a list of objects, do some work on each of those objects, and then combine the results. This is very similar to the common [map-reduce](https://en.wikipedia.org/wiki/MapReduce) operation. This can be tricky for a few reasons. First, it can be tough to define a structured graph ahead of time because the length of the list of objects may be unknown. Second, in order to do this map-reduce you need multiple versions of the state to exist... but the graph shares a common shared state, so how can this be?
LangGraph supports this via the [Send](./low_level.md#send) api. This can be used to allow a conditional edge to Send multiple different states to multiple nodes. The state it sends can be different from the state of the core graph.
See a how-to guide for this [here](../how-tos/map-reduce.ipynb)
## Multi-agent
A term you may have heard is "multi-agent" architectures. What exactly does this mean?
Given that it is hard to even define an "agent", it's almost impossible to exactly define a "multi-agent" architecture. When most people talk about a multi-agent architecture, they typically mean a system where there are multiple different LLM-based systems. These LLM-based systems can be as simple as a prompt and an LLM call, or as complex as a [ReAct agent](#react-agent).
The big question in multi-agent systems is how they communicate. This involves both the schema of how they communicate, as well as the sequence in which they communicate. LangGraph is perfect for orchestrating these types of systems. It allows you to define multiple agents (each one is a node) an arbitrary state (to encapsulate the schema of how they communicate) as well as the edges (to control the sequence in which they communicate).
## Planning
One of the big things that agentic systems struggle with is long term planning. A common technique to overcome this is to have an explicit planning this. This generally involves calling an LLM to come up with a series of steps to execute. From there, the system then tries to execute the series of tasks (this could use a sub-agent to do so). Optionally, you can revisit the plan after each step and update it if needed.
## Reflection
Agents often struggle to produce reliable results. Therefore, it can be helpful to check whether the agent has completed a task correctly or not. If it has - then you can finish. If it hasn't - then you can take the feedback on why it's not correct and pass it back into another iteration of the agent.
This "reflection" step often uses an LLM, but doesn't have to. A good example of where using an LLM may not be necessary is in coding, when you can try to compile the generated code and use any errors as the feedback.
## ReAct Agent
One of the most common agent architectures is what is commonly called the ReAct agent architecture. In this architecture, an LLM is called repeatedly in a while-loop. At each step the agent decides which tools to call, and what the inputs to those tools should be. Those tools are then executed, and the outputs are fed back into the LLM as observations. The while-loop terminates when the agent decides it is not worth calling any more tools.
One of the few high level, pre-built agents we have in LangGraph - you can use it with [`create_react_agent`](../reference/prebuilt.md#create_react_agent)
This is named after and based on the [ReAct](https://arxiv.org/abs/2210.03629) paper. However, there are several differences between this paper and our implementation:
- First, we use [tool-calling](#tool-calling) to have LLMs call tools, whereas the paper used prompting + parsing of raw output. This is because tool calling did not exist when the paper was written, but is generally better and more reliable.
- Second, we use messages to prompt the LLM, whereas the paper used string formatting. This is because at the time of writing, LLMs didn't even expose a message-based interface, whereas now that's the only interface they expose.
- Third, the paper required all inputs to the tools to be a single string. This was largely due to LLMs not being super capable at the time, and only really being able to generate a single input. Our implementation allows for using tools that require multiple inputs.
- Forth, the paper only looks at calling a single tool at the time, largely due to limitations in LLMs performance at the time. Our implementation allows for calling multiple tools at a time.
- Finally, the paper asked the LLM to explicitly generate a "Thought" step before deciding which tools to call. This is the "Reasoning" part of "ReAct". Our implementation does not do this by default, largely because LLMs have gotten much better and that is not as necessary. Of course, if you wish to prompt it do so, you certainly can.
See [this guide](../how-tos/human_in_the_loop/time-travel.ipynb) for a full walkthrough of how to use the prebuilt ReAct agent.
+55
View File
@@ -0,0 +1,55 @@
# LangGraph for Agentic Applications
## What does it mean to be agentic?
Other people may talk about a system being an "agent" - we prefer to talk about systems being "agentic". But what does this actually mean?
When we talk about systems being "agentic", we are talking about systems that use an LLM to decide the control flow of an application. There are different levels that an LLM can be used to decide the control flow, and this spectrum of "agentic" makes more sense to us than defining an arbitrary cutoff for what is or isn't an agent.
Examples of using an LLM to decide the control of an application:
- Using an LLM to route between two potential paths
- Using an LLM to decide which of many tools to call
- Using an LLM to decide whether the generated answer is sufficient or more work is need
The more times these types of decisions are made inside an application, the more agentic it is.
If these decisions are being made in a loop, then its even more agentic!
There are other concepts often associated with being agentic, but we would argue these are a by-product of the above definition:
- [Tool calling](agentic_concepts.md#tool-calling): this is often how LLMs make decisions
- Action taking: often times, the LLMs' outputs are used as the input to an action
- [Memory](agentic_concepts.md#memory): reliable systems need to have knowledge of things that occurred
- [Planning](agentic_concepts.md#planning): planning steps (either explicit or implicit) are useful for ensuring that the LLM, when making decisions, makes them in the highest fidelity way.
## Why LangGraph?
LangGraph has several core principles that we believe make it the most suitable framework for building agentic applications:
- [Controllability](../how-tos/index.md#controllability)
- [Human-in-the-Loop](../how-tos/index.md#human-in-the-loop)
- [Streaming First](../how-tos/index.md#streaming)
**Controllability**
LangGraph is extremely low level. This gives you a high degree of control over what the system you are building actually does. We believe this is important because it is still hard to get agentic systems to work reliably, and we've seen that the more control you exercise over them, the more likely it is that they will "work".
**Human-in-the-Loop**
LangGraph comes with a built-in persistence layer as a first-class concept. This enables several different human-in-the-loop interaction patterns. We believe that "Human-Agent Interaction" patterns will be the new "Human-Computer Interaction", and have built LangGraph with built in persistence to enable this.
**Streaming First**
LangGraph comes with first class support for streaming. Agentic applications often take a while to run, and so giving the user some idea of what is happening is important, and streaming is a great way to do that. LangGraph supports streaming of both events ([like a tool call being taken](../how-tos/stream-updates.ipynb)) as well as of [tokens that an LLM may emit](../how-tos/streaming-tokens.ipynb).
## Deployment
So you've built your LangGraph object - now what?
Now you need to deploy it.
There are many ways to deploy LangGraph objects, and the right solution depends on your needs and use case.
We'll highlight two ways here: using [LangGraph Cloud](../cloud/index.md) or rolling your own solution.
[LangGraph Cloud](../cloud/index.md) is an opinionated way to deploy LangGraph objects from the LangChain team. Please see the [LangGraph Cloud documentation](../cloud/index.md) for all the details about what it involves, to see if it is a good fit for you.
If it is not a good fit, you may want to roll your own deployment. In this case, we would recommend using [FastAPI](https://fastapi.tiangolo.com/) to stand up a server. You can then call this graph from inside the FastAPI server as you see fit.
+55 -425
View File
@@ -1,427 +1,57 @@
# Conceptual Guides
Welcome to LangGraph, a Python library for building complex, scalable AI agents using graph-based state machines. In this guide, we'll explore the core concepts behind LangGraph and why it's uniquely suited for creating reliable, fault-tolerant agent systems. We assume you have already learned the basic covered in the [introduction tutorial](https://langchain-ai.github.io/langgraph/tutorials/introduction/#requirements) and want to deepen your understanding of LangGraph's underlying design and inner workings.
First off, why graphs?
## Background: Agents & AI Workflows as Graphs
While everyone has a slightly different definition of what constitutes an "AI Agent", we will take "agent" to mean any system that tasks a language model with controlling a looping workflow and takes actions. The prototypical LLM agent uses a ~["reasoning and action" (ReAct)](https://arxiv.org/abs/2210.03629)-style design, applying an LLM to power a basic loop with the following steps:
- reason and plan actions to take
- take actions using tools (regular software functions)
- observe the effects of the tools and re-plan or react as appropriate
While LLM agents are surprisingly effective at this, the naive agent loop doesn't deliver the [reliability users expect at scale](https://en.wikipedia.org/wiki/High_availability). They're beautifully stochastic. Well-designed systems take advantage of that randomness and apply it sensibly within a well-designed composite system and make that system **tolerant** to mistakes in the LLM's outputs, because mistakes **will** occur.
We think agents are exciting and new, but AI design patterns should apply applicable good engineering practices from Software 2.0. Some similarities include:
- AI applications must balance autonomous operations with user control.
- Agent applications resemble distributed systems in their need for error tolerance and correction.
- Multi-agent systems resemble multi-player web apps in their need for parallelism + conflict resolution.
- Everyone loves an undo button and version control.
LangGraph's primary [StateGraph](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.StateGraph) abstraction is designed to support these and other needs, providing an API that is lower level than other agent frameworks such as LangChain's [AgentExecutor](https://python.langchain.com/v0.1/docs/modules/agents/) to give you full control of where and how to apply "AI."
It extends Google's [Pregel](https://research.google/pubs/pregel-a-system-for-large-scale-graph-processing/) graph processing framework to provide fault tolerance and recovery when running long or error-prone workloads. When developing, you can focus on a local action or task-specific agent, and the system composes these actions to form a more capable and scalable application.
Its parallelism and `State` reduction functionality let you control what happens if, for example, multiple agents return conflicting information.
And finally, its persistent, versioned checkpointing system lets you roll back the agent's state, explore other paths, and maintain full control of what is going on.
The following sections go into greater detail about how and why all of this works.
## Core Design
At its core, LangGraph models agent workflows as state machines. You define the behavior of your agents using three key components:
1. `State`: A shared data structure that represents the current snapshot of your application. It can be any Python type, but is typically a `TypedDict` or Pydantic `BaseModel`.
2. `Nodes`: Python functions that encode the logic of your agents. They receive the current `State` as input, perform some computation or side-effect, and return an updated `State`.
3. `Edges`: Control flow rules that determine which `Node` to execute next based on the current `State`. They can be conditional branches or fixed transitions.
By composing `Nodes` and `Edges`, you can create complex, looping workflows that evolve the `State` over time. The real power, though, comes from how LangGraph manages that `State`.
Or in short: _nodes do the work. edges tell what to do next_.
LangGraph's underlying graph algorithm uses [message passing](https://en.wikipedia.org/wiki/Message_passing) to define a general program. When a `Node` completes, it sends a message along one or more edges to other node(s). These nodes run their functions, pass the resulting messages to the next set of nodes, and on and on it goes. Inspired by [Pregel](https://research.google/pubs/pregel-a-system-for-large-scale-graph-processing/), the program proceeds in discrete "super-steps" that are all executed conceptually in parallel. Whenever the graph is run, all the nodes start in an `inactive` state. Whenever an incoming edge (or "channel") receives a new message (state), the node becomes `active`, runs the function, and responds with updates. At the end of each superstep, each node votes to `halt` by marking itself as `inactive` if it has no more incoming messages. The graph terminates when all nodes are `inactive` and when no messages are in transit.
We will go through a full execution of a StateGraph later, but first, lets explore these concepts in more detail.
## Nodes
In StateGraph, nodes are typically python functions (sync or `async`) where the **first** positional argument is the [state](#state-management), and (optionally), the **second** positional argument is a "config", containing optional [configurable parameters](#configuration) (such as a `thread_id`).
Similar to `NetworkX`, you add these nodes to a graph using the [add_node](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.MessageGraph) method:
```python
from langchain_core.runnables import RunnableConfig
from langgraph.graph import END, START, StateGraph
builder = StateGraph(dict)
def my_node(state: dict, config: RunnableConfig):
print("In node: ", config["configurable"]["user_id"])
return {"results": f"Hello, {state['input']}!"}
# The second argument is optional
def my_other_node(state: dict):
return state
builder.add_node("my_node", my_node)
builder.add_node("other_node", my_other_node)
builder.add_edge(START, "my_node")
builder.add_edge("my_node", "other_node")
builder.add_edge("other_node", END)
graph = builder.compile()
graph.invoke({"input": "Will"}, {"configurable": {"user_id": "abcd-123"}})
# In node: abcd-123
# {'results': 'Hello, Will!'}
```
Behind the scenes, functions are converted to [RunnableLambda's](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.RunnableLambda.html#langchain_core.runnables.base.RunnableLambda), which add batch and async support to your function, along with native tracing and debugging.
## Edges
Edges define how the logic is routed and how the graph decides to stop. Similar to nodes, they accept the current `state` of the graph and return a value.
By default, the value is the name of the node or nodes to send the state to next. All those nodes will be run in parallel as a part of the next superstep.
If you want to reuse an edge, you can optionally provide a dictionary that maps the edge's output to the name of the next node.
If you **always** want to go from node A to node B, you can use the [add_edge](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.StateGraph.add_edge) method directly.
If you want to **optionally** route to 1 or more edges (or optionally terminate), you can use the [add_conditional_edges](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.StateGraph.add_conditional_edges) method.
If a node has multiple out-going edges, **all** of those destination nodes will be executed in parallel as a part of the next superstep.
## State Management
LangGraph introduces two key ideas to state management: state schemas and reducers.
The state schema defines the type of the object that is given to each of the graph's `Node`.
Reducers define how to apply `Node` outputs to the current `State`. For example, you might use a reducer to merge a new dialogue response into a conversation history, or average together outputs from multiple agent nodes. By annotating your `State` fields with reducer functions, you can precisely control how data flows through your application.
We'll illustrate how reducers work with an example. Compare the following two `State`. Can you guess the output in both case?
```python
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import END, START, StateGraph
class StateA(TypedDict):
value: int
builder = StateGraph(StateA)
builder.add_node("my_node", lambda state: {"value": 1})
builder.add_edge(START, "my_node")
builder.add_edge("my_node", END)
graph = builder.compile()
graph.invoke({"value": 5})
```
And `StateB`:
```python
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import END, START, StateGraph
def add(existing: int, new: int):
return existing + new
class StateB(TypedDict):
# highlight-next-line
value: Annotated[int, add]
builder = StateGraph(StateB)
builder.add_node("my_node", lambda state: {"value": 1})
builder.add_edge(START, "my_node")
builder.add_edge("my_node", END)
graph = builder.compile()
graph.invoke({"value": 5})
```
If you guesed "1" and "6", then you're correct!
In the first case (`StateA`), the result is "1", since the default **reducer** for your state is a direct overwrite.
In the second case (`StateB`), the result is "6" since we have have created the `add` function as the **reducer**. This function takes the existing state (for that field) and the state update (if provided) and returns the updated value for that state.
In general, **reducers** provided as annotations tell the graph **how to process updates for this field**.
While we typically use `TypedDict` as the graph's `state_schema` (i.e., `State`), it can be almost any [type](https://docs.python.org/3/library/stdtypes.html#type-objects), meaning the following graph is also completely valid:
```python
# Analogous to StateA above
builder = StateGraph(int)
builder.add_node("my_node", lambda state: 1)
builder.add_edge(START, "my_node")
builder.add_edge("my_node", END)
builder.compile().invoke(5)
# Analogous to StateB
def add(left, right):
return left + right
builder = StateGraph(Annotated[int, add])
builder.add_node("my_node", lambda state: 1)
builder.add_edge(START, "my_node")
builder.add_edge("my_node", END)
graph = builder.compile()
graph.invoke(5)
```
This also means you can [use a Pydantic BaseModel](https://langchain-ai.github.io/langgraph/how-tos/state-model/) as your graph state to add **default values** and additional data validation.
When building simple chatbots like ChatGPT, the state can be as simple as a list of chat messages. This is the state used by [MessageGraph](https://langchain-ai.github.io/langgraph/reference/graphs/?h=message+graph#langgraph.graph.MessageGraph) (a light wrapper of `StateGraph`), which is only slightly more involved than the following:
```python
builder = StateGraph(Annotated[list, add])
```
Using a shared state within a graph comes with some design tradeoffs. For instance, you may think it feels like using dreaded global variables (though this can be addressed by namespacing arguments). However, sharing a typed state provides a number of benefits relevant to building AI workflows, including:
1. The data flow is fully inspectable before and after each "superstep".
2. The state is mutable, making it easy to let users or other software write to the same state between supersteps to control an agent's direction (using [update_state](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.graph.CompiledGraph.update_state)).
3. It is well-defined when checkpointing, making it easy to save and resume or even fully version control the execution of your entire workflows in whatever storage backend you wish.
We will talk about checkpointing more in the next section.
## Persistence
Any "intelligent" system needs memory to function. AI agents are no different, requiring memory across one or more timeframes:
- they _always_ need to remember the steps already taken **within this task** (to avoid repeating itself when answering a given query).
- they _typically_ need to remember the previous turns within a multi-turn conversation with a user (for coreference resolution and additional context).
- they _ideally_ need to "remember" context from previous interactions with the user and from actions in a given "environment" (such as an application context) to be more personalized and efficient in its behavior.
That last form of memory covers a lot (personalization, optimization, continual learning, etc.) and is beyond the scope of this conversation, although it can be easily integrated in any LangGraph workflow, and we are actively exploring the best way to expose this functionality natively.
The first two forms of memory are natively supported by the [StateGraph](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.StateGraph) API via [checkpointers](https://langchain-ai.github.io/langgraph/reference/checkpoints/#basecheckpointsaver).
#### Checkpoints
A checkpoint represents the state of a `thread` within a (potentially) multi-turn interaction between your application and a user (or users or other systems). Checkpoints that are made _within_ a single run will have a set of `next` nodes that will be executed when starting from this state. Checkpoints that are made at the end of a given run are identical, except there are no `next` nodes to transition to (the graph is awaiting user input).
Checkpointing supports chat memory and much more, letting you tag and persist every state your system has taken, regardless of whether it is within a single run or across many turns. Let's explore a bit why that is useful.
#### Single-turn Memory
**Within** a given run, each step of the agent is checkpointed. This means you could ask your agent to go create world peace. In the likely scenario that it runs into an error as it fails to do so, you can resume its quest at any time by resuming from one of its saved checkpoints.
This also lets you build **human-in-the-loop** workflows, common in use cases like [customer support bots](https://langchain-ai.github.io/langgraph/tutorials/customer-support/customer-support/), [programming assistants](https://langchain-ai.github.io/langgraph/tutorials/usaco/usaco/), and other applications. Before or after executing a given node, you can `interrupt` the graph's execution and "escalate" control to a user or support person. That person may respond immediately. Or they could respond a month from now. Either way, your workflow can resume at any time as if no time had passed at all.
#### Multi-turn Memory
Checkpoints are saved under a "thread_id" to support multi-turn interactions between users and your system. To the developer, there is absolutely no difference in how you configure your graph to add multi-turn memory support, since the checkpointing works the same throughout.
If you have some portion of state that you want to retain across turns and some state that you want to treat as "ephemeral", you can always clear the relevant state in the graph's final node.
Using checkpointing is as easy as calling `compile(checkpointer=my_checkpointer)` and then invoking it with a `thread_id` within its `configurable` parameters. You can see more in the following sections!
## Threads
Threads in LangGraph represent separate **sessions** of a graph. They organize state checkpoints within discrete sessions to facilitate multi-conversation and multi-user support in an application.
A typical chat bot application would have multiple threads for each user. Each thread represents a single conversation, with its own persistent chat history and other state. Checkpoints within a thread can be rewound and branched as needed.
Threads in LangGraph are distinct from [operating system threads](https://docs.python.org/3/library/threading.html), which are units of execution managed by the OS. They are more akin to a [conversational thread](<https://en.wikipedia.org/wiki/Thread_(online_communication)>) in email, twitter, and other messaging apps.
When a `StateGraph` is compiled with a checkpointer, each invocation of the graph requires a `thread_id` to be provided via [configuration (see below)](#configuration).
## Configuration
For any given graph deployment, you'll likely want some amount of configurable values that you can control at runtime. These differ from the graph **inputs** in that they aren't meant to be treated as state variables. They are more akin to "[out-of-band](https://en.wikipedia.org/wiki/Out-of-band)" communication.
A common example is a conversational `thread_id`, a `user_id`, a choice of which LLM to use, how many documents to return in a retriever, etc. While you **could** pass this within the state, it is nicer to separate out from the regular data flow. Configurable values are also automatically added to LangSmith traces as [metadata](https://docs.smith.langchain.com/concepts/tracing#metadata).
#### Example
Let's review another example to see how our multi-turn memory works! Can you guess what `result` and `result2` look like if you run this graph?
```python
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, StateGraph
def add(left, right):
return left + right
class State(TypedDict):
total: Annotated[int, add]
turn: str
builder = StateGraph(State)
builder.add_node("add_one", lambda x: {"total": 1})
builder.add_edge(START, "add_one")
builder.add_edge("add_one", END)
memory = MemorySaver()
graph = builder.compile(checkpointer=memory)
thread_id = "some-thread"
config = {"configurable": {"thread_id": thread_id}}
result = graph.invoke({"total": 1, "turn": "First Turn"}, config)
result2 = graph.invoke({"turn": "Next Turn"}, config)
result3 = graph.invoke({"total": 5}, config)
result4 = graph.invoke({"total": 5}, {"configurable": {"thread_id": "new-thread-id"}})
```
If you guessed the following, you're correct!
```python
>>> result
{'total': 2, 'turn': 'First Turn'}
>>> result2
{'total': 3, 'turn': 'Next Turn'}
>>> result3
{'total': 9, 'turn': 'Next Turn'}
>>> result4
{'total': 6}
```
For the first run, no checkpoint existed, so the graph ran on the raw input. The "total" value is incremented from 1 to 2, and the "turn" is set to "First Turn".
For the second run, the user provides an update to "turn" but no total! Since we are loading from the state, the previous result is incremented by one (in our "add_one" node), and the "turn" is overwritten by the user.
For the third run, the "turn" remains the same, since it is loaded from the checkpoint but not overwritten by the user. The "total" is incremented by the value provided by the user, since this is **reduced** (i.e., used to update the existing value) by the `add` function.
For the fourth run, we are using a **new thread id** for which no checkpoint is found, so the result is just the user's provided **total** incremented by one.
You probably noticed that this user-facing behavior is equivalent to running the following **without a checkpointer**.
```python
graph = builder.compile()
result = graph.invoke({"total": 1, "turn": "First Turn"})
result2 = graph.invoke({**result, "turn": "Next Turn"})
result3 = graph.invoke({**result2, "total": result2["total"] + 5})
result4 = graph.invoke({"total": 5})
```
Run this for yourself to confirm equivalence. User inputs and checkpoint loading is treated more or less the same as any other **state update**.
Now that we've introduced the core concepts behind LangGraph, it may be instructive to walk through an end-to-end example to see how all the pieces fit together.
## Data flow of a single execution of a StateGraph
As engineers, we are never really satisfied until we know what's going on "under the hood". In the previous sections, we explained some of the LangGraph's core concepts. Now it's time to really show how they fit together.
Let's extend our toy example above with a conditional edge and then walk through two consecutive invocations.
```python
from typing import Annotated, Literal
from typing_extensions import TypedDict
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, StateGraph
def add(left, right):
return left + right
class State(TypedDict):
total: Annotated[int, add]
builder = StateGraph(State)
builder.add_node("add_one", lambda x: {"total": 1})
builder.add_node("double", lambda x: {"total": x["total"]})
builder.add_edge(START, "add_one")
def route(state: State) -> Literal["double", "__end__"]:
if state["total"] < 6:
return "double"
return "__end__" # This is what END is
builder.add_conditional_edges("add_one", route)
builder.add_edge("double", "add_one")
memory = MemorySaver()
graph = builder.compile(checkpointer=memory)
```
...
And then call it for the first time:
```python
thread_id = "some-thread"
config = {"configurable": {"thread_id": thread_id}}
for step in graph.stream({"total": 1}, config, stream_mode="debug"):
print(step["step"], step["type"], step["payload"].get("values"))
# 0 checkpoint {'total': 1}
# 1 task None
# 1 task_result None
# 1 checkpoint {'total': 2}
# 2 task None
# 2 task_result None
# 2 checkpoint {'total': 4}
# 3 task None
# 3 task_result None
# 3 checkpoint {'total': 5}
# 4 task None
# 4 task_result None
# 4 checkpoint {'total': 10}
# 5 task None
# 5 task_result None
# 5 checkpoint {'total': 11}
```
To inspect the trace of this run, check out the [LangSmith link here](https://smith.langchain.com/public/0c543370-d459-4b8d-9962-058f67bdc9ce/r). We'll walk through the execution below:
1. First, the graph looks for a checkpoint. None is found, so the state is thus initialized with a total of 0.
2. Next, the graph applies the user's input as an update to the state. The reducer adds the input (1) to the existing value (0). At the end of this superstep, the total is (1).
3. After that, the "add_one" node is called, returning 1.
4. Next, the reducer adds this update to the existing total (1). The state is now 2.
5. Then, the conditional edge "`route`" is called. Since the value is less than 6, we continue to the 'double' node.
6. Double takes the existing state (2), and returns it. The reducer is then called and adds it to the existing state. The state is now 4.
7. The graph then loops back through add_one (5), checks the conditional edge and proceeds to since it's < 6. After doubling, the total is (10).
8. The fixed edge loops back to add_one (11), checks the conditional edge, and since it is greater than 6, the program terminates.
For our second run, we will use the same configuration:
```python
for step in graph.stream(
{"total": -2, "turn": "First Turn"}, config, stream_mode="debug"
):
print(step["step"], step["type"], step["payload"].get("values"))
# 7 checkpoint {'total': 9}
# 8 task None
# 8 task_result None
# 8 checkpoint {'total': 10}
```
To inspect the trace of this run, check out the [LangSmith link here](https://smith.langchain.com/public/494f1817-46f5-4051-b41c-2dc416ce8b4d/r). We'll walk through the execution below:
1. First, it applies the update from the user's input. The `add` **reducer** updates the total from 0 to -2.
2. Next, the graph looks for the checkpoint. It loads it to memory as the initial state. Total is (9) now ((-2) + 11).
3. After that, the 'add_one' node is called with this state. It returns 10.
4. That update is applied using the reducer, raising the value to 10.
5. Next, the "route" conditional edge is triggered. Since the value is greater than 6, we terminate the program, ending where we started at (11).
In this guide we will explore the concepts behind build agentic and multi-agent systems with LangGraph. We assume you have already learned the basic covered in the [introduction tutorial](https://langchain-ai.github.io/langgraph/tutorials/introduction) and want to deepen your understanding of LangGraph's underlying design and inner workings.
There are three main parts to this concept guide. First, we'll discuss at a very high level what it means to be agentic. Next, we'll look at lower-level concepts in LangGraph that are core for understanding how to build your own agentic systems. Finally, we'll discuss common agentic patterns and how you can achieve those with LangGraph. These will be mostly conceptual guides - for more technical, hands-on guides see our [how-to guides](https://langchain-ai.github.io/langgraph/how-tos/)
LangGraph for Agentic Applications
- [What does it mean to be agentic?](high_level.md#what-does-it-mean-to-be-agentic)
- [Why LangGraph](high_level.md#why-langgraph)
- [Deployment](high_level.md#deployment)
Low Level Concepts
- [Graphs](low_level.md#graphs)
- [StateGraph](low_level.md#stategraph)
- [MessageGraph](low_level.md#messagegraph)
- [Compiling Your Graph](low_level.md#compiling-your-graph)
- [State](low_level.md#state)
- [Schema](low_level.md#schema)
- [Reducers](low_level.md#reducers)
- [MessageState](low_level.md#messagestate)
- [Nodes](low_level.md#nodes)
- [`START` node](low_level.md#start-node)
- [`END` node](low_level.md#end-node)
- [Edges](low_level.md#edges)
- [Normal Edges](low_level.md#normal-edges)
- [Conditional Edges](low_level.md#conditional-edges)
- [Entry Point](low_level.md#entry-point)
- [Conditional Entry Point](low_level.md#conditional-entry-point)
- [Send](low_level.md#send)
- [Checkpointer](low_level.md#checkpointer)
- [Threads](low_level.md#threads)
- [Checkpointer states](low_level.md#checkpointer-state)
- [Get state](low_level.md#get-state)
- [Get state history](low_level.md#get-state-history)
- [Update state](low_level.md#update-state)
- [Configuration](low_level.md#configuration)
- [Visualization](low_level.md#visualization)
- [Streaming](low_level.md#streaming)
Common Agentic Patterns
- [Structured output](agentic_concepts.md#structured-output)
- [Tool calling](agentic_concepts.md#tool-calling)
- [Memory](agentic_concepts.md#memory)
- [Human in the loop](agentic_concepts.md#human-in-the-loop)
- [Approval](agentic_concepts.md#approval)
- [Wait for input](agentic_concepts.md#wait-for-input)
- [Edit agent actions](agentic_concepts.md#edit-agent-actions)
- [Time travel](agentic_concepts.md#time-travel)
- [Map-Reduce](agentic_concepts.md#map-reduce)
- [Multi-agent](agentic_concepts.md#multi-agent)
- [Planning](agentic_concepts.md#planning)
- [Reflection](agentic_concepts.md#reflection)
- [Off-the-shelf ReAct Agent](agentic_concepts.md#react-agent)
+405
View File
@@ -0,0 +1,405 @@
# Low Level Conceptual Guide
## Graphs
At its core, LangGraph models agent workflows as graphs. You define the behavior of your agents using three key components:
1. [`State`](#state): A shared data structure that represents the current snapshot of your application. It can be any Python type, but is typically a `TypedDict` or Pydantic `BaseModel`.
2. [`Nodes`](#nodes): Python functions that encode the logic of your agents. They receive the current `State` as input, perform some computation or side-effect, and return an updated `State`.
3. [`Edges`](#edges): Python functions that determine which `Node` to execute next based on the current `State`. They can be conditional branches or fixed transitions.
By composing `Nodes` and `Edges`, you can create complex, looping workflows that evolve the `State` over time. The real power, though, comes from how LangGraph manages that `State`. To emphasize: `Nodes` and `Edges` are nothing more than Python functions - they can contain an LLM or just good ol' Python code.
In short: _nodes do the work. edges tell what to do next_.
LangGraph's underlying graph algorithm uses [message passing](https://en.wikipedia.org/wiki/Message_passing) to define a general program. When a `Node` completes, it sends a message along one or more edges to other node(s). These nodes run their functions, pass the resulting messages to the next set of nodes, and on and on it goes. Inspired by [Pregel](https://research.google/pubs/pregel-a-system-for-large-scale-graph-processing/), the program proceeds in discrete "super-steps" that are all executed conceptually in parallel. Whenever the graph is run, all the nodes start in an `inactive` state. Whenever an incoming edge (or "channel") receives a new message (state), the node becomes `active`, runs the function, and responds with updates. At the end of each superstep, each node votes to `halt` by marking itself as `inactive` if it has no more incoming messages. The graph terminates when all nodes are `inactive` and when no messages are in transit.
### StateGraph
The `StateGraph` class is the main graph class to uses. This is parameterized by a user defined `State` object.
### MessageGraph
The `MessageGraph` class is a special type of graph. The `State` of a `MessageGraph` is ONLY a list of messages. This class is rarely used except for chatbots, as most applications require the `State` to be more complex than a list of messages.
### Compiling your graph
To build your graph, you first define the [state](#state), you then add [nodes](#nodes) and [edges](#edges), and then you compile it. What exactly is compiling your graph and why is it needed?
Compiling is a pretty simple step. It provides a few basic checks on the structure of your graph (no orphaned nodes, etc). It is also where you can specify runtime args like [checkpointers](#checkpointer) and [breakpoints](#breakpoints). You compile your graph by just calling the `.compile` method:
```python
graph = graph_builder.compile(...)
```
You **MUST** compile your graph before you can use it.
## State
The first thing you do when you define a graph is define the `State` of the graph. The `State` consists of the [schema of the graph](#schema) as well as [`reducer` functions](#reducers) which specify how to apply updates to the state. The schema of the `State` will be the input schema to all `Nodes` and `Edges` in the graph, and can be either a `TypedDict` or a `Pydantic` model. All `Nodes` will emit updates to the `State` which are then applied using the specified `reducer` function.
### Schema
The main documented way to specify the schema of a graph is by using `TypedDict`. However, we also support [using a Pydantic BaseModel](../how-tos/state-model.ipynb) as your graph state to add **default values** and additional data validation.
### Reducers
Reducers are key to understanding how updates from nodes are applied to the `State`. Each key in the `State` has its own independent reducer function. If no reducer function is explicitly specified then it is assumed that all updates to that key should override it. Let's take a look at a few examples to understand them better.
**Example A:**
```python
from typing import TypedDict
class State(TypedDict):
foo: int
bar: list[str]
```
In this example, no reducer functions are specified for any key. Let's assume the input to the graph is `{"foo": 1, "bar": ["hi"]}`. Let's then assume the first `Node` returns `{"foo": 2}`. This is treated as an update to the state. Notice that the `Node` does not need to return the whole `State` schema - just an update. After applying this update, the `State` would then be `{"foo": 2, "bar": ["hi"]}`. If the second node returns `{"bar": ["bye"]}` then the `State` would then be `{"foo": 2, "bar": ["bye"]}`
**Example B:**
```python
from typing import TypedDict, Annotated
from operator import add
class State(TypedDict):
foo: int
bar: Annotated[list[str], add]
```
In this example, we've used the `Annotated` type to specify a reducer function (`operator.add`) for the second key (`bar`). Note that the first key remains unchanged. Let's assume the input to the graph is `{"foo": 1, "bar": ["hi"]}`. Let's then assume the first `Node` returns `{"foo": 2}`. This is treated as an update to the state. Notice that the `Node` does not need to return the whole `State` schema - just an update. After applying this update, the `State` would then be `{"foo": 2, "bar": ["hi"]}`. If the second node returns `{"bar": ["bye"]}` then the `State` would then be `{"foo": 2, "bar": ["hi", "bye"]}`. Notice here that the `bar` key is updated by adding the two lists together.
### MessageState
`MessageState` is one of the few opinionated components in LangGraph. `MessageState` is a special state designed to make it easy to use a list of messages as a key in your state. Specifically, `MessageState` is defined as:
```python
from langchain_core.messages import AnyMessage
from langgraph.graph.message import add_messages
from typing import Annotated, TypedDict
class MessagesState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
```
What this is doing is creating a `TypedDict` with a single key: `messages`. This is a list of `Message` objects, with `add_messages` as a reducer. `add_messages` basically adds messages to the existing list (it also does some nice extra things, like convert from OpenAI message format to the standard LangChain message format, handle updates based on message IDs, etc).
We often see a list of messages being a key component of state, so this prebuilt state is intended to make it easy to use messages. Typically, there is more state to track than just messages, so we see people subclass this state and add more fields, like:
```python
from langgraph.graph import MessagesState
class State(MessagesState):
documents: list[str]
```
## Nodes
In LangGraph, nodes are typically python functions (sync or `async`) where the **first** positional argument is the [state](#state), and (optionally), the **second** positional argument is a "config", containing optional [configurable parameters](#configuration) (such as a `thread_id`).
Similar to `NetworkX`, you add these nodes to a graph using the [add_node][langgraph.graph.StateGraph.add_node] method:
```python
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph
builder = StateGraph(dict)
def my_node(state: dict, config: RunnableConfig):
print("In node: ", config["configurable"]["user_id"])
return {"results": f"Hello, {state['input']}!"}
# The second argument is optional
def my_other_node(state: dict):
return state
builder.add_node("my_node", my_node)
builder.add_node("other_node", my_other_node)
...
```
Behind the scenes, functions are converted to [RunnableLambda's](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.RunnableLambda.html#langchain_core.runnables.base.RunnableLambda), which add batch and async support to your function, along with native tracing and debugging.
If you add a node to graph without specifying a name, it will be given a default name equivalent to the function name.
```python
builder.add_node(my_node)
# You can then create edges to/from this node by referencing it as `"my_node"`
```
### `START` Node
The `START` Node is a special node that represents the node sends user input to the graph. The main purpose for referencing this node is to determine which nodes should be called first.
```python
from langgraph.graph import START
graph.add_edge(START, "node_a")
```
### `END` Node
The `END` Node is a special node that represents a terminal node. This node is referenced when you want to denote which edges have no actions after they are done.
```
from langgraph.graph import END
graph.add_edge("node_a", END)
```
## Edges
Edges define how the logic is routed and how the graph decides to stop. This is a big part of how your agents work and how different nodes communicate with each other. There are a few key types of edges:
- Normal Edges: Go directly from one node to the next.
- Conditional Edges: Call a function to determine which node(s) to go to next.
- Entry Point: Which node to call first when user input arrives.
- Conditional Entry Point: Call a function to determine which node(s) to call first when user input arrives.
A node can have MULTIPLE outgoing edges. If a node has multiple out-going edges, **all** of those destination nodes will be executed in parallel as a part of the next superstep.
### Normal Edges
If you **always** want to go from node A to node B, you can use the [add_edge][langgraph.graph.StateGraph.add_edge] method directly.
```python
graph.add_edge("node_a", "node_b")
```
### Conditional Edges
If you want to **optionally** route to 1 or more edges (or optionally terminate), you can use the [add_conditional_edges][langgraph.graph.StateGraph.add_conditional_edges] method. This method accepts the name of a node and a "routing function" to call after that node is executed:
```python
graph.add_edge("node_a", routing_function)
```
Similar to nodes, the `routing_function` accept the current `state` of the graph and return a value.
By default, the return value `routing_function` is used as the name of the node (or a list of nodes) to send the state to next. All those nodes will be run in parallel as a part of the next superstep.
You can optionally provide a dictionary that maps the `routing_function`'s output to the name of the next node.
```python
graph.add_edge("node_a", routing_function, {True: "node_b", False: "node_c"})
```
### Entry Point
The entry point is first node to call when the graph starts. You can use [`set_entry_point`][langgraph.graph.StateGraph.set_entry_point] to specify this.
```python
graph.set_entry_point("node_a")
```
This is equivalent to adding an edge between the `START` node and this node. You may want to use `START` directly when you want to have **multiple** nodes be called first.
```python
from langgraph.graph import START
graph.add_edge(START, "node_a")
```
### Conditional Entry Point
The conditional entry point is used when you want to specify a function to call to determine which node(s) should be called first.
You can use [`set_conditional_entry_point`][langgraph.graph.StateGraph.set_conditional_entry_point] to specify this.
```python
graph.set_conditional_entry_point(routing_function)
```
You can optionally provide a dictionary that maps the `routing_function`'s output to the name of the next node.
```python
graph.set_conditional_entry_point(routing_function, {True: "node_b", False: "node_c"})
```
## `Send`
By default, `Nodes` and `Edges` are defined ahead of time and operate on the same shared state. However, there can be cases where the exact edges are not known ahead of time and/or you may want different versions of `State` to exist at the same time. A common of example of this is with `map-reduce` design patterns. In this design pattern, a first node may generate a list of objects, and you may want to apply some other node to all those objects. The number of objects may be unknown ahead of time (meaning the number of edges may not be known) and the input `State` to the downstream `Node` should be different (one for each generated object).
To support this design pattern, LangGraph supports returning [`Send`](../reference/graphs.md#send) objects from conditional edges. `Send` takes two arguments: first is the name of the node, and second is the state to pass to that node.
```python
def continue_to_jokes(state: OverallState):
return [Send("generate_joke", {"subject": s}) for s in state['subjects']]
graph.add_conditional_edges("node_a", continue_to_jokes)
```
## Checkpointer
One of the main benefits of LangGraph is that it comes backed by a persistence layer. This is accomplished via [checkpointers][basecheckpointsaver].
Checkpointers can be used to save a _checkpoint_ of the state of a graph after all steps of the graph. This allows for several things.
First, it allows for [human-in-the-loop workflows](agentic_concepts.md#human-in-the-loop), as it allows humans to inspect, interrupt, and approve steps. Checkpointers are needed for these workflows as the human has to be able to view the state of a graph at any point in time, and the graph has to be to resume execution after the human has made any updates to the state.
Second, it allows for ["memory"](agentic_concepts.md#memory) between interactions. You can use checkpointers to create threads and save the state of a thread after a graph executes. In the case of repeated human interactions (like conversations) any follow up messages can be sent to that checkpoint, which will retain its memory of previous ones.
See [this guide](../how-tos/persistence.ipynb) for how to add a checkpointer to your graph.
## Threads
When using a checkpointer, you must specify a `thread_id` or `thread_ts` when running the graph.
Threads are used to checkpoint multiple different runs. This can be used to enable a multi-tenant chat applications.
`thread_id` is simply the ID of a thread. This is always required
`thread_ts` can optionally be passed. This identifier refers to a specific checkpoint within a thread. This can be used to kick of a run of a graph from some point halfway through a thread.
You must pass these when invoking the graph as part of the configurable part of the config.
```python
config = {"configurable": {"thread_id": "a"}}
graph.invoke(inputs, config=config)
```
See [this guide](../how-tos/persistence.ipynb) for how to use threads.
## Checkpointer state
When you use a checkpointer with a graph, you can interact with the state of that graph.
This usually done when enabling different human-in-the-loop interaction patterns.
Each time you run the graph, the checkpointer creates several checkpoints every time a
node or set of nodes finishes running.
The most recent checkpoint is the current state of the thread.
When interacting with the checkpointer state, you must specify a [thread identifier](#threads).
Each checkpoint has two properties:
- **values**: This is the value of the state at this point in time.
- **next**: This is a tuple of the nodes to execute next in the graph.
### Get state
You can get the state of a checkpointer by calling `graph.get_state(config)`. The config should contain `thread_id`, and the state will be fetched for that thread.
### Get state history
You can also call `graph.get_state_history(config)` to get a list of the history of the graph. The config should contain `thread_id`, and the state history will be fetched for that thread.
### Update state
You can also interact with the state directly and update it. This takes three different components:
- config
- values
- `as_node`
**config**
The config should contain `thread_id` specifying which thread to update.
**values**
These are the values that will be used to update the state. Note that this update is treated exactly as any update from a node is treated. This means that these values will be passed to the [reducer](#reducers) functions that are part of the state. So this does NOT automatically overwrite the state. Let's walk through an example.
Let's assume you have defined the state of your graph as:
```python
from typing import TypedDict, Annotated
from operator import add
class State(TypedDict):
foo: int
bar: Annotated[list[str], add]
```
Let's now assume the current state of the graph is
```
{"foo": 1, "bar": ["a"]}
```
If you update the state as below:
```
graph.update_state(config, {"foo": 2, "bar": ["b"]})
```
Then the new state of the graph will be:
```
{"foo": 2, "bar": ["a", "b"]}
```
The `foo` key is completely changed (because there is no reducer specified for that key, so it overwrites it). However, there is a reducer specified for the `bar` key, and so it appends `"b"` to the state of `bar`.
**`as_node`**
The final thing you specify when calling `update_state` is `as_node`. This update will be applied as if it came from node `as_node`. If `as_node` is not provided, it will be set to the last node that updated the state, if not ambiguous.
The reason this matters is that the next steps in the graph to execute depend on the last node to have given an update, so this can be used to control which node executes next.
## Configuration
When creating a graph, you can also mark that certain parts of the graph are configurable. This is commonly done to enable easily switching between models or system prompts. This allows you to create a single "cognitive architecture" (the graph) but have multiple different instance of it.
You can optionally specify a `config_schema` when creating a graph.
```python
class ConfigSchema(TypedDict):
llm: str
graph = StateGraph(State, config_schema=ConfigSchema)
```
You can then pass this configuration into the graph using the `configurable` config field.
```python
config = {"configurable": {"llm": "anthropic"}}
graph.invoke(inputs, config=config)
```
You can then access and use this configuration inside a node:
```python
def node_a(state, config):
llm_type = config.get("configurable", {}).get("llm", "openai")
llm = get_llm(llm_type)
...
```
See [this guide](../how-tos/configuration.ipynb) for a full breakdown on configuration
## Breakpoints
It can often be useful to set breakpoints before or after certain nodes execute. This can be used to wait for human approval before continuing. These can be set when you ["compile" a graph](#compiling-your-graph). You can set breakpoints either _before_ a node executes (using `interrupt_before`) or after a node executes (using `interrupt_after`.)
You **MUST** use a [checkpoiner](#checkpointer) when using breakpoints. This is because your graph needs to be able to resume execution.
In order to resume execution, you can just invoke your graph with `None` as the input.
```python
# Initial run of graph
graph.invoke(inputs, config=config)
# Let's assume it hit a breakpoint somewhere, you can then resume by passing in None
graph.invoke(None, config=config)
```
See [this guide](../how-tos/human_in_the_loop/breakpoints.ipynb) for a full walkthrough of how to add breakpoints.
## Visualization
It's often nice to be able to visualize graphs, especially as they get more complex. LangGraph comes with several built-in ways to visualize graphs. See [this how-to guide](../how-tos/visualization.ipynb) for more info.
## Streaming
LangGraph is built with first class support for streaming. There are several different streaming modes that LangGraph supports:
- [`"values"`](../how-tos/stream-values.ipynb): This streams the full value of the state after each step of the graph.
- [`"updates`](../how-tos/stream-updates.ipynb): This streams the updates to the state after each step of the graph. If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are streamed separately.
- `"debug"`: This streams as much information as possible throughout the execution of the graph.
In addition, you can use the [`astream_events`](../how-tos/streaming-events-from-within-tools.ipynb) method to stream back events that happen _inside_ nodes. This is useful for [streaming tokens of LLM calls](../how-tos/streaming-tokens.ipynb).
+48 -23
View File
@@ -1,37 +1,62 @@
---
hide:
- toc
---
# How-to guides
Welcome to the LangGraph how-to guides! These guides provide practical, step-by-step instructions for accomplishing key tasks in LangGraph.
## Core
## Controllability
The core guides show how to address common needs when building out AI workflows, with special focus placed on [ReAct](https://arxiv.org/abs/2210.03629)-style agents with [tool calling](https://python.langchain.com/docs/modules/model_io/chat/function_calling/) (agents that <strong>Re</strong>ason and **Act** to accomplish tasks).
- [How to create a ReAct agent](create-react-agent.ipynb)
- [How to add persistence ("memory") to your graph](persistence.ipynb)
- [How to view and update graph state](time-travel.ipynb)
- [How to run graph asynchronously](async.ipynb)
- [How to stream graph responses](streaming-tokens.ipynb)
- [How to visualize your graph](visualization.ipynb)
- [How to add runtime configuration to your graph](configuration.ipynb)
### Design patterns
Recipes showing how to apply common design patterns in your workflows:
LangGraph is known for being a highly controllable agent framework.
These how-to guides show how to achieve that controllability.
- [How to create subgraphs](subgraph.ipynb)
- [How to create branches for parallel execution](branching.ipynb)
- [How to create map-reduce branches for parallel execution](map-reduce.ipynb)
- [How to add human-in-the-loop](human-in-the-loop.ipynb)
The following examples are useful especially if you are used to LangChain's `AgentExecutor` configurations.
- [How to force an agent to call a tool](force-calling-a-tool-first.ipynb)
- [How to pass runtime values to tools](pass-run-time-values-to-tools.ipynb)
- [How to let agent return tool results directly](dynamically-returning-directly.ipynb)
- [How to have agent respond in structured format](respond-in-format.ipynb)
- [How to manage agent steps](managing-agent-steps.ipynb)
## Persistence
### Advanced
LangGraph makes it easy to persist state across graph runs. The guide below shows how to add persistence to your graph.
- [How to add persistence ("memory") to your graph](persistence.ipynb)
## Human in the Loop
One of LangGraph's main benefits is that it makes human-in-the-loop workflows easy.
These guides cover common examples of that.
- [How to add breakpoints](human_in_the_loop/breakpoints.ipynb)
- [How to edit graph state](human_in_the_loop/edit-graph-state.ipynb)
- [How to wait for user input](human_in_the_loop/wait-user-input.ipynb)
- [How to view and update past graph state](human_in_the_loop/time-travel.ipynb)
## Streaming
LangGraph is built to be streaming first.
These guides show how to use different streaming modes.
- [How to stream full state of your graph](stream-values.ipynb)
- [How to stream state updates of your graph](stream-updates.ipynb)
- [How to stream LLM tokens](streaming-tokens.ipynb)
- [How to stream arbitrarily nested content](streaming-content.ipynb)
- [How to configure multiple streaming modes at the same time](stream-multiple.ipynb)
- [How to stream events from within a tool](streaming-events-from-within-tools.ipynb)
## Other
- [How to run graph asynchronously](async.ipynb)
- [How to visualize your graph](visualization.ipynb)
- [How to add runtime configuration to your graph](configuration.ipynb)
- [How to use a Pydantic model as your state](state-model.ipynb)
- [How to extract structured output with re-prompting](./extraction/retries.ipynb)
## Prebuilt ReAct Agent
These guides show how to use the prebuilt ReAct agent.
Please note that here will we use a **prebuilt agent**. One of the big benefits of LangGraph is that you can easily create your own agent architectures. So while it's fine to start here to build an agent quickly, we would strongly recommend learning how to build your own agent so that you can take full advantage of LangGraph.
- [How to create a ReAct agent](create-react-agent.ipynb)
- [How to add memory to a ReAct agent](create-react-agent-memory.ipynb)
- [How to add a custom system prompt to a ReAct agent](create-react-agent-system-prompt.ipynb)
- [How to add human-in-the-loop processes to a ReAct agent](create-react-agent-hitl.ipynb)
+3 -127
View File
@@ -1,131 +1,7 @@
---
hide_comments: true
hide:
- navigation
---
# 🦜🕸️LangGraph
![Version](https://img.shields.io/pypi/v/langgraph)
[![Downloads](https://static.pepy.tech/badge/langgraph/month)](https://pepy.tech/project/langgraph)
[![Open Issues](https://img.shields.io/github/issues-raw/langchain-ai/langgraph)](https://github.com/langchain-ai/langgraph/issues)
[![](https://dcbadge.vercel.app/api/server/6adMQxSpJS?compact=true&style=flat)](https://discord.com/channels/1038097195422978059/1170024642245832774)
⚡ Build language agents as graphs ⚡
!!! note "Python version :material-language-python:"
Looking for the JS version? Click [:fontawesome-brands-square-js: here](https://github.com/langchain-ai/langgraphjs) ([:simple-readme: JS docs](https://langchain-ai.github.io/langgraphjs/)).
## Overview
Suppose you're building a customer support assistant. You want your assistant to be able to:
1. Use tools to respond to questions
2. Connect with a human if needed
3. Be able to pause the process indefinitely and resume whenever the human responds
LangGraph makes this all easy. First install:
```bash
pip install -U langgraph
```
Then define your assistant:
```python
import json
from langchain_anthropic import ChatAnthropic
from langchain_community.tools.tavily_search import TavilySearchResults
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import END, MessageGraph
from langgraph.prebuilt.tool_node import ToolNode
# 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 not last_message.tool_calls:
return END
else:
return "action"
# Define a new graph
workflow = MessageGraph()
tools = [TavilySearchResults(max_results=1)]
model = ChatAnthropic(model="claude-3-haiku-20240307").bind_tools(tools)
workflow.add_node("agent", model)
workflow.add_node("action", ToolNode(tools))
workflow.set_entry_point("agent")
# Conditional agent -> action OR agent -> END
workflow.add_conditional_edges(
"agent",
should_continue,
)
# Always transition `action` -> `agent`
workflow.add_edge("action", "agent")
memory = SqliteSaver.from_conn_string(":memory:") # Here we only save in-memory
# Setting the interrupt means that any time an action is called, the machine will stop
app = workflow.compile(checkpointer=memory, interrupt_before=["action"])
```
Now, run the graph:
```python
# Run the graph
thread = {"configurable": {"thread_id": "4"}}
for event in app.stream("what is the weather in sf currently", thread, stream_mode="values"):
event[-1].pretty_print()
```
We configured the graph to **wait** before executing the `action`. The `SqliteSaver` persists the state. Resume at any time.
```python
for event in app.stream(None, thread, stream_mode="values"):
event[-1].pretty_print()
```
The graph orchestrates everything:
- The `MessageGraph` contains the agent's "Memory"
- Conditional edges enable dynamic routing between the chatbot, tools, and the user
- Persistence makes it easy to stop, resume, and even rewind for full control over your application
With LangGraph, you can build complex, stateful agents without getting bogged down in manual state and interrupt management. Just define your nodes, edges, and state schema - and let the graph take care of the rest.
## Tutorials
Consult the [Tutorials](tutorials/index.md) to learn more about building with LangGraph, including advanced use cases.
## How-To Guides
Check out the [How-To Guides](how-tos/index.md) for instructions on handling common tasks with LangGraph
## Reference
For documentation on the core APIs, check out the [Reference](reference/graphs.md) docs.
## Conceptual Guides
Once you've learned the basics, if you want to further understand LangGraph's core abstractions, check out the [Conceptual Guides](./concepts/index.md).
## Why LangGraph?
LangGraph is framework agnostic (each node is a regular python function). It extends the core Runnable API (shared interface for streaming, async, and batch calls) to make it easy to:
- Seamless state management across multiple turns of conversation or tool usage
- The ability to flexibly route between nodes based on dynamic criteria
- Smooth switching between LLMs and human intervention
- Persistence for long-running, multi-session applications
If you're building a straightforward DAG, Runnables are a great fit. But for more complex, stateful applications with nonlinear flows, LangGraph is the perfect tool for the job.
{!README.md!}
+6 -5
View File
@@ -13,17 +13,19 @@ graph = StateGraph(MyState)
```
::: langgraph.graph.StateGraph
handler: python
handler: python
## MessageGraph
::: langgraph.graph.message.MessageGraph
## CompiledGraph
::: langgraph.graph.graph.CompiledGraph
handler: python
## StreamMode
::: langgraph.pregel.StreamMode
## Constants
@@ -63,5 +65,4 @@ builder.add_conditional_edges("my_node", my_condition)
## Send
::: langgraph.constants.Send
handler: python
::: langgraph.constants.Send
+8 -1
View File
@@ -1,3 +1,8 @@
---
hide:
- toc
---
# Tutorials
Welcome to the LangGraph Tutorials! These notebooks introduce LangGraph through building various language agents and applications.
@@ -58,4 +63,6 @@ Learn from example implementations of graphs designed for specific scenarios and
- [Web Research (STORM)](storm/storm.ipynb): Generate Wikipedia-like articles via research and multi-perspective QA
- [TNT-LLM](tnt-llm/tnt-llm.ipynb): Build rich, interpretable taxonomies of user intentand using the classification system developed by Microsoft for their Bing Copilot application.
- [Web Navigation](web-navigation/web_voyager.ipynb): Build an agent that can navigate and interact with websites
- [Competitive Programming](usaco/usaco.ipynb): Build an agent with few-shot "episodic memory" and human-in-the-loop collaboration to solve problems from the USA Computing Olympiad; adapted from the ["Can Language Models Solve Olympiad Programming?"](https://arxiv.org/abs/2404.10952v1) paper by Shi, Tang, Narasimhan, and Yao.
- [Competitive Programming](usaco/usaco.ipynb): Build an agent with few-shot "episodic memory" and human-in-the-loop collaboration to solve problems from the USA Computing Olympiad; adapted from the ["Can Language Models Solve Olympiad Programming?"](https://arxiv.org/abs/2404.10952v1) paper by Shi, Tang, Narasimhan, and Yao.
- [Complex data extraction](extraction/retries.ipynb): Build an agent that can use function calling to do complex extraction tasks
-
+121 -100
View File
@@ -34,18 +34,18 @@ theme:
- search.suggest
- toc.follow
palette:
- scheme: default
primary: white
accent: gray
toggle:
icon: material/brightness-7
name: Switch to dark mode
- scheme: slate
primary: grey
accent: white
toggle:
icon: material/brightness-4
name: Switch to light mode
- scheme: default
primary: white
accent: gray
toggle:
icon: material/brightness-7
name: Switch to dark mode
- scheme: slate
primary: grey
accent: white
toggle:
icon: material/brightness-4
name: Switch to light mode
font:
text: "Public Sans"
code: "Roboto Mono"
@@ -60,7 +60,7 @@ plugins:
- https://docs.python.org/3/objects.inv
- https://api.python.langchain.com/en/latest/objects.inv
options:
members_order: source
members_order: source
allow_inspection: true
heading_level: 3
show_bases: true
@@ -68,9 +68,9 @@ plugins:
inherited_members: true
# merge_init_into_class: true
selection:
docstring_style: google
docstring_style: google
docstring_section_style: list
show_root_toc_entry: false
show_root_toc_entry: false
# show_signature_annotations: true
# show_symbol_type_heading: true
show_symbol_type_toc: true
@@ -80,21 +80,11 @@ plugins:
execute: false
include_source: True
include_requirejs: true
- git-committers:
repository: langchain-ai/langgraph
branch: main
docs_path: docs/docs/
token: !ENV ["MKDOCS_GIT_COMMITTERS_APIKEY"]
# TODO: Add minify plugin once it works alright with code block copying
# - minify:
# minify_html: true
nav:
- Home:
- 'index.md'
- Quick Start: how-tos/docs/quickstart.ipynb
- Intro to LangGraph: tutorials/introduction.ipynb
- Home:
- "index.md"
- Tutorials:
- 'tutorials/index.md'
- "tutorials/index.md"
- Introduction: tutorials/introduction.ipynb
- Chatbots:
- Customer Support: tutorials/customer-support/customer-support.ipynb
@@ -110,92 +100,115 @@ nav:
- Self-RAG using local LLMs: tutorials/rag/langgraph_self_rag_local.ipynb
- SQL Agent: tutorials/sql-agent.ipynb
- Agent Architectures:
- Multi-Agent Systems:
- Collaboration: tutorials/multi_agent/multi-agent-collaboration.ipynb
- Supervision: tutorials/multi_agent/agent_supervisor.ipynb
- Hierarchical Teams: tutorials/multi_agent/hierarchical_agent_teams.ipynb
- Planning Agents:
- Plan-and-Execute: tutorials/plan-and-execute/plan-and-execute.ipynb
- Reasoning without Observation: tutorials/rewoo/rewoo.ipynb
- LLMCompiler: tutorials/llm-compiler/LLMCompiler.ipynb
- Reflection & Critique:
- Basic Reflection: tutorials/reflection/reflection.ipynb
- Reflexion: tutorials/reflexion/reflexion.ipynb
- Language Agent Tree Search: tutorials/lats/lats.ipynb
- Self-Discover Agent: tutorials/self-discover/self-discover.ipynb
- Multi-Agent Systems:
- Collaboration: tutorials/multi_agent/multi-agent-collaboration.ipynb
- Supervision: tutorials/multi_agent/agent_supervisor.ipynb
- Hierarchical Teams: tutorials/multi_agent/hierarchical_agent_teams.ipynb
- Planning Agents:
- Plan-and-Execute: tutorials/plan-and-execute/plan-and-execute.ipynb
- Reasoning without Observation: tutorials/rewoo/rewoo.ipynb
- LLMCompiler: tutorials/llm-compiler/LLMCompiler.ipynb
- Reflection & Critique:
- Basic Reflection: tutorials/reflection/reflection.ipynb
- Reflexion: tutorials/reflexion/reflexion.ipynb
- Language Agent Tree Search: tutorials/lats/lats.ipynb
- Self-Discover Agent: tutorials/self-discover/self-discover.ipynb
- Evaluation & Analysis:
- Chatbot Evaluation via Simulation:
- Agent-based: tutorials/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb
- In LangSmith: tutorials/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb
- Experimental:
- Web Research (STORM): tutorials/storm/storm.ipynb
- TNT-LLM: tutorials/tnt-llm/tnt-llm.ipynb
- Web Navigation: tutorials/web-navigation/web_voyager.ipynb
- Competitive Programming: tutorials/usaco/usaco.ipynb
- Web Research (STORM): tutorials/storm/storm.ipynb
- TNT-LLM: tutorials/tnt-llm/tnt-llm.ipynb
- Web Navigation: tutorials/web-navigation/web_voyager.ipynb
- Competitive Programming: tutorials/usaco/usaco.ipynb
- Extract structured output: tutorials/extraction/retries.ipynb
- "How-to Guides":
- 'how-tos/index.md'
- Core:
- Create a ReAct agent: how-tos/create-react-agent.ipynb
- Add persistence ("memory"): how-tos/persistence.ipynb
- View and update graph state: how-tos/time-travel.ipynb
- Run graph asynchronously: how-tos/async.ipynb
- Stream graph responses: how-tos/streaming-tokens.ipynb
- Visualize your graph: how-tos/visualization.ipynb
- Add runtime configuration: how-tos/configuration.ipynb
- Design Patterns:
- Create subgraphs: how-tos/subgraph.ipynb
- Create branches for parallel execution: how-tos/branching.ipynb
- Create map-reduce branches for parallel execution: how-tos/map-reduce.ipynb
- Add human-in-the-loop: how-tos/human-in-the-loop.ipynb
- Force an agent to call a tool: how-tos/force-calling-a-tool-first.ipynb
- Pass runtime values to tools: how-tos/pass-run-time-values-to-tools.ipynb
- Let agent return tool results directly: how-tos/dynamically-returning-directly.ipynb
- Have agent respond in structured format: how-tos/respond-in-format.ipynb
- Manage agent steps: how-tos/managing-agent-steps.ipynb
- Advanced:
- Use Pydantic model as state: how-tos/state-model.ipynb
- Extract structured output with re-prompting: how-tos/extraction/retries.ipynb
- 'Conceptual Guides':
- 'concepts/index.md'
- "how-tos/index.md"
- Controllability:
- Create subgraphs: how-tos/subgraph.ipynb
- Create branches for parallel execution: how-tos/branching.ipynb
- Create map-reduce branches for parallel execution: how-tos/map-reduce.ipynb
- Persistence:
- Add persistence ("memory"): how-tos/persistence.ipynb
- Human-in-the-loop:
- Add breakpoints: how-tos/human_in_the_loop/breakpoints.ipynb
- Wait for user input: how-tos/human_in_the_loop/wait-user-input.ipynb
- View and update past graph state: how-tos/human_in_the_loop/time-travel.ipynb
- Edit graph state: how-tos/human_in_the_loop/edit-graph-state.ipynb
- Streaming:
- Stream full state: how-tos/stream-values.ipynb
- Stream state updates: how-tos/stream-updates.ipynb
- Stream LLM tokens: how-tos/streaming-tokens.ipynb
- Stream arbitrarily nested content: how-tos/streaming-content.ipynb
- Configure multiple streaming modes: how-tos/stream-multiple.ipynb
- Stream events from within tools: how-tos/streaming-events-from-within-tools.ipynb
- Other:
- Run graph asynchronously: how-tos/async.ipynb
- Visualize your graph: how-tos/visualization.ipynb
- Add runtime configuration: how-tos/configuration.ipynb
- Use Pydantic model as state: how-tos/state-model.ipynb
- Prebuilt ReAct Agent:
- Create a ReAct agent: how-tos/create-react-agent.ipynb
- Add memory to a ReAct agent: how-tos/create-react-agent-memory.ipynb
- Add a system prompt to a ReAct agent: how-tos/create-react-agent-system-prompt.ipynb
- Add human-in-the-Loop to a ReAct agent: how-tos/create-react-agent-hitl.ipynb
- "Conceptual Guides":
- "concepts/index.md"
- LangGraph for Agentic Applications: concepts/high_level.md
- Low Level LangGraph Concepts: concepts/low_level.md
- Common Agentic Patterns: concepts/agentic_concepts.md
- Reference:
- Graphs: reference/graphs.md
- Checkpointing: reference/checkpoints.md
- Prebuilt Components: reference/prebuilt.md
- Errors: reference/errors.md
- 'Cloud (alpha)':
- 'cloud/index.md'
- "Cloud (alpha)":
- "cloud/index.md"
- Tutorials:
- Quick Start: 'cloud/quick_start.md'
- SDK:
- Python: 'cloud/sdk/python_sdk.ipynb'
- JS/TS: 'cloud/sdk/js_sdk.ipynb'
- Deployment:
- Self-Hosted: 'cloud/deployment/self_hosted.md'
- Managed: 'cloud/deployment/managed.md'
- Quick Start: "cloud/quick_start.md"
- How-to Guides:
- Deployment:
- Setup App: "cloud/deployment/setup.md"
- Deploy to Cloud: "cloud/deployment/cloud.md"
- Test with Studio: "cloud/deployment/studio.md"
- Self-Host: "cloud/deployment/self_hosted.md"
- Streaming:
- Stream Messages: 'cloud/how-tos/cloud_examples/stream_messages.ipynb'
- Stream Values: 'cloud/how-tos/cloud_examples/stream_values.ipynb'
- Stream Updates: 'cloud/how-tos/cloud_examples/stream_updates.ipynb'
- Double Texting:
- Interrupt: 'cloud/how-tos/cloud_examples/interrupt_concurrent.ipynb'
- Rollback: 'cloud/how-tos/cloud_examples/rollback_concurrent.ipynb'
- Reject: 'cloud/how-tos/cloud_examples/reject_concurrent.ipynb'
- Enqueue: 'cloud/how-tos/cloud_examples/enqueue_concurrent.ipynb'
- Run Agent in Background: 'cloud/how-tos/cloud_examples/background_run.ipynb'
- Run Multiple Agents in Thread: 'cloud/how-tos/cloud_examples/same-thread.ipynb'
- Human in the loop: 'cloud/how-tos/cloud_examples/human-in-the-loop_cloud.ipynb'
- Stream Values: "cloud/how-tos/cloud_examples/stream_values.ipynb"
- Stream Updates: "cloud/how-tos/cloud_examples/stream_updates.ipynb"
- Stream Messages: "cloud/how-tos/cloud_examples/stream_messages.ipynb"
- Stream Events: "cloud/how-tos/cloud_examples/stream_events.ipynb"
- Stream Debug: "cloud/how-tos/cloud_examples/stream_debug.ipynb"
- Multiple Modes: "cloud/how-tos/cloud_examples/stream_multiple.ipynb"
- Double Texting:
- Interrupt: "cloud/how-tos/cloud_examples/interrupt_concurrent.ipynb"
- Rollback: "cloud/how-tos/cloud_examples/rollback_concurrent.ipynb"
- Reject: "cloud/how-tos/cloud_examples/reject_concurrent.ipynb"
- Enqueue: "cloud/how-tos/cloud_examples/enqueue_concurrent.ipynb"
- Run Agent in Background: "cloud/how-tos/cloud_examples/background_run.ipynb"
- Run Multiple Agents in Thread: "cloud/how-tos/cloud_examples/same-thread.ipynb"
- Human-in-the-Loop:
- Add Breakpoint: 'cloud/how-tos/cloud_examples/human_in_the_loop_breakpoint.ipynb'
- Wait for User Input: 'cloud/how-tos/cloud_examples/human_in_the_loop_user_input.ipynb'
- Edit Graph State: 'cloud/how-tos/cloud_examples/human_in_the_loop_edit_state.ipynb'
- Replay and Branch from Prior States: 'cloud/how-tos/cloud_examples/human_in_the_loop_time_travel.ipynb'
- Create Agents with Configuration: 'cloud/how-tos/cloud_examples/configuration_cloud.ipynb'
- Conceptual Guides:
- API Concepts: 'cloud/api_concepts.md'
- Reference:
- API: 'cloud/reference/api_ref.md'
- Convert LangGraph calls to LangGraph Cloud calls: 'cloud/how-tos/cloud_examples/langgraph_to_langgraph_cloud.ipynb'
- Create Cron Jobs: 'cloud/how-tos/cloud_examples/cron_jobs.ipynb'
- Create Stateless Runs: 'cloud/how-tos/cloud_examples/stateless_runs.ipynb'
- Integrate Webhooks: 'cloud/how-tos/cloud_examples/webhooks.ipynb'
- SDK:
- Python: 'cloud/reference/sdk/python_sdk_ref.md'
- JS/TS: 'cloud/reference/sdk/js_ts_sdk_ref.md'
- CLI: 'cloud/reference/cli.md'
- Python: "cloud/sdk/python_sdk.ipynb"
- JS/TS: "cloud/sdk/js_sdk.ipynb"
- Conceptual Guides: "cloud/concepts/index.md"
- Reference:
- API: "cloud/reference/api/api_ref.md"
- SDK:
- Python: "cloud/reference/sdk/python_sdk_ref.md"
- JS/TS: "cloud/reference/sdk/js_ts_sdk_ref.md"
- CLI: "cloud/reference/cli.md"
- Environment Variables: "cloud/reference/env_var.md"
markdown_extensions:
- abbr
@@ -226,7 +239,7 @@ markdown_extensions:
- pymdownx.magiclink:
normalize_issue_symbols: true
repo_url_shorthand: true
user: langchain-ai
user: langchain-ai
repo: langgraph
- pymdownx.mark
- pymdownx.smartsymbols
@@ -243,12 +256,15 @@ markdown_extensions:
combine_header_slug: true
- pymdownx.tasklist:
custom_checkbox: true
- markdown_include.include:
base_path: ./
- github-callouts
extra_css:
- css/mkdocstrings.css
extra:
social:
- icon: fontawesome/brands/js
- icon: fontawesome/brands/js
link: https://langchain-ai.github.io/langgraphjs/
- icon: fontawesome/brands/github
link: https://github.com/langchain-ai/langgraph
@@ -268,5 +284,10 @@ extra:
- icon: material/emoticon-sad-outline
name: This page could be improved
data: 0
note: >-
note: >-
Thanks for your feedback! Please help us improve this page by adding to the discussion below.
validation:
omitted_files: warn
absolute_links: warn
unrecognized_links: warn
anchors: warn
+174 -411
View File
@@ -13,7 +13,7 @@
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 1,
"id": "b8e6408a-b37e-428f-9567-077fa55d58e8",
"metadata": {},
"outputs": [],
@@ -26,22 +26,22 @@
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": 2,
"id": "4947e9bc-111f-4991-8c41-1041da9bf0ba",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[{'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n",
" 'graph_id': 'agent',\n",
" 'config': {},\n",
" 'created_at': '2024-05-18T00:19:39.688822+00:00',\n",
" 'updated_at': '2024-05-18T00:19:39.688822+00:00',\n",
" 'metadata': {'created_by': 'system'}}]"
"{'assistant_id': 'e90fee30-be91-43aa-a33c-d54bd219072e',\n",
" 'graph_id': 'agent',\n",
" 'created_at': '2024-06-18T18:06:55.102231+00:00',\n",
" 'updated_at': '2024-06-18T18:06:55.102231+00:00',\n",
" 'config': {'configurable': {'model_name': 'anthropic'}},\n",
" 'metadata': {}}"
]
},
"execution_count": 7,
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
@@ -49,53 +49,36 @@
"source": [
"# List available assistants\n",
"assistants = await client.assistants.search()\n",
"assistants"
"assistants[0]"
]
},
{
"cell_type": "code",
"execution_count": 9,
"execution_count": 3,
"id": "230c0464-a6e5-420f-9e38-ca514e5634ce",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n",
" 'graph_id': 'agent',\n",
" 'config': {},\n",
" 'created_at': '2024-05-18T00:19:39.688822+00:00',\n",
" 'updated_at': '2024-05-18T00:19:39.688822+00:00',\n",
" 'metadata': {'created_by': 'system'}}"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"outputs": [],
"source": [
"# Get the first assistant, we will use this one\n",
"assistant = assistants[0]\n",
"assistant"
"# NOTE: we can use `assistant_id` UUID from the above response, or just pass graph ID instead when creating runs. we'll use graph ID here\n",
"assistant_id = \"agent\""
]
},
{
"cell_type": "code",
"execution_count": 10,
"execution_count": 4,
"id": "56aa5159-5583-4134-9210-709b969bda6f",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'thread_id': '6ada015b-b47a-4c4f-a5cd-580893cb6d0c',\n",
" 'created_at': '2024-05-18T00:50:26.367620+00:00',\n",
" 'updated_at': '2024-05-18T00:50:26.367620+00:00',\n",
"{'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n",
" 'created_at': '2024-06-21T14:58:02.079462+00:00',\n",
" 'updated_at': '2024-06-21T14:58:02.079462+00:00',\n",
" 'metadata': {}}"
]
},
"execution_count": 10,
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
@@ -108,7 +91,7 @@
},
{
"cell_type": "code",
"execution_count": 11,
"execution_count": 5,
"id": "147c3f98-f889-4f05-a090-6b31f2a0b291",
"metadata": {},
"outputs": [
@@ -118,450 +101,238 @@
"[]"
]
},
"execution_count": 11,
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# If we list runs on this thread, we can see it is empty\n",
"runs = await client.runs.list(thread['thread_id'])\n",
"runs = await client.runs.list(thread[\"thread_id\"])\n",
"runs"
]
},
{
"cell_type": "code",
"execution_count": null,
"execution_count": 6,
"id": "8c7b44ef-4816-496d-88a1-2f7327cf576d",
"metadata": {},
"outputs": [],
"source": [
"# Let's kick off a run\n",
"input = {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf\"}]}\n",
"run = await client.runs.create(thread['thread_id'], assistant[\"assistant_id\"], input=input)\n"
"run = await client.runs.create(thread[\"thread_id\"], assistant_id, input=input)"
]
},
{
"cell_type": "code",
"execution_count": 13,
"execution_count": 7,
"id": "d84b4d80-b0aa-4d9f-a05d-0744b2fe8f72",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'run_id': 'e843abcb-e478-421b-91e1-a8ae171b14f4',\n",
" 'thread_id': '6ada015b-b47a-4c4f-a5cd-580893cb6d0c',\n",
"{'run_id': '1ef2fdea-814c-6165-8b2a-a40e2a028198',\n",
" 'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n",
" 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n",
" 'created_at': '2024-05-18T00:50:27.618761+00:00',\n",
" 'updated_at': '2024-05-18T00:50:27.618761+00:00',\n",
" 'created_at': '2024-06-21T14:58:02.095911+00:00',\n",
" 'updated_at': '2024-06-21T14:58:02.095911+00:00',\n",
" 'metadata': {},\n",
" 'status': 'pending',\n",
" 'metadata': {}}"
" 'kwargs': {'input': {'messages': [{'role': 'human',\n",
" 'content': 'what's the weather in sf'}]},\n",
" 'config': {'metadata': {'created_by': 'system'},\n",
" 'configurable': {'run_id': '1ef2fdea-814c-6165-8b2a-a40e2a028198',\n",
" 'user_id': '',\n",
" 'graph_id': 'agent',\n",
" 'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n",
" 'thread_ts': None,\n",
" 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}},\n",
" 'webhook': None,\n",
" 'temporary': False,\n",
" 'stream_mode': ['events'],\n",
" 'feedback_keys': None,\n",
" 'interrupt_after': None,\n",
" 'interrupt_before': None},\n",
" 'multitask_strategy': 'reject'}"
]
},
"execution_count": 13,
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# The first time we poll it, we can see `status=pending`\n",
"await client.runs.get(thread['thread_id'], run['run_id'])"
"await client.runs.get(thread[\"thread_id\"], run[\"run_id\"])"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "ce124bd3-f197-4b73-9ff6-bb36730dd003",
"metadata": {
"scrolled": true
},
"outputs": [
{
"data": {
"text/plain": [
"[{'event_id': '3ac6d963-442f-481c-9fde-b8a27bc0e277',\n",
" 'run_id': 'e843abcb-e478-421b-91e1-a8ae171b14f4',\n",
" 'received_at': '2024-05-18T00:50:29.570649+00:00',\n",
" 'span_id': 'd4e4a6ee-da2f-4b5f-b656-1a1f73065161',\n",
" 'event': 'on_tool_start',\n",
" 'name': 'tavily_search_results_json',\n",
" 'data': {'input': {'query': 'weather in san francisco'}},\n",
" 'metadata': {'graph_id': 'agent',\n",
" 'thread_id': '6ada015b-b47a-4c4f-a5cd-580893cb6d0c',\n",
" 'created_by': 'system',\n",
" 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'},\n",
" 'tags': ['seq:step:1']},\n",
" {'event_id': 'f8961760-6f13-40e6-9eef-6d4d68e0ed19',\n",
" 'run_id': 'e843abcb-e478-421b-91e1-a8ae171b14f4',\n",
" 'received_at': '2024-05-18T00:50:29.569708+00:00',\n",
" 'span_id': '73295bb1-6cd3-403d-abc1-4f9d96a63894',\n",
" 'event': 'on_chain_start',\n",
" 'name': 'action',\n",
" 'data': {},\n",
" 'metadata': {'graph_id': 'agent',\n",
" 'thread_id': '6ada015b-b47a-4c4f-a5cd-580893cb6d0c',\n",
" 'created_by': 'system',\n",
" 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'},\n",
" 'tags': ['graph:step:2']},\n",
" {'event_id': '44b5f815-f60c-468f-8d23-96dfdaa0ed20',\n",
" 'run_id': 'e843abcb-e478-421b-91e1-a8ae171b14f4',\n",
" 'received_at': '2024-05-18T00:50:29.568202+00:00',\n",
" 'span_id': 'e843abcb-e478-421b-91e1-a8ae171b14f4',\n",
" 'event': 'on_chain_stream',\n",
" 'name': 'LangGraph',\n",
" 'data': {'chunk': {'messages': [{'id': '46b31c3a-01bf-4946-bd5a-fa6a7f6c97ce',\n",
" 'name': None,\n",
" 'type': 'human',\n",
" 'content': 'what's the weather in sf',\n",
" 'example': False,\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {}},\n",
" {'id': 'run-4885d5a0-cd89-4f00-8558-e85b542a710c',\n",
" 'name': None,\n",
" 'type': 'ai',\n",
" 'content': [{'id': 'toolu_018yyEfJHihdVfWypRNLqDug',\n",
" 'name': 'tavily_search_results_json',\n",
" 'type': 'tool_use',\n",
" 'input': {'query': 'weather in san francisco'}}],\n",
" 'example': False,\n",
" 'tool_calls': [{'id': 'toolu_018yyEfJHihdVfWypRNLqDug',\n",
" 'args': {'query': 'weather in san francisco'},\n",
" 'name': 'tavily_search_results_json'}],\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {},\n",
" 'invalid_tool_calls': []}]}},\n",
" 'metadata': {'graph_id': 'agent',\n",
" 'thread_id': '6ada015b-b47a-4c4f-a5cd-580893cb6d0c',\n",
" 'created_by': 'system',\n",
" 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'},\n",
" 'tags': []},\n",
" {'event_id': '0917b31d-f49d-43d4-a8ed-a9eebd8904e8',\n",
" 'run_id': 'e843abcb-e478-421b-91e1-a8ae171b14f4',\n",
" 'received_at': '2024-05-18T00:50:29.566761+00:00',\n",
" 'span_id': 'f469ac2e-17a3-491c-bc03-0c56aa30a68b',\n",
" 'event': 'on_chain_end',\n",
" 'name': 'agent',\n",
" 'data': {'input': {'messages': [{'role': 'human',\n",
" 'content': 'what's the weather in sf'}]},\n",
" 'output': {'messages': [{'id': 'run-4885d5a0-cd89-4f00-8558-e85b542a710c',\n",
" 'name': None,\n",
" 'type': 'ai',\n",
" 'content': [{'id': 'toolu_018yyEfJHihdVfWypRNLqDug',\n",
" 'name': 'tavily_search_results_json',\n",
" 'type': 'tool_use',\n",
" 'input': {'query': 'weather in san francisco'}}],\n",
" 'example': False,\n",
" 'tool_calls': [{'id': 'toolu_018yyEfJHihdVfWypRNLqDug',\n",
" 'args': {'query': 'weather in san francisco'},\n",
" 'name': 'tavily_search_results_json'}],\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {},\n",
" 'invalid_tool_calls': []}]}},\n",
" 'metadata': {'graph_id': 'agent',\n",
" 'thread_id': '6ada015b-b47a-4c4f-a5cd-580893cb6d0c',\n",
" 'created_by': 'system',\n",
" 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'},\n",
" 'tags': ['graph:step:1']},\n",
" {'event_id': 'bbe33ebf-04ba-43d5-8718-e2da23295675',\n",
" 'run_id': 'e843abcb-e478-421b-91e1-a8ae171b14f4',\n",
" 'received_at': '2024-05-18T00:50:29.566076+00:00',\n",
" 'span_id': 'f469ac2e-17a3-491c-bc03-0c56aa30a68b',\n",
" 'event': 'on_chain_stream',\n",
" 'name': 'agent',\n",
" 'data': {'chunk': {'messages': [{'id': 'run-4885d5a0-cd89-4f00-8558-e85b542a710c',\n",
" 'name': None,\n",
" 'type': 'ai',\n",
" 'content': [{'id': 'toolu_018yyEfJHihdVfWypRNLqDug',\n",
" 'name': 'tavily_search_results_json',\n",
" 'type': 'tool_use',\n",
" 'input': {'query': 'weather in san francisco'}}],\n",
" 'example': False,\n",
" 'tool_calls': [{'id': 'toolu_018yyEfJHihdVfWypRNLqDug',\n",
" 'args': {'query': 'weather in san francisco'},\n",
" 'name': 'tavily_search_results_json'}],\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {},\n",
" 'invalid_tool_calls': []}]}},\n",
" 'metadata': {'graph_id': 'agent',\n",
" 'thread_id': '6ada015b-b47a-4c4f-a5cd-580893cb6d0c',\n",
" 'created_by': 'system',\n",
" 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'},\n",
" 'tags': ['graph:step:1']},\n",
" {'event_id': '38333127-fa97-4830-8157-f76264778d81',\n",
" 'run_id': 'e843abcb-e478-421b-91e1-a8ae171b14f4',\n",
" 'received_at': '2024-05-18T00:50:29.564195+00:00',\n",
" 'span_id': 'ef55c681-be15-4f3d-9aee-3a9ff05d8746',\n",
" 'event': 'on_chain_end',\n",
" 'name': 'should_continue',\n",
" 'data': {'input': {'messages': [{'id': 'abc3581e-417b-4ca1-ab31-de7108e64b3b',\n",
" 'name': None,\n",
" 'type': 'human',\n",
" 'content': 'what's the weather in sf',\n",
" 'example': False,\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {}},\n",
" {'id': 'run-4885d5a0-cd89-4f00-8558-e85b542a710c',\n",
" 'name': None,\n",
" 'type': 'ai',\n",
" 'content': [{'id': 'toolu_018yyEfJHihdVfWypRNLqDug',\n",
" 'name': 'tavily_search_results_json',\n",
" 'type': 'tool_use',\n",
" 'input': {'query': 'weather in san francisco'}}],\n",
" 'example': False,\n",
" 'tool_calls': [{'id': 'toolu_018yyEfJHihdVfWypRNLqDug',\n",
" 'args': {'query': 'weather in san francisco'},\n",
" 'name': 'tavily_search_results_json'}],\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {},\n",
" 'invalid_tool_calls': []}]},\n",
" 'output': 'continue'},\n",
" 'metadata': {'graph_id': 'agent',\n",
" 'thread_id': '6ada015b-b47a-4c4f-a5cd-580893cb6d0c',\n",
" 'created_by': 'system',\n",
" 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'},\n",
" 'tags': ['seq:step:3']},\n",
" {'event_id': '408a7785-c715-4bcb-a5aa-950f414baa77',\n",
" 'run_id': 'e843abcb-e478-421b-91e1-a8ae171b14f4',\n",
" 'received_at': '2024-05-18T00:50:29.563289+00:00',\n",
" 'span_id': 'ef55c681-be15-4f3d-9aee-3a9ff05d8746',\n",
" 'event': 'on_chain_start',\n",
" 'name': 'should_continue',\n",
" 'data': {'input': {'messages': [{'id': 'abc3581e-417b-4ca1-ab31-de7108e64b3b',\n",
" 'name': None,\n",
" 'type': 'human',\n",
" 'content': 'what's the weather in sf',\n",
" 'example': False,\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {}},\n",
" {'id': 'run-4885d5a0-cd89-4f00-8558-e85b542a710c',\n",
" 'name': None,\n",
" 'type': 'ai',\n",
" 'content': [{'id': 'toolu_018yyEfJHihdVfWypRNLqDug',\n",
" 'name': 'tavily_search_results_json',\n",
" 'type': 'tool_use',\n",
" 'input': {'query': 'weather in san francisco'}}],\n",
" 'example': False,\n",
" 'tool_calls': [{'id': 'toolu_018yyEfJHihdVfWypRNLqDug',\n",
" 'args': {'query': 'weather in san francisco'},\n",
" 'name': 'tavily_search_results_json'}],\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {},\n",
" 'invalid_tool_calls': []}]}},\n",
" 'metadata': {'graph_id': 'agent',\n",
" 'thread_id': '6ada015b-b47a-4c4f-a5cd-580893cb6d0c',\n",
" 'created_by': 'system',\n",
" 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'},\n",
" 'tags': ['seq:step:3']},\n",
" {'event_id': '679c8ae7-5cd2-4462-936e-20f0ea45cfb8',\n",
" 'run_id': 'e843abcb-e478-421b-91e1-a8ae171b14f4',\n",
" 'received_at': '2024-05-18T00:50:29.560628+00:00',\n",
" 'span_id': '4885d5a0-cd89-4f00-8558-e85b542a710c',\n",
" 'event': 'on_chat_model_end',\n",
" 'name': 'ChatAnthropic',\n",
" 'data': {'input': {'messages': [[{'id': None,\n",
" 'name': None,\n",
" 'type': 'human',\n",
" 'content': 'what's the weather in sf',\n",
" 'example': False,\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {}}]]},\n",
" 'output': {'run': None,\n",
" 'llm_output': None,\n",
" 'generations': [[{'text': '',\n",
" 'type': 'ChatGeneration',\n",
" 'message': {'id': 'run-4885d5a0-cd89-4f00-8558-e85b542a710c',\n",
" 'name': None,\n",
" 'type': 'ai',\n",
" 'content': [{'id': 'toolu_018yyEfJHihdVfWypRNLqDug',\n",
" 'name': 'tavily_search_results_json',\n",
" 'type': 'tool_use',\n",
" 'input': {'query': 'weather in san francisco'}}],\n",
" 'example': False,\n",
" 'tool_calls': [{'id': 'toolu_018yyEfJHihdVfWypRNLqDug',\n",
" 'args': {'query': 'weather in san francisco'},\n",
" 'name': 'tavily_search_results_json'}],\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {},\n",
" 'invalid_tool_calls': []},\n",
" 'generation_info': None}]]}},\n",
" 'metadata': {'graph_id': 'agent',\n",
" 'thread_id': '6ada015b-b47a-4c4f-a5cd-580893cb6d0c',\n",
" 'created_by': 'system',\n",
" 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n",
" 'ls_model_type': 'chat'},\n",
" 'tags': ['seq:step:1']},\n",
" {'event_id': '055fcf73-36e7-444b-990d-6263ec50925c',\n",
" 'run_id': 'e843abcb-e478-421b-91e1-a8ae171b14f4',\n",
" 'received_at': '2024-05-18T00:50:29.559491+00:00',\n",
" 'span_id': '4885d5a0-cd89-4f00-8558-e85b542a710c',\n",
" 'event': 'on_chat_model_stream',\n",
" 'name': 'ChatAnthropic',\n",
" 'data': {'chunk': {'id': 'run-4885d5a0-cd89-4f00-8558-e85b542a710c',\n",
" 'name': None,\n",
" 'type': 'AIMessageChunk',\n",
" 'content': [{'id': 'toolu_018yyEfJHihdVfWypRNLqDug',\n",
" 'name': 'tavily_search_results_json',\n",
" 'type': 'tool_use',\n",
" 'input': {'query': 'weather in san francisco'}}],\n",
" 'example': False,\n",
" 'tool_calls': [{'id': 'toolu_018yyEfJHihdVfWypRNLqDug',\n",
" 'args': {'query': 'weather in san francisco'},\n",
" 'name': 'tavily_search_results_json'}],\n",
" 'tool_call_chunks': [{'id': 'toolu_018yyEfJHihdVfWypRNLqDug',\n",
" 'args': '{\"query\": \"weather in san francisco\"}',\n",
" 'name': 'tavily_search_results_json',\n",
" 'index': 0}],\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {},\n",
" 'invalid_tool_calls': []}},\n",
" 'metadata': {'graph_id': 'agent',\n",
" 'thread_id': '6ada015b-b47a-4c4f-a5cd-580893cb6d0c',\n",
" 'created_by': 'system',\n",
" 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n",
" 'ls_model_type': 'chat'},\n",
" 'tags': ['seq:step:1']},\n",
" {'event_id': 'cf90f755-5a12-46bd-8f60-adc862388635',\n",
" 'run_id': 'e843abcb-e478-421b-91e1-a8ae171b14f4',\n",
" 'received_at': '2024-05-18T00:50:27.873602+00:00',\n",
" 'span_id': '4885d5a0-cd89-4f00-8558-e85b542a710c',\n",
" 'event': 'on_chat_model_start',\n",
" 'name': 'ChatAnthropic',\n",
" 'data': {'input': {'messages': [[{'id': None,\n",
" 'name': None,\n",
" 'type': 'human',\n",
" 'content': 'what's the weather in sf',\n",
" 'example': False,\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {}}]]}},\n",
" 'metadata': {'graph_id': 'agent',\n",
" 'thread_id': '6ada015b-b47a-4c4f-a5cd-580893cb6d0c',\n",
" 'created_by': 'system',\n",
" 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n",
" 'ls_model_type': 'chat'},\n",
" 'tags': ['seq:step:1']}]"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"execution_count": 8,
"id": "3639da3c-bfe5-454c-ab1e-8ed7af394dfe",
"metadata": {},
"outputs": [],
"source": [
"# We can list events for the run\n",
"await client.runs.list_events(thread['thread_id'], run['run_id'])"
"# Wait until the run finishes\n",
"await client.runs.join(thread[\"thread_id\"], run[\"run_id\"])"
]
},
{
"cell_type": "code",
"execution_count": 15,
"execution_count": 9,
"id": "8fa206ed-515e-4607-9a80-bebafe76cc24",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'run_id': 'e843abcb-e478-421b-91e1-a8ae171b14f4',\n",
" 'thread_id': '6ada015b-b47a-4c4f-a5cd-580893cb6d0c',\n",
"{'run_id': '1ef2fdea-814c-6165-8b2a-a40e2a028198',\n",
" 'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n",
" 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n",
" 'created_at': '2024-05-18T00:50:27.618761+00:00',\n",
" 'updated_at': '2024-05-18T00:50:27.618761+00:00',\n",
" 'created_at': '2024-06-21T14:58:02.095911+00:00',\n",
" 'updated_at': '2024-06-21T14:58:02.095911+00:00',\n",
" 'metadata': {},\n",
" 'status': 'success',\n",
" 'metadata': {}}"
" 'kwargs': {'input': {'messages': [{'role': 'human',\n",
" 'content': 'what's the weather in sf'}]},\n",
" 'config': {'metadata': {'created_by': 'system'},\n",
" 'configurable': {'run_id': '1ef2fdea-814c-6165-8b2a-a40e2a028198',\n",
" 'user_id': '',\n",
" 'graph_id': 'agent',\n",
" 'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n",
" 'thread_ts': None,\n",
" 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}},\n",
" 'webhook': None,\n",
" 'temporary': False,\n",
" 'stream_mode': ['events'],\n",
" 'feedback_keys': None,\n",
" 'interrupt_after': None,\n",
" 'interrupt_before': None},\n",
" 'multitask_strategy': 'reject'}"
]
},
"execution_count": 15,
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Eventually, it should finish and we should see `status=success`\n",
"await client.runs.get(thread['thread_id'], run['run_id'])"
"await client.runs.get(thread[\"thread_id\"], run[\"run_id\"])"
]
},
{
"cell_type": "code",
"execution_count": 16,
"execution_count": 10,
"id": "8de4495f-7873-487c-b1a8-ad2a78a1ff35",
"metadata": {},
"outputs": [],
"source": [
"# We can get the final results\n",
"results = await client.runs.list_events(thread['thread_id'], run['run_id'])"
"final_result = await client.threads.get_state(thread[\"thread_id\"])"
]
},
{
"cell_type": "code",
"execution_count": 21,
"execution_count": 11,
"id": "9da76fce-66e4-4f1b-8c24-09759889e50e",
"metadata": {},
"outputs": [],
"source": [
"# The results are sorted by time, so the most recent (final) step is the 0 index\n",
"final_result = results[0]"
]
},
{
"cell_type": "code",
"execution_count": 22,
"id": "02279ff3-c153-4ec4-be4d-1613a0dff4ee",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'event_id': '1af2076e-8ec7-4f2e-bc2c-6fbbf586397c',\n",
" 'run_id': 'e843abcb-e478-421b-91e1-a8ae171b14f4',\n",
" 'received_at': '2024-05-18T00:50:35.557925+00:00',\n",
" 'span_id': 'e843abcb-e478-421b-91e1-a8ae171b14f4',\n",
" 'event': 'on_chain_end',\n",
" 'name': 'LangGraph',\n",
" 'data': {'output': {'messages': [{'id': '46b31c3a-01bf-4946-bd5a-fa6a7f6c97ce',\n",
" 'name': None,\n",
" 'type': 'human',\n",
" 'content': 'what's the weather in sf',\n",
" 'example': False,\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {}},\n",
" {'id': 'run-4885d5a0-cd89-4f00-8558-e85b542a710c',\n",
" 'name': None,\n",
" 'type': 'ai',\n",
" 'content': [{'id': 'toolu_018yyEfJHihdVfWypRNLqDug',\n",
" 'name': 'tavily_search_results_json',\n",
" 'type': 'tool_use',\n",
" 'input': {'query': 'weather in san francisco'}}],\n",
" 'example': False,\n",
" 'tool_calls': [{'id': 'toolu_018yyEfJHihdVfWypRNLqDug',\n",
" 'args': {'query': 'weather in san francisco'},\n",
" 'name': 'tavily_search_results_json'}],\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {},\n",
" 'invalid_tool_calls': []},\n",
" {'id': '045e936d-ee47-4236-95ff-793b6b32b590',\n",
" 'name': 'tavily_search_results_json',\n",
" 'type': 'tool',\n",
" 'content': '[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{\\'location\\': {\\'name\\': \\'San Francisco\\', \\'region\\': \\'California\\', \\'country\\': \\'United States of America\\', \\'lat\\': 37.78, \\'lon\\': -122.42, \\'tz_id\\': \\'America/Los_Angeles\\', \\'localtime_epoch\\': 1715993410, \\'localtime\\': \\'2024-05-17 17:50\\'}, \\'current\\': {\\'last_updated_epoch\\': 1715993100, \\'last_updated\\': \\'2024-05-17 17:45\\', \\'temp_c\\': 17.8, \\'temp_f\\': 64.0, \\'is_day\\': 1, \\'condition\\': {\\'text\\': \\'Partly cloudy\\', \\'icon\\': \\'//cdn.weatherapi.com/weather/64x64/day/116.png\\', \\'code\\': 1003}, \\'wind_mph\\': 15.0, \\'wind_kph\\': 24.1, \\'wind_degree\\': 300, \\'wind_dir\\': \\'WNW\\', \\'pressure_mb\\': 1013.0, \\'pressure_in\\': 29.9, \\'precip_mm\\': 0.0, \\'precip_in\\': 0.0, \\'humidity\\': 65, \\'cloud\\': 25, \\'feelslike_c\\': 17.8, \\'feelslike_f\\': 64.0, \\'vis_km\\': 16.0, \\'vis_miles\\': 9.0, \\'uv\\': 5.0, \\'gust_mph\\': 16.2, \\'gust_kph\\': 26.1}}\"}]',\n",
" 'tool_call_id': 'toolu_018yyEfJHihdVfWypRNLqDug',\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {}},\n",
" {'id': 'run-069f1ddd-64b3-451d-bb84-75dec23a7286',\n",
" 'name': None,\n",
" 'type': 'ai',\n",
" 'content': \"The search results provide the current weather conditions in San Francisco. According to the data, as of 5:45pm on May 17, 2024, the weather in San Francisco is partly cloudy with a temperature of around 64°F (17.8°C). The wind is blowing from the west-northwest at 15 mph (24 km/h) with gusts up to 16 mph (26 km/h). The humidity is 65% and visibility is 9 miles (16 km). The UV index is 5.\\n\\nSo in summary, it's a partly cloudy spring day in San Francisco with mild temperatures and moderate winds. The weather seems pleasant for being outdoors during the daytime hours.\",\n",
" 'example': False,\n",
" 'tool_calls': [],\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {},\n",
" 'invalid_tool_calls': []}]}},\n",
" 'metadata': {'graph_id': 'agent',\n",
" 'thread_id': '6ada015b-b47a-4c4f-a5cd-580893cb6d0c',\n",
"{'values': {'messages': [{'content': 'what's the weather in sf',\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {},\n",
" 'type': 'human',\n",
" 'name': None,\n",
" 'id': 'bfe07fff-cb40-40be-84d5-a061d2c40006',\n",
" 'example': False},\n",
" {'content': [{'id': 'toolu_01QUzhhfDQkpbPSediUrXvQb',\n",
" 'input': {'query': 'weather in san francisco'},\n",
" 'name': 'tavily_search_results_json',\n",
" 'type': 'tool_use'}],\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {},\n",
" 'type': 'ai',\n",
" 'name': None,\n",
" 'id': 'run-6d8665ca-a77d-4b44-9a7b-4e975b155fb1',\n",
" 'example': False,\n",
" 'tool_calls': [{'name': 'tavily_search_results_json',\n",
" 'args': {'query': 'weather in san francisco'},\n",
" 'id': 'toolu_01QUzhhfDQkpbPSediUrXvQb'}],\n",
" 'invalid_tool_calls': [],\n",
" 'usage_metadata': None},\n",
" {'content': '[{\"url\": \"https://www.timeanddate.com/weather/usa/san-francisco/historic\", \"content\": \"San Francisco Weather History for the Previous 24 Hours Show weather for: Previous 24 hours June 17, 2024 June 16, 2024 June 15, 2024 June 14, 2024 June 13, 2024 June 12, 2024 June 11, 2024 June 10, 2024 June 9, 2024 June 8, 2024 June 7, 2024 June 6, 2024 June 5, 2024 June 4, 2024 June 3, 2024 June 2, 2024\"}]',\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {},\n",
" 'type': 'tool',\n",
" 'name': 'tavily_search_results_json',\n",
" 'id': '257a1f29-2f66-4f9e-b35d-c8818dbbaa3f',\n",
" 'tool_call_id': 'toolu_01QUzhhfDQkpbPSediUrXvQb'},\n",
" {'content': [{'text': 'The search results provide historic weather data for San Francisco, but do not give the current weather conditions. To get the current weather forecast for San Francisco, I would need to refine my search query. Here is an updated search:',\n",
" 'type': 'text'},\n",
" {'id': 'toolu_01RLJEcWYRvRoBhiHdrhoRZx',\n",
" 'input': {'query': 'san francisco weather forecast today'},\n",
" 'name': 'tavily_search_results_json',\n",
" 'type': 'tool_use'}],\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {},\n",
" 'type': 'ai',\n",
" 'name': None,\n",
" 'id': 'run-ca41dbf8-7e89-4ff2-a245-87098d7928ba',\n",
" 'example': False,\n",
" 'tool_calls': [{'name': 'tavily_search_results_json',\n",
" 'args': {'query': 'san francisco weather forecast today'},\n",
" 'id': 'toolu_01RLJEcWYRvRoBhiHdrhoRZx'}],\n",
" 'invalid_tool_calls': [],\n",
" 'usage_metadata': None},\n",
" {'content': '[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{\\'location\\': {\\'name\\': \\'San Francisco\\', \\'region\\': \\'California\\', \\'country\\': \\'United States of America\\', \\'lat\\': 37.78, \\'lon\\': -122.42, \\'tz_id\\': \\'America/Los_Angeles\\', \\'localtime_epoch\\': 1718981382, \\'localtime\\': \\'2024-06-21 7:49\\'}, \\'current\\': {\\'last_updated_epoch\\': 1718981100, \\'last_updated\\': \\'2024-06-21 07:45\\', \\'temp_c\\': 12.8, \\'temp_f\\': 55.0, \\'is_day\\': 1, \\'condition\\': {\\'text\\': \\'Overcast\\', \\'icon\\': \\'//cdn.weatherapi.com/weather/64x64/day/122.png\\', \\'code\\': 1009}, \\'wind_mph\\': 6.9, \\'wind_kph\\': 11.2, \\'wind_degree\\': 200, \\'wind_dir\\': \\'SSW\\', \\'pressure_mb\\': 1011.0, \\'pressure_in\\': 29.84, \\'precip_mm\\': 0.01, \\'precip_in\\': 0.0, \\'humidity\\': 86, \\'cloud\\': 100, \\'feelslike_c\\': 12.2, \\'feelslike_f\\': 53.9, \\'windchill_c\\': 11.2, \\'windchill_f\\': 52.1, \\'heatindex_c\\': 12.0, \\'heatindex_f\\': 53.5, \\'dewpoint_c\\': 9.4, \\'dewpoint_f\\': 48.8, \\'vis_km\\': 16.0, \\'vis_miles\\': 9.0, \\'uv\\': 3.0, \\'gust_mph\\': 7.6, \\'gust_kph\\': 12.2}}\"}]',\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {},\n",
" 'type': 'tool',\n",
" 'name': 'tavily_search_results_json',\n",
" 'id': 'c80a3720-6a9f-4ff0-9ce2-6112e66a6f81',\n",
" 'tool_call_id': 'toolu_01RLJEcWYRvRoBhiHdrhoRZx'},\n",
" {'content': 'The updated search provides the current weather forecast for San Francisco. According to the results, as of 7:49am on June 21, 2024 in San Francisco, the temperature is 55°F (12.8°C), it is overcast with 100% cloud cover, and there are light winds from the south-southwest around 7 mph (11 km/h). The forecast also shows low precipitation of 0.01 mm, high humidity of 86%, and visibility of 9 miles (16 km).\\n\\nIn summary, the current weather in San Francisco is cool, overcast, and breezy based on this weather forecast data. Let me know if you need any other details!',\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {},\n",
" 'type': 'ai',\n",
" 'name': None,\n",
" 'id': 'run-4f23b53d-a8ec-4038-b3ed-08b2560bf81c',\n",
" 'example': False,\n",
" 'tool_calls': [],\n",
" 'invalid_tool_calls': [],\n",
" 'usage_metadata': None}]},\n",
" 'next': [],\n",
" 'config': {'configurable': {'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n",
" 'thread_ts': '1ef2fdea-f879-65a5-8005-443b6a4039aa'}},\n",
" 'metadata': {'step': 5,\n",
" 'run_id': '1ef2fdea-814c-6165-8b2a-a40e2a028198',\n",
" 'source': 'loop',\n",
" 'writes': {'agent': {'messages': [{'id': 'run-4f23b53d-a8ec-4038-b3ed-08b2560bf81c',\n",
" 'name': None,\n",
" 'type': 'ai',\n",
" 'content': 'The updated search provides the current weather forecast for San Francisco. According to the results, as of 7:49am on June 21, 2024 in San Francisco, the temperature is 55°F (12.8°C), it is overcast with 100% cloud cover, and there are light winds from the south-southwest around 7 mph (11 km/h). The forecast also shows low precipitation of 0.01 mm, high humidity of 86%, and visibility of 9 miles (16 km).\\n\\nIn summary, the current weather in San Francisco is cool, overcast, and breezy based on this weather forecast data. Let me know if you need any other details!',\n",
" 'example': False,\n",
" 'tool_calls': [],\n",
" 'usage_metadata': None,\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {},\n",
" 'invalid_tool_calls': []}]}},\n",
" 'user_id': '',\n",
" 'graph_id': 'agent',\n",
" 'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n",
" 'created_by': 'system',\n",
" 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'},\n",
" 'tags': []}"
" 'created_at': '2024-06-21T14:58:14.591805+00:00',\n",
" 'parent_config': {'configurable': {'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n",
" 'thread_ts': '1ef2fdea-d44c-6fc4-8004-d2713436777d'}}}"
]
},
"execution_count": 22,
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
@@ -572,40 +343,32 @@
},
{
"cell_type": "code",
"execution_count": 28,
"execution_count": 12,
"id": "ddd6e698-4609-4389-b84a-bb8939fff08b",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"\"The search results provide the current weather conditions in San Francisco. According to the data, as of 5:45pm on May 17, 2024, the weather in San Francisco is partly cloudy with a temperature of around 64°F (17.8°C). The wind is blowing from the west-northwest at 15 mph (24 km/h) with gusts up to 16 mph (26 km/h). The humidity is 65% and visibility is 9 miles (16 km). The UV index is 5.\\n\\nSo in summary, it's a partly cloudy spring day in San Francisco with mild temperatures and moderate winds. The weather seems pleasant for being outdoors during the daytime hours.\""
"'The updated search provides the current weather forecast for San Francisco. According to the results, as of 7:49am on June 21, 2024 in San Francisco, the temperature is 55°F (12.8°C), it is overcast with 100% cloud cover, and there are light winds from the south-southwest around 7 mph (11 km/h). The forecast also shows low precipitation of 0.01 mm, high humidity of 86%, and visibility of 9 miles (16 km).\\n\\nIn summary, the current weather in San Francisco is cool, overcast, and breezy based on this weather forecast data. Let me know if you need any other details!'"
]
},
"execution_count": 28,
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# We can get the content of the final message\n",
"final_result['data']['output']['messages'][-1]['content']"
"final_result[\"values\"][\"messages\"][-1][\"content\"]"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "535638f7-48a9-49bb-9a0b-57a5b36d0696",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"display_name": "langgraph-example-dev",
"language": "python",
"name": "python3"
"name": "langgraph-example-dev"
},
"language_info": {
"codemirror_mode": {
@@ -617,7 +380,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.1"
"version": "3.11.9"
}
},
"nbformat": 4,
@@ -68,7 +68,7 @@
"# We can do this by getting the default assistant\n",
"# There should always be a default assistant with no configuration\n",
"assistants = await client.assistants.search()\n",
"assistants = [a for a in assistants if not a['config']]\n",
"assistants = [a for a in assistants if not a[\"config\"]]\n",
"base_assistant = assistants[0]"
]
},
@@ -93,10 +93,12 @@
],
"source": [
"# We can now call `.get_schemas` to get schemas associated with this graph\n",
"schemas = await client.assistants.get_schemas(assistant_id=base_assistant[\"assistant_id\"])\n",
"schemas = await client.assistants.get_schemas(\n",
" assistant_id=base_assistant[\"assistant_id\"]\n",
")\n",
"# There are multiple types of schemas\n",
"# We can get the `config_schema` to look at the the configurable parameters\n",
"schemas['config_schema']['definitions']['Configurable']['properties']"
"schemas[\"config_schema\"][\"definitions\"][\"Configurable\"][\"properties\"]"
]
},
{
@@ -106,7 +108,9 @@
"metadata": {},
"outputs": [],
"source": [
"assistant = await client.assistants.create(graph_id=\"agent\", config={\"configurable\": {\"model_name\": \"openai\"}})"
"assistant = await client.assistants.create(\n",
" graph_id=\"agent\", config={\"configurable\": {\"model_name\": \"openai\"}}\n",
")"
]
},
{
@@ -163,7 +167,9 @@
"source": [
"thread = await client.threads.create()\n",
"input = {\"messages\": [{\"role\": \"user\", \"content\": \"who made you?\"}]}\n",
"async for event in client.runs.stream(thread['thread_id'], assistant['assistant_id'], input=input):\n",
"async for event in client.runs.stream(\n",
" thread[\"thread_id\"], assistant[\"assistant_id\"], input=input\n",
"):\n",
" print(event)"
]
},
+122
View File
@@ -0,0 +1,122 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Cron Jobs\n",
"\n",
"Sometimes you don't want to run your graph based on user interaction, but rather you would like to schedule your graph to run on a schedule - for example if you wish for your graph to compose and send out a weekly email of to-dos for your team. LangGraph Cloud allows you to do this without having to write your own script by using the `Crons` client. To schedule a graph job, you need to pass a [cron expression](https://crontab.cronhub.io/) to inform the client when you want to run the graph. `Cron` jobs are run in the background and do not interfere with normal invocations of the graph.\n",
"\n",
"## Setup\n",
"\n",
"First, let's setup our SDK client, assistant, and thread:"
]
},
{
"cell_type": "code",
"execution_count": 110,
"metadata": {},
"outputs": [],
"source": [
"from langgraph_sdk import get_client\n",
"client = get_client()\n",
"assistants = await client.assistants.search()\n",
"assistants = [a for a in assistants if not a['config']]\n",
"assistant = assistants[0]\n",
"thread = await client.threads.create()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Cron job on a thread \n",
"\n",
"To create a cron job associated with a specific thread, you can write:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# This schedules a job to run at 15:27 (3:27PM) every day\n",
"cron_1 = await client.crons.create_for_thread(thread['thread_id'],assistant['assistant_id'],schedule=\"27 15 * * *\",input={'messages':[{\"role\":\"user\",\"content\":\"What time is it?\"}]})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Note that it is **very** important to delete `Cron` jobs that are no longer useful. Otherwise you could rack up unwanted API charges to the LLM! You can delete a `Cron` job using the following code:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"await client.crons.delete(cron_1['cron_id'])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Cron job threadless\n",
"\n",
"You can also create threadless cron jobs by using the following code:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# This schedules a job to run at 15:27 (3:27PM) every day\n",
"cron_2 = await client.crons.create(assistant['assistant_id'],schedule=\"27 15 * * *\",input={'messages':[{\"role\":\"user\",\"content\":\"What time is it?\"}]})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Again, remember to delete your job once you are done with it!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"await client.crons.delete(cron_2['cron_id'])"
]
}
],
"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.9"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
@@ -6,7 +6,9 @@
"source": [
"## Enqueue\n",
"\n",
"There are several strategies for handling concurrent runs in your graph. This notebook covers how to use the `enqueue` option - please see the other how-to guides in the \"Double Texting\" directory to learn about the other methods.\n",
"This notebook assumes knowledge of what double-texting is, which you can learn about in the [double-texting conceptual guide](https://langchain-ai.github.io/langgraph/cloud/concepts/#double-texting).\n",
"\n",
"The guide covers the `enqueue` option for double texting, which adds the interruptions to a queue and executes them in the order they are received by the client. Below is a quick example of using the `enqueue` option.\n",
"\n",
"First, let's import our required packages and instantiate our client, assistant, and thread."
]
@@ -28,7 +30,7 @@
"outputs": [],
"source": [
"client = get_client()\n",
"assistant = await client.assistants.create(\"agent\")\n",
"assistant_id = \"agent\"\n",
"thread = await client.threads.create()"
]
},
@@ -41,7 +43,7 @@
"# this run will be interrupted\n",
"first_run = await client.runs.create(\n",
" thread[\"thread_id\"],\n",
" assistant[\"assistant_id\"],\n",
" assistant_id,\n",
" input={\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf?\"}]},\n",
")"
]
@@ -54,7 +56,7 @@
"source": [
"second_run = await client.runs.create(\n",
" thread[\"thread_id\"],\n",
" assistant[\"assistant_id\"],\n",
" assistant_id,\n",
" input={\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in nyc?\"}]},\n",
" multitask_strategy=\"enqueue\",\n",
")"
@@ -175,10 +177,14 @@
}
],
"metadata": {
"kernelspec": {
"display_name": "",
"name": ""
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 2
"nbformat_minor": 4
}
@@ -1,685 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53",
"metadata": {},
"source": [
"# How to have a human in the loop\n",
"\n",
"With it's built in persistence layer, LangGraph API is perfect for human-in-the-loop workflows.\n",
"Here we cover a few such examples:\n",
"\n",
"1. Having a human in the loop to approve a tool call\n",
"2. Having a human in the loop to edit a tool call\n",
"3. Having a human in the loop to edit an old state and resume execution from there\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "521d975b-e94b-4c37-bfa1-82d969e2a4dc",
"metadata": {},
"outputs": [],
"source": [
"from langgraph_sdk import get_client"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "27a1392b-86c3-464e-99a8-90ffc965f3ec",
"metadata": {},
"outputs": [],
"source": [
"client = get_client()"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "4947e9bc-111f-4991-8c41-1041da9bf0ba",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[{'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n",
" 'graph_id': 'agent',\n",
" 'config': {},\n",
" 'created_at': '2024-05-18T00:19:39.688822+00:00',\n",
" 'updated_at': '2024-05-18T00:19:39.688822+00:00',\n",
" 'metadata': {'created_by': 'system'}}]"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# There should always be a default assistant with no configuration\n",
"assistants = await client.assistants.search()\n",
"assistants = [a for a in assistants if not a['config']]\n",
"assistants"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "230c0464-a6e5-420f-9e38-ca514e5634ce",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n",
" 'graph_id': 'agent',\n",
" 'config': {},\n",
" 'created_at': '2024-05-18T00:19:39.688822+00:00',\n",
" 'updated_at': '2024-05-18T00:19:39.688822+00:00',\n",
" 'metadata': {'created_by': 'system'}}"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"assistant = assistants[0]\n",
"assistant"
]
},
{
"cell_type": "markdown",
"id": "e0209129-239b-452e-a59a-47be716bbf8c",
"metadata": {},
"source": [
"## Approve a tool call"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "56aa5159-5583-4134-9210-709b969bda6f",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'thread_id': '54ed0901-6767-46c9-a5f9-b65c1c5fd89c',\n",
" 'created_at': '2024-05-18T22:46:16.724701+00:00',\n",
" 'updated_at': '2024-05-18T22:46:16.724701+00:00',\n",
" 'metadata': {}}"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"thread = await client.threads.create()\n",
"thread"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "147c3f98-f889-4f05-a090-6b31f2a0b291",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[]"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"runs = await client.runs.list(thread['thread_id'])\n",
"runs"
]
},
{
"cell_type": "markdown",
"id": "77dae6ad-bb7b-468d-b7fd-9b8a35f13ccb",
"metadata": {},
"source": [
"We now want to add a human-in-the-loop step before a tool is called.\n",
"We can do this by adding `interrupt_before=[\"action\"]`, which tells us to interrupt before calling the action node.\n",
"We can do this either when compiling the graph or when kicking off a run.\n",
"Here we will do it when kicking of a run."
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "7da70e20-1a4e-4df2-b996-1927f474c835",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Receiving new event of type: metadata...\n",
"{'run_id': '3b77ef83-687a-4840-8858-0371f91a92c3'}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: data...\n",
"{'agent': {'messages': [{'content': [{'id': 'toolu_01HwZqM1ptX6E15A5LAmyZTB', 'input': {'query': 'weather in san francisco'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-e5d17791-4d37-4ad2-815f-a0c4cba62585', 'example': False, 'tool_calls': [{'name': 'tavily_search_results_json', 'args': {'query': 'weather in san francisco'}, 'id': 'toolu_01HwZqM1ptX6E15A5LAmyZTB'}], 'invalid_tool_calls': []}]}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: end...\n",
"None\n",
"\n",
"\n",
"\n"
]
}
],
"source": [
"input = {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf\"}]}\n",
"async for chunk in client.runs.stream(\n",
" thread['thread_id'], assistant['assistant_id'], input=input, stream_mode=\"updates\", interrupt_before=['action']\n",
"):\n",
" print(f\"Receiving new event of type: {chunk.event}...\")\n",
" print(chunk.data)\n",
" print(\"\\n\\n\")"
]
},
{
"cell_type": "markdown",
"id": "a36ac0d6-7843-4fab-909c-0b5b6e725a7f",
"metadata": {},
"source": [
"We can now kick off a new run on the same thread with `None` as the input in order to just continue the existing thread."
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "bded66c7-b56e-4db5-809f-fa5a31d8a012",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Receiving new event of type: metadata...\n",
"{'run_id': 'a46f733d-cf5b-4ee3-9e07-08612468c8df'}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: data...\n",
"{'action': {'messages': [{'content': '[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{\\'location\\': {\\'name\\': \\'San Francisco\\', \\'region\\': \\'California\\', \\'country\\': \\'United States of America\\', \\'lat\\': 37.78, \\'lon\\': -122.42, \\'tz_id\\': \\'America/Los_Angeles\\', \\'localtime_epoch\\': 1716072201, \\'localtime\\': \\'2024-05-18 15:43\\'}, \\'current\\': {\\'last_updated_epoch\\': 1716071400, \\'last_updated\\': \\'2024-05-18 15:30\\', \\'temp_c\\': 18.9, \\'temp_f\\': 66.0, \\'is_day\\': 1, \\'condition\\': {\\'text\\': \\'Partly cloudy\\', \\'icon\\': \\'//cdn.weatherapi.com/weather/64x64/day/116.png\\', \\'code\\': 1003}, \\'wind_mph\\': 18.6, \\'wind_kph\\': 29.9, \\'wind_degree\\': 280, \\'wind_dir\\': \\'W\\', \\'pressure_mb\\': 1015.0, \\'pressure_in\\': 29.96, \\'precip_mm\\': 0.0, \\'precip_in\\': 0.0, \\'humidity\\': 59, \\'cloud\\': 25, \\'feelslike_c\\': 18.9, \\'feelslike_f\\': 66.0, \\'vis_km\\': 16.0, \\'vis_miles\\': 9.0, \\'uv\\': 5.0, \\'gust_mph\\': 23.0, \\'gust_kph\\': 37.1}}\"}]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'tavily_search_results_json', 'id': '8be98ff3-6d61-41c5-8384-8db6b7abdbfb', 'tool_call_id': 'toolu_01HwZqM1ptX6E15A5LAmyZTB'}]}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: data...\n",
"{'agent': {'messages': [{'content': \"The weather in San Francisco is currently partly cloudy with a temperature of around 66°F (18.9°C). There are westerly winds of 18.6 mph (29.9 km/h) with gusts up to 23 mph (37.1 km/h). The humidity is 59% and visibility is good at 9 miles (16 km). UV levels are moderate at 5.0.\\n\\nIn summary, it's a nice partly cloudy spring day in San Francisco with comfortable temperatures and a moderate breeze. The weather conditions seem ideal for being outdoors and enjoying the city.\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-7a8a2ff8-d0d6-4200-b0a5-926f2b6a4798', 'example': False, 'tool_calls': [], 'invalid_tool_calls': []}]}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: end...\n",
"None\n",
"\n",
"\n",
"\n"
]
}
],
"source": [
"input = None\n",
"async for chunk in client.runs.stream(\n",
" thread['thread_id'], assistant['assistant_id'], input=input, stream_mode=\"updates\", interrupt_before=['action']\n",
"):\n",
" print(f\"Receiving new event of type: {chunk.event}...\")\n",
" print(chunk.data)\n",
" print(\"\\n\\n\")"
]
},
{
"cell_type": "markdown",
"id": "2072ce5a-8771-42f9-b2de-5d3a7a9c817b",
"metadata": {},
"source": [
"## Edit a tool call\n",
"\n",
"What if we want to edit the tool call?\n",
"We can also do that.\n",
"Let's kick off another run, with the same `interrupt_before=['action']`"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "b226b687-02da-4eef-9286-46dba92b17ba",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Receiving new event of type: metadata...\n",
"{'run_id': 'c7c8e313-dad9-47d9-bd03-e112c94eff9e'}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: data...\n",
"{'agent': {'messages': [{'content': [{'id': 'toolu_01NGhKmeciaT7TfhBSwUT3mi', 'input': {'query': 'weather in los angeles'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-3d417aa5-e9c1-4b76-90f8-597519c28af9', 'example': False, 'tool_calls': [{'name': 'tavily_search_results_json', 'args': {'query': 'weather in los angeles'}, 'id': 'toolu_01NGhKmeciaT7TfhBSwUT3mi'}], 'invalid_tool_calls': []}]}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: end...\n",
"None\n",
"\n",
"\n",
"\n"
]
}
],
"source": [
"input = {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in la?\"}]}\n",
"async for chunk in client.runs.stream(\n",
" thread['thread_id'], assistant['assistant_id'], input=input, stream_mode=\"updates\", interrupt_before=['action']\n",
"):\n",
" print(f\"Receiving new event of type: {chunk.event}...\")\n",
" print(chunk.data)\n",
" print(\"\\n\\n\")"
]
},
{
"cell_type": "markdown",
"id": "ab338423-c18d-446c-9aa3-3ad2f16d742a",
"metadata": {},
"source": [
"We can now inspect the state of the thread"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "bd9ca1f4-c3b0-4fa3-8c91-233a9129a142",
"metadata": {},
"outputs": [],
"source": [
"thread_state = await client.threads.get_state(thread['thread_id'])"
]
},
{
"cell_type": "markdown",
"id": "31e82414-afd2-46c4-a605-ce3eb46df485",
"metadata": {},
"source": [
"Let's get the last message of the thread - this is the one we want to update"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "fe832ec1-7ae0-4d11-8408-d4da88d4dced",
"metadata": {},
"outputs": [],
"source": [
"last_message = thread_state['values']['messages'][-1]"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "434253fe-7397-45e2-8be8-91d002088a96",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[{'id': 'toolu_01NGhKmeciaT7TfhBSwUT3mi',\n",
" 'input': {'query': 'weather in los angeles'},\n",
" 'name': 'tavily_search_results_json',\n",
" 'type': 'tool_use'}]"
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"last_message['content']"
]
},
{
"cell_type": "markdown",
"id": "6d007b31-c8a2-465c-bc78-a5909ca7931c",
"metadata": {},
"source": [
"Let's now modify the tool call to say Louisiana"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "55fcb316-450b-4b8c-9ae9-e7ee395acc55",
"metadata": {},
"outputs": [],
"source": [
"last_message['tool_calls'] = [{\n",
" 'id': last_message['tool_calls'][0]['id'],\n",
" 'name': 'tavily_search_results_json',\n",
" # We change the query to say temperature\n",
" 'args': {'query': 'weather in Louisiana'}\n",
"}]\n",
"# last_message['content'] = [{\n",
"# 'id': last_message['content'][0]['id'],\n",
"# 'name': 'tavily_search_results_json',\n",
"# # We change the query to say temperature\n",
"# 'input': {'query': 'weather in Louisiana'},\n",
"# 'type': 'tool_use'\n",
"# }]"
]
},
{
"cell_type": "markdown",
"id": "d49be54e-5334-47be-8dfb-78b8a8155e98",
"metadata": {},
"source": [
"We can now update the state - we only need to pass in the last updated message because our graph will handle the update."
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "0438f997-bad3-48f6-b532-9ac3a95263c2",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'configurable': {'thread_id': '54ed0901-6767-46c9-a5f9-b65c1c5fd89c',\n",
" 'thread_ts': '1ef15688-1dbd-68f5-8007-75dc0e110124'}}"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"await client.threads.update_state(thread['thread_id'], values={\"messages\": [last_message]})"
]
},
{
"cell_type": "markdown",
"id": "c96668ab-80fa-4ae6-a90b-773a943ba331",
"metadata": {},
"source": [
"Let's now check the state of the thread again, and in particular the final message"
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "31936711-4af4-4bd1-ac10-9ce52922dd2f",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[{'name': 'tavily_search_results_json',\n",
" 'args': {'query': 'weather in Louisiana'},\n",
" 'id': 'toolu_01NGhKmeciaT7TfhBSwUT3mi'}]"
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"thread_state = await client.threads.get_state(thread['thread_id'])\n",
"thread_state['values']['messages'][-1]['tool_calls']"
]
},
{
"cell_type": "markdown",
"id": "20aa8ff3-7876-4db2-9333-c5396cd637ac",
"metadata": {},
"source": [
"Great! We changed it. If we now resume execution (by kicking off a new run with null inputs on the same thread) it should use that new tool call."
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "8e2c4eeb-2888-4979-9877-aa4a53dec5ea",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Receiving new event of type: metadata...\n",
"{'run_id': '1a1ebed1-3581-418a-81be-e834b40c5c82'}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: data...\n",
"{'action': {'messages': [{'content': '[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{\\'location\\': {\\'name\\': \\'Louisiana\\', \\'region\\': \\'Missouri\\', \\'country\\': \\'USA United States of America\\', \\'lat\\': 39.44, \\'lon\\': -91.06, \\'tz_id\\': \\'America/Chicago\\', \\'localtime_epoch\\': 1716072393, \\'localtime\\': \\'2024-05-18 17:46\\'}, \\'current\\': {\\'last_updated_epoch\\': 1716072300, \\'last_updated\\': \\'2024-05-18 17:45\\', \\'temp_c\\': 29.0, \\'temp_f\\': 84.2, \\'is_day\\': 1, \\'condition\\': {\\'text\\': \\'Partly cloudy\\', \\'icon\\': \\'//cdn.weatherapi.com/weather/64x64/day/116.png\\', \\'code\\': 1003}, \\'wind_mph\\': 6.9, \\'wind_kph\\': 11.2, \\'wind_degree\\': 220, \\'wind_dir\\': \\'SW\\', \\'pressure_mb\\': 1011.0, \\'pressure_in\\': 29.86, \\'precip_mm\\': 0.0, \\'precip_in\\': 0.0, \\'humidity\\': 46, \\'cloud\\': 50, \\'feelslike_c\\': 31.4, \\'feelslike_f\\': 88.6, \\'vis_km\\': 16.0, \\'vis_miles\\': 9.0, \\'uv\\': 7.0, \\'gust_mph\\': 7.4, \\'gust_kph\\': 11.9}}\"}]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'tavily_search_results_json', 'id': '728f8ac9-729e-4bf7-b560-b332a73c8f47', 'tool_call_id': 'toolu_01NGhKmeciaT7TfhBSwUT3mi'}]}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: data...\n",
"{'agent': {'messages': [{'content': [{'text': 'The search results seem to be for the weather in Louisiana, Missouri rather than Los Angeles, California. Let me try the search again:', 'type': 'text'}, {'id': 'toolu_019YAXWMK33tG9DaxMzrowc8', 'input': {'query': 'weather in los angeles california'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-c42a3b14-2611-4a1d-8907-95dcdb18f07f', 'example': False, 'tool_calls': [{'name': 'tavily_search_results_json', 'args': {'query': 'weather in los angeles california'}, 'id': 'toolu_019YAXWMK33tG9DaxMzrowc8'}], 'invalid_tool_calls': []}]}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: end...\n",
"None\n",
"\n",
"\n",
"\n"
]
}
],
"source": [
"input = None\n",
"async for chunk in client.runs.stream(\n",
" thread['thread_id'], assistant['assistant_id'], input=input, stream_mode=\"updates\", interrupt_before=['action']\n",
"):\n",
" print(f\"Receiving new event of type: {chunk.event}...\")\n",
" print(chunk.data)\n",
" print(\"\\n\\n\")"
]
},
{
"cell_type": "markdown",
"id": "065f8165-43d8-4876-86af-0cfffd712fee",
"metadata": {},
"source": [
"## Edit an old state\n",
"\n",
"Let's now imagine we want to go back in time and edit the tool call after we had already made it.\n",
"In order to do this, we can get first get the full history of the thread."
]
},
{
"cell_type": "code",
"execution_count": 46,
"id": "de050efd-73a4-441e-91e0-18e08f773a42",
"metadata": {},
"outputs": [],
"source": [
"thread_history = await client.threads.get_history(thread['thread_id'], limit=100)"
]
},
{
"cell_type": "code",
"execution_count": 47,
"id": "07e15435-4a5f-4c2a-b748-0e0f7ab02a28",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"11"
]
},
"execution_count": 47,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(thread_history)"
]
},
{
"cell_type": "markdown",
"id": "a292e721-36c4-41b8-85e4-378f0770652a",
"metadata": {},
"source": [
"After that, we can get the correct state we want to be in. The 0th index state is the most recent one, while the -1 index state is the first.\n",
"In this case, we want to go to the state where the last message had the tool calls for `weather in los angeles`"
]
},
{
"cell_type": "code",
"execution_count": 48,
"id": "132d207c-11cb-4efb-a330-88ebdfc612c8",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[{'name': 'tavily_search_results_json',\n",
" 'args': {'query': 'weather in los angeles'},\n",
" 'id': 'toolu_01FnuDKhUfagwoqhNfiTYTfS'}]"
]
},
"execution_count": 48,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"rewind_state = thread_history[3]\n",
"rewind_state['values']['messages'][-1]['tool_calls']"
]
},
{
"cell_type": "code",
"execution_count": 49,
"id": "45e01ddf-2ccf-4029-b431-e5fce2235b59",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'configurable': {'thread_id': 'df85453d-cb86-48c8-ae84-12081faa1bdf',\n",
" 'thread_ts': '1ef15582-3442-6db7-8006-9166bbb0e80f'}}"
]
},
"execution_count": 49,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"rewind_state['config']"
]
},
{
"cell_type": "markdown",
"id": "d229468e-2f94-4b29-b56b-1d402554dcfb",
"metadata": {},
"source": [
"If we want to, we can now resume execution from that place in time"
]
},
{
"cell_type": "code",
"execution_count": 50,
"id": "94ebc63e-f2cf-4da1-bc8d-52c4731ab0c6",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Receiving new event of type: metadata...\n",
"{'run_id': 'a1cc9263-ef0a-4c04-9194-6f01624d0ef0'}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: data...\n",
"{'action': {'messages': [{'content': '[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{\\'location\\': {\\'name\\': \\'Los Angeles\\', \\'region\\': \\'California\\', \\'country\\': \\'United States of America\\', \\'lat\\': 34.05, \\'lon\\': -118.24, \\'tz_id\\': \\'America/Los_Angeles\\', \\'localtime_epoch\\': 1716071728, \\'localtime\\': \\'2024-05-18 15:35\\'}, \\'current\\': {\\'last_updated_epoch\\': 1716071400, \\'last_updated\\': \\'2024-05-18 15:30\\', \\'temp_c\\': 20.0, \\'temp_f\\': 68.0, \\'is_day\\': 1, \\'condition\\': {\\'text\\': \\'Partly cloudy\\', \\'icon\\': \\'//cdn.weatherapi.com/weather/64x64/day/116.png\\', \\'code\\': 1003}, \\'wind_mph\\': 2.2, \\'wind_kph\\': 3.6, \\'wind_degree\\': 226, \\'wind_dir\\': \\'SW\\', \\'pressure_mb\\': 1016.0, \\'pressure_in\\': 29.99, \\'precip_mm\\': 0.0, \\'precip_in\\': 0.0, \\'humidity\\': 61, \\'cloud\\': 50, \\'feelslike_c\\': 20.0, \\'feelslike_f\\': 68.0, \\'vis_km\\': 16.0, \\'vis_miles\\': 9.0, \\'uv\\': 6.0, \\'gust_mph\\': 12.6, \\'gust_kph\\': 20.3}}\"}]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'tavily_search_results_json', 'id': '7137b2e5-566b-418b-b642-b3c6b64c5224', 'tool_call_id': 'toolu_01FnuDKhUfagwoqhNfiTYTfS'}]}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: data...\n",
"{'agent': {'messages': [{'content': 'The search results show the current weather conditions in Los Angeles. As of 3:30pm on May 18, 2024, the weather in Los Angeles is partly cloudy with a temperature around 68°F (20°C). Winds are light from the southwest around 2-3 mph. The humidity is 61% and visibility is good at 9 miles. Overall, it appears to be a nice spring day in LA with partly sunny skies and comfortable temperatures in the upper 60s.', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-3966b68a-c381-4933-a852-e6a4697c962c', 'example': False, 'tool_calls': [], 'invalid_tool_calls': []}]}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: end...\n",
"None\n",
"\n",
"\n",
"\n"
]
}
],
"source": [
"input = None\n",
"async for chunk in client.runs.stream(\n",
" thread['thread_id'], \n",
" assistant['assistant_id'], \n",
" input=input, \n",
" stream_mode=\"updates\", \n",
" interrupt_before=['action'],\n",
" config=rewind_state['config']\n",
"):\n",
" print(f\"Receiving new event of type: {chunk.event}...\")\n",
" print(chunk.data)\n",
" print(\"\\n\\n\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "492f1d37-0979-4210-8dd7-bc70cdc308f3",
"metadata": {},
"outputs": [],
"source": []
}
],
"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.1"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,116 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to Add Breakpoints\n",
"\n",
"When creating LangGraph agents, it is often nice to add a human-in-the-loop component.\n",
"This can be helpful when giving them access to tools.\n",
"Often in these situations you may want to manually approve an action before taking.\n",
"\n",
"This can be in several ways, but the primary supported way is to add an \"interrupt\" before a node is executed.\n",
"This interrupts execution at that node.\n",
"You can then resume from that spot to continue. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"### Code for your graph\n",
"\n",
"In this how-to we use a simple ReAct style hosted graph (you can see the full code for defining it [here](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/breakpoints/)). The important thing is that there are two nodes (one named `agent` that calls the LLM, and one named `action` that calls the tool), and a routing function from `agent` that determines whether to call `action` next or just end the graph run (the `action` node always calls the `agent` node after execution).\n",
"\n",
"### SDK Initialization"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langgraph_sdk import get_client\n",
"client = get_client()\n",
"assistants = await client.assistants.search()\n",
"assistants = [a for a in assistants if not a['config']]\n",
"assistant = assistants[0]\n",
"assistant_id = assistant['assistant_id']\n",
"thread = await client.threads.create()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Adding a breakpoint\n",
"\n",
"We now want to add a breakpoint in our graph run, which we will do before a tool is called.\n",
"We can do this by adding `interrupt_before=[\"action\"]`, which tells us to interrupt before calling the action node.\n",
"We can do this either when compiling the graph or when kicking off a run.\n",
"Here we will do it when kicking of a run, if you would like to to do it at compile time you need to edit the python file where your graph is defined and add the `interrupt_before` parameter when you call `.compile`.\n",
"\n",
"First let's access our hosted Langgraph instance through the SDK:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"And, now let's compile it with a breakpoint before the tool node:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Receiving new event of type: metadata...\n",
"{'run_id': '3b77ef83-687a-4840-8858-0371f91a92c3'}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: data...\n",
"{'agent': {'messages': [{'content': [{'id': 'toolu_01HwZqM1ptX6E15A5LAmyZTB', 'input': {'query': 'weather in san francisco'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-e5d17791-4d37-4ad2-815f-a0c4cba62585', 'example': False, 'tool_calls': [{'name': 'tavily_search_results_json', 'args': {'query': 'weather in san francisco'}, 'id': 'toolu_01HwZqM1ptX6E15A5LAmyZTB'}], 'invalid_tool_calls': []}]}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: end...\n",
"None\n",
"\n",
"\n",
"\n"
]
}
],
"source": [
"input = {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf\"}]}\n",
"async for chunk in client.runs.stream(\n",
" thread[\"thread_id\"],\n",
" assistant_id,\n",
" input=input,\n",
" stream_mode=\"updates\",\n",
" interrupt_before=[\"action\"],\n",
"):\n",
" print(f\"Receiving new event of type: {chunk.event}...\")\n",
" print(chunk.data)\n",
" print(\"\\n\\n\")"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
@@ -0,0 +1,173 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to Edit State of a Deployed Graph\n",
"\n",
"When creating LangGraph agents, it is often nice to add a human-in-the-loop component. This can be helpful when giving them access to tools. Often in these situations you may want to edit the graph state before continuing (for example, to edit what tool is being called, or how it is being called).\n",
"\n",
"This can be in several ways, but the primary supported way is to add an \"interrupt\" before a node is executed. This interrupts execution at that node. You can then use update_state to update the state, and then resume from that spot to continue.\n",
"\n",
"## Setup\n",
"\n",
"We are not going to show the full code for the graph we are hosting, but you can see it [here](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/edit-graph-state/#build-the-agent) if you want to. Once this graph is hosted, we are ready to invoke it and wait for user input. \n",
"\n",
"### SDK initialization\n",
"\n",
"First, we need to setup our client so that we can communicate with our hosted graph:"
]
},
{
"cell_type": "code",
"execution_count": 37,
"metadata": {},
"outputs": [],
"source": [
"from langgraph_sdk import get_client\n",
"client = get_client()\n",
"assistants = await client.assistants.search()\n",
"assistants = [a for a in assistants if not a['config']]\n",
"assistant = assistants[0]\n",
"thread = await client.threads.create()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Editing state\n",
"\n",
"### Initial invocation\n",
"\n",
"Now let's invoke our graph, making sure to interrupt before the `action` node."
]
},
{
"cell_type": "code",
"execution_count": 38,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'agent': {'messages': [{'content': [{'text': \"Certainly! I'll search for the current weather in San Francisco for you using the search function. Here's how I'll do that:\", 'type': 'text'}, {'id': 'toolu_01KEJMBFozSiZoS4mAcPZeqQ', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-6dbb0167-f8f6-4e2a-ab68-229b2d1fbb64', 'example': False, 'tool_calls': [{'name': 'search', 'args': {'query': 'current weather in San Francisco'}, 'id': 'toolu_01KEJMBFozSiZoS4mAcPZeqQ'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n"
]
}
],
"source": [
"input = {'messages':[{\"role\":\"user\",\"content\":\"search for weather in SF\"}]}\n",
"\n",
"async for chunk in client.runs.stream(\n",
" thread[\"thread_id\"],\n",
" assistant[\"assistant_id\"], # graph_id\n",
" input=input,\n",
" stream_mode=\"updates\",\n",
" interrupt_before=[\"action\"],\n",
"):\n",
" if chunk.data and \"run_id\" not in chunk.data:\n",
" print(chunk.data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Edit the state\n",
"\n",
"Now, let's assume we actually meant to search for the weather in Sidi Frej (another city with the initials SF). We can edit the state to properly reflect that:"
]
},
{
"cell_type": "code",
"execution_count": 39,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'configurable': {'thread_id': '88d58d3f-4151-47a9-a8e0-e42fdd3527b8',\n",
" 'thread_ts': '1ef3274b-a809-6913-8002-91536ce6554d'}}"
]
},
"execution_count": 39,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# First, lets get the current state\n",
"current_state = await client.threads.get_state(thread['thread_id'])\n",
"\n",
"# Let's now get the last message in the state\n",
"# This is the one with the tool calls that we want to update\n",
"last_message = current_state['values']['messages'][-1]\n",
"\n",
"# Let's now update the args for that tool call\n",
"last_message['tool_calls'][0]['args'] = {'query': 'current weather in Sidi Frej'}\n",
"\n",
"# Let's now call `update_state` to pass in this message in the `messages` key\n",
"# This will get treated as any other update to the state\n",
"# It will get passed to the reducer function for the `messages` key\n",
"# That reducer function will use the ID of the message to update it\n",
"# It's important that it has the right ID! Otherwise it would get appended\n",
"# as a new message\n",
"await client.threads.update_state(thread['thread_id'], {\"messages\": last_message})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Resume invocation\n",
"\n",
"Now we can resume our graph run but with the updated state:"
]
},
{
"cell_type": "code",
"execution_count": 40,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'action': {'messages': [{'content': '[\"I looked up: current weather in Sidi Frej. Result: It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini 😈.\"]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': '1161b8d1-bee4-4188-9be8-698aecb69f10', 'tool_call_id': 'toolu_01KEJMBFozSiZoS4mAcPZeqQ'}]}}\n",
"{'agent': {'messages': [{'content': [{'text': 'I apologize for the confusion in my search query. It seems the search function interpreted \"SF\" as \"Sidi Frej\" instead of \"San Francisco\" as we intended. Let me search again with the full city name to get the correct information:', 'type': 'text'}, {'id': 'toolu_0111rrwgfAcmurHZn55qjqTR', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-b8c25779-cfb4-46fc-a421-48553551242f', 'example': False, 'tool_calls': [{'name': 'search', 'args': {'query': 'current weather in San Francisco'}, 'id': 'toolu_0111rrwgfAcmurHZn55qjqTR'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n",
"{'action': {'messages': [{'content': '[\"I looked up: current weather in San Francisco. Result: It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini 😈.\"]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': '6bc632ae-5ee6-4d01-9532-79c524a2d443', 'tool_call_id': 'toolu_0111rrwgfAcmurHZn55qjqTR'}]}}\n",
"{'agent': {'messages': [{'content': \"Now, based on the search results, I can provide you with information about the current weather in San Francisco:\\n\\nThe weather in San Francisco is currently sunny. \\n\\nIt's worth noting that the search result included an unusual comment about Gemini, which doesn't seem directly related to the weather. This might be due to the search engine including some astrological information or a joke in its results. However, for the purpose of weather information, we can focus on the fact that it's sunny in San Francisco right now.\\n\\nIs there anything else you'd like to know about the weather in San Francisco or any other location?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-227a042b-dd97-476e-af32-76a3703af5d8', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n"
]
}
],
"source": [
"async for chunk in client.runs.stream(\n",
" thread[\"thread_id\"],\n",
" assistant[\"assistant_id\"], # graph_id\n",
" input=None,\n",
" stream_mode=\"updates\",\n",
"):\n",
" if chunk.data and \"run_id\" not in chunk.data:\n",
" print(chunk.data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"As you can see it now looks up the current weather in Sidi Frej (although our dummy search node still returns results for SF because we don't actually do a search in this example, we just return the same \"It's sunny in San Francisco ...\" result every time)."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
@@ -0,0 +1,235 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to Replay and Branch from Prior States\n",
"\n",
"With Langgraph Cloud you have the ability to return to any of your prior states and either re-run the graph to reproduce issues noticed during testing, or branch out in a different way from what was originally done in the prior states. In this guide we will show a quick example of how to rerun past states and how to branch off from previous states as well.\n",
"\n",
"## Setup\n",
"\n",
"We are not going to show the full code for the graph we are hosting, but you can see it [here](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/time-travel/#build-the-agent) if you want to. Once this graph is hosted, we are ready to invoke it and wait for user input. \n",
"\n",
"### SDK initialization\n",
"\n",
"First, we need to setup our client so that we can communicate with our hosted graph:"
]
},
{
"cell_type": "code",
"execution_count": 99,
"metadata": {},
"outputs": [],
"source": [
"from langgraph_sdk import get_client\n",
"client = get_client()\n",
"assistants = await client.assistants.search()\n",
"assistants = [a for a in assistants if not a['config']]\n",
"assistant = assistants[0]\n",
"thread = await client.threads.create()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Replay a state\n",
"\n",
"### Initial invocation\n",
"\n",
"Before replaying a state - we need to create states to replay from! In order to do this, let's invoke our graph with a simple message:"
]
},
{
"cell_type": "code",
"execution_count": 100,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'agent': {'messages': [{'content': [{'text': \"Certainly! I'll use the search function to look up the current weather in San Francisco for you. Let me do that now.\", 'type': 'text'}, {'id': 'toolu_011vroKUtWU7SBdrngpgpFMn', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-ee639877-d97d-40f8-96dc-d0d1ae22d203', 'example': False, 'tool_calls': [{'name': 'search', 'args': {'query': 'current weather in San Francisco'}, 'id': 'toolu_011vroKUtWU7SBdrngpgpFMn'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n",
"{'action': {'messages': [{'content': '[\"I looked up: current weather in San Francisco. Result: It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini 😈.\"]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': '7bad0e72-5ebe-4b08-9b8a-b99b0fe22fb7', 'tool_call_id': 'toolu_011vroKUtWU7SBdrngpgpFMn'}]}}\n",
"{'agent': {'messages': [{'content': \"Based on the search results, I can provide you with information about the current weather in San Francisco:\\n\\nThe weather in San Francisco is currently sunny. This is great news for outdoor activities and enjoying the city's beautiful sights.\\n\\nIt's worth noting that the search result included an unusual comment about Geminis, which isn't typically part of a weather report. This might be due to the search engine including some astrological information or a joke in its results. However, for the purpose of answering your question about the weather, we can focus on the fact that it's sunny in San Francisco.\\n\\nIf you need any more specific information about the weather in San Francisco, such as temperature, wind speed, or forecast for the coming days, please let me know, and I'd be happy to search for that information for you.\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-dbac539a-33c8-4f0c-9e20-91f318371e7c', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n"
]
}
],
"source": [
"input = {'messages':[{\"role\":\"user\",\"content\":\"Please search the weather in SF\"}]}\n",
"\n",
"async for chunk in client.runs.stream(\n",
" thread[\"thread_id\"],\n",
" assistant[\"assistant_id\"], # graph_id\n",
" input=input,\n",
" stream_mode=\"updates\",\n",
"):\n",
" if chunk.data and \"run_id\" not in chunk.data:\n",
" print(chunk.data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now let's get our list of states, and invoke from the third state (right before the tool get called):"
]
},
{
"cell_type": "code",
"execution_count": 101,
"metadata": {},
"outputs": [],
"source": [
"states = await client.threads.get_history(thread['thread_id'])"
]
},
{
"cell_type": "code",
"execution_count": 102,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['action']"
]
},
"execution_count": 102,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# We can confirm that this state is correct by checking the 'next' attribute and seeing that it is the tool call node\n",
"state_to_replay = states[2]\n",
"state_to_replay['next']"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To rerun from a state, we need to pass in the `checkpoint_id` into the config of the run like follows:"
]
},
{
"cell_type": "code",
"execution_count": 103,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'action': {'messages': [{'content': '[\"I looked up: current weather in San Francisco. Result: It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini 😈.\"]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': 'eba650e5-400e-4938-8508-f878dcbcc532', 'tool_call_id': 'toolu_011vroKUtWU7SBdrngpgpFMn'}]}}\n",
"{'agent': {'messages': [{'content': \"Based on the search results, I can provide you with information about the current weather in San Francisco:\\n\\nThe weather in San Francisco is currently sunny. This is great news if you're planning any outdoor activities or simply want to enjoy a pleasant day in the city.\\n\\nIt's worth noting that the search result included an unusual comment about Geminis, which doesn't seem directly related to the weather. This appears to be a playful or humorous addition to the weather report, possibly from the source where this information was obtained.\\n\\nIs there anything else you'd like to know about the weather in San Francisco or any other information you need?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-bc6dca3f-a1e2-4f59-a69b-fe0515a348bb', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n"
]
}
],
"source": [
"async for chunk in client.runs.stream(\n",
" thread[\"thread_id\"],\n",
" assistant[\"assistant_id\"], # graph_id\n",
" input=None,\n",
" stream_mode=\"updates\",\n",
" config={\"configurable\":{\"thread_ts\":state_to_replay['checkpoint_id']}}\n",
"):\n",
" if chunk.data and \"run_id\" not in chunk.data:\n",
" print(chunk.data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"As we can see, the graph restarted from the tool node with the same input as our original graph run.\n",
"\n",
"## Branch off from previous state\n",
"\n",
"Using LangGraph's checkpointing, you can do more than just replay past states. You can branch off previous locations to let the agent explore alternate trajectories or to let a user \"version control\" changes in a workflow.\n",
"\n",
"Let's show how to do this to edit the state at a particular point in time. Let's update the state to change the input to the tool"
]
},
{
"cell_type": "code",
"execution_count": 104,
"metadata": {},
"outputs": [],
"source": [
"# Let's now get the last message in the state\n",
"# This is the one with the tool calls that we want to update\n",
"last_message = state_to_replay['values']['messages'][-1]\n",
"\n",
"# Let's now update the args for that tool call\n",
"last_message['tool_calls'][0]['args'] = {'query': 'current weather in SF'}\n",
"\n",
"new_state = await client.threads.update_state(thread['thread_id'],{\"messages\":[last_message]},checkpoint_id=state_to_replay['checkpoint_id'])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now we can rerun our graph with this new config, starting from the `new_state`, which is a branch of our `state_to_replay`:"
]
},
{
"cell_type": "code",
"execution_count": 105,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'action': {'messages': [{'content': '[\"I looked up: current weather in SF. Result: It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini 😈.\"]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': '2baf9941-4fda-4081-9f87-d76795d289f1', 'tool_call_id': 'toolu_011vroKUtWU7SBdrngpgpFMn'}]}}\n",
"{'agent': {'messages': [{'content': \"Based on the search results, I can provide you with information about the current weather in San Francisco (SF):\\n\\nThe weather in San Francisco is currently sunny. This means it's a clear day with plenty of sunshine. \\n\\nIt's worth noting that the specific temperature wasn't provided in the search result, but sunny weather in San Francisco typically means comfortable temperatures. San Francisco is known for its mild climate, so even on sunny days, it's often not too hot.\\n\\nThe search result also included a playful reference to astrological signs, mentioning Gemini. However, this is likely just a joke or part of the search engine's presentation and not related to the actual weather conditions.\\n\\nIs there any specific information about the weather in San Francisco you'd like to know more about? I'd be happy to perform another search if you need details on temperature, wind conditions, or the forecast for the coming days.\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-a83de52d-ed18-4402-9384-75c462485743', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n"
]
}
],
"source": [
"async for chunk in client.runs.stream(\n",
" thread[\"thread_id\"],\n",
" assistant[\"assistant_id\"], # graph_id\n",
" input=None,\n",
" stream_mode=\"updates\",\n",
" config={\"configurable\":{\"thread_ts\":new_state['configurable']['thread_ts']}}\n",
"):\n",
" if chunk.data and \"run_id\" not in chunk.data:\n",
" print(chunk.data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"As we can see, the search query changed from San Francisco to SF, just as we had hoped!"
]
}
],
"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.9"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
@@ -0,0 +1,175 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to Wait for User Input\n",
"\n",
"One of the main human-in-the-loop interaction patterns is waiting for human input. A key use case involves asking the user clarifying questions. One way to accomplish this is simply go to the `END` node and exit the graph. Then, any user response comes back in as fresh invocation of the graph. This is basically just creating a chatbot architecture.\n",
"\n",
"The issue with this is it is tough to resume back in a particular point in the graph. Often times the agent is halfway through some process, and just needs a bit of a user input. Although it is possible to design your graph in such a way where you have a `conditional_entry_point` to route user messages back to the right place, that is not super scalable (as it essentially involves having a routing function that can end up almost anywhere).\n",
"\n",
"A separate way to do this is to have a node explicitly for getting user input. This is easy to implement in a notebook setting - you just put an `input()` call in the node. But that isn't exactly production ready.\n",
"\n",
"Luckily, LangGraph makes it possible to do similar things in a production way. The basic idea is:\n",
"\n",
"- Set up a node that represents human input. This can have specific incoming/outgoing edges (as you desire). There shouldn't actually be any logic inside this node.\n",
"- Add a breakpoint before the node. This will stop the graph before this node executes (which is good, because there's no real logic in it anyways)\n",
"- Use `.update_state` to update the state of the graph. Pass in whatever human response you get. The key here is to use the `as_node` parameter to apply this update **as if you were that node**. This will have the effect of making it so that when you resume execution next it resumes as if that node just acted, and not from the beginning."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"We are not going to show the full code for the graph we are hosting, but you can see it [here](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/wait-user-input/#build-the-agent) if you want to. Once this graph is hosted, we are ready to invoke it and wait for user input. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### SDK initialization\n",
"\n",
"First, we need to setup our client so that we can communicate with our hosted graph:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from langgraph_sdk import get_client\n",
"client = get_client()\n",
"assistants = await client.assistants.search()\n",
"assistants = [a for a in assistants if not a['config']]\n",
"assistant = assistants[0]\n",
"thread = await client.threads.create()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Waiting for user input\n",
"\n",
"### Initial invocation\n",
"\n",
"Now, let's invoke our graph by interrupting before `ask_human` node:"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'agent': {'messages': [{'content': [{'text': \"Certainly! I'll use the AskHuman function to ask the user about their location, and then I'll use the search function to look up the weather for that location. Let's start by asking the user where they are.\", 'type': 'text'}, {'id': 'toolu_01RFahzYPvnPWTb2USk2RdKR', 'input': {'question': 'Where are you currently located?'}, 'name': 'AskHuman', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-a8422215-71d3-4093-afb4-9db141c94ddb', 'example': False, 'tool_calls': [{'name': 'AskHuman', 'args': {'question': 'Where are you currently located?'}, 'id': 'toolu_01RFahzYPvnPWTb2USk2RdKR'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n"
]
}
],
"source": [
"input = {'messages':[{\"role\":\"user\",\"content\":\"Use the search tool to ask the user where they are, then look up the weather there\"}]}\n",
"\n",
"async for chunk in client.runs.stream(\n",
" thread[\"thread_id\"],\n",
" assistant[\"assistant_id\"], # graph_id\n",
" input=input,\n",
" stream_mode=\"updates\",\n",
" interrupt_before=[\"ask_human\"],\n",
"):\n",
" if chunk.data and \"run_id\" not in chunk.data:\n",
" print(chunk.data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Adding user input to state\n",
"\n",
"We now want to update this thread with a response from the user. We then can kick off another run.\n",
"\n",
"Because we are treating this as a tool call, we will need to update the state as if it is a response from a tool call. In order to do this, we will need to check the state to get the ID of the tool call."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'configurable': {'thread_id': '10d0ee61-db47-48fc-a58c-109a1e68cd73',\n",
" 'thread_ts': '1ef32729-3cc3-6647-8002-14dcb621b46e'}}"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"state = await client.threads.get_state(thread['thread_id'])\n",
"tool_call_id = state['values']['messages'][-1]['tool_calls'][0]['id']\n",
"\n",
"# We now create the tool call with the id and the response we want\n",
"tool_message = [{\"tool_call_id\": tool_call_id, \"type\": \"tool\", \"content\": \"san francisco\"}]\n",
"\n",
"await client.threads.update_state(thread['thread_id'], {\"messages\": tool_message}, as_node=\"ask_human\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Invoking after receiving human input\n",
"\n",
"We can now tell the agent to continue. We can just pass in None as the input to the graph, since no additional input is needed:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'agent': {'messages': [{'content': [{'text': \"Thank you for letting me know that you're in San Francisco. Now, I'll use the search function to look up the weather in San Francisco.\", 'type': 'text'}, {'id': 'toolu_01K57ofmgG2wyJ8tYJjbq5k7', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-241baed7-db5e-44ce-ac3c-56431705c22b', 'example': False, 'tool_calls': [{'name': 'search', 'args': {'query': 'current weather in San Francisco'}, 'id': 'toolu_01K57ofmgG2wyJ8tYJjbq5k7'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n",
"{'action': {'messages': [{'content': '[\"I looked up: current weather in San Francisco. Result: It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini 😈.\"]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': '8b699b95-8546-4557-8e66-14ea71a15ed8', 'tool_call_id': 'toolu_01K57ofmgG2wyJ8tYJjbq5k7'}]}}\n",
"{'agent': {'messages': [{'content': \"Based on the search results, I can provide you with information about the current weather in San Francisco:\\n\\nThe weather in San Francisco is currently sunny. It's a beautiful day in the city! \\n\\nHowever, I should note that the search result included an unusual comment about Gemini zodiac signs. This appears to be either a joke or potentially irrelevant information added by the search engine. For accurate and detailed weather information, you might want to check a reliable weather service or app for San Francisco.\\n\\nIs there anything else you'd like to know about the weather or San Francisco?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-b4d7309f-f849-46aa-b6ef-475bcabd2be9', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n"
]
}
],
"source": [
"async for chunk in client.runs.stream(\n",
" thread[\"thread_id\"],\n",
" assistant[\"assistant_id\"], # graph_id\n",
" input=None,\n",
" stream_mode=\"updates\",\n",
"):\n",
" if chunk.data and \"run_id\" not in chunk.data:\n",
" print(chunk.data)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

@@ -6,7 +6,9 @@
"source": [
"## Interrupt\n",
"\n",
"There are several strategies for handling concurrent runs in your graph. This notebook covers how to use the `interrupt` option - please see the other how-to guides in the \"Double Texting\" directory to learn about the other methods.\n",
"This notebook assumes knowledge of what double-texting is, which you can learn about in the [double-texting conceptual guide](https://langchain-ai.github.io/langgraph/cloud/concepts/#double-texting).\n",
"\n",
"The guide covers the `interrupt` option for double texting, which interrupts the prior run of the graph and starts a new one with the double-text. This option does not delete the first run, but rather keeps it in the database but sets its status to `interrupted`. Below is a quick example of using the `interrupt` option.\n",
"\n",
"First, let's import our required packages and instantiate our client, assistant, and thread."
]
@@ -30,7 +32,7 @@
"outputs": [],
"source": [
"client = get_client()\n",
"assistant = await client.assistants.create(\"agent\")\n",
"assistant_id = \"agent\"\n",
"thread = await client.threads.create()"
]
},
@@ -43,13 +45,13 @@
"# the first run will be interrupted\n",
"interrupted_run = await client.runs.create(\n",
" thread[\"thread_id\"],\n",
" assistant[\"assistant_id\"],\n",
" assistant_id,\n",
" input={\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf?\"}]},\n",
")\n",
"await asyncio.sleep(2)\n",
"run = await client.runs.create(\n",
" thread[\"thread_id\"],\n",
" assistant[\"assistant_id\"],\n",
" assistant_id,\n",
" input={\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in nyc?\"}]},\n",
" multitask_strategy=\"interrupt\",\n",
")"
@@ -166,10 +168,14 @@
}
],
"metadata": {
"kernelspec": {
"display_name": "",
"name": ""
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 2
"nbformat_minor": 4
}
File diff suppressed because it is too large Load Diff
+15 -23
View File
@@ -6,7 +6,9 @@
"source": [
"## Reject\n",
"\n",
"There are several strategies for handling concurrent runs in your graph. This notebook covers how to use the `reject` option - please see the other how-to guides in the \"Double Texting\" directory to learn about the other methods.\n",
"This notebook assumes knowledge of what double-texting is, which you can learn about in the [double-texting conceptual guide](https://langchain-ai.github.io/langgraph/cloud/concepts/#double-texting).\n",
"\n",
"The guide covers the `reject` option for double texting, which rejects the new run of the graph by throwing an error and continues with the original run until completion. Below is a quick example of using the `reject` option.\n",
"\n",
"First, let's import our required packages and instantiate our client, assistant, and thread."
]
@@ -28,24 +30,8 @@
"metadata": {},
"outputs": [],
"source": [
"client = get_client()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"assistant = await client.assistants.create(\"agent\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"client = get_client()\n",
"assistant_id = \"agent\"\n",
"thread = await client.threads.create()"
]
},
@@ -57,7 +43,7 @@
"source": [
"run = await client.runs.create(\n",
" thread[\"thread_id\"],\n",
" assistant[\"assistant_id\"],\n",
" assistant_id,\n",
" input={\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf?\"}]},\n",
")"
]
@@ -80,8 +66,10 @@
"try:\n",
" await client.runs.create(\n",
" thread[\"thread_id\"],\n",
" assistant[\"assistant_id\"],\n",
" input={\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in nyc?\"}]},\n",
" assistant_id,\n",
" input={\n",
" \"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in nyc?\"}]\n",
" },\n",
" multitask_strategy=\"reject\",\n",
" )\n",
"except httpx.HTTPStatusError as e:\n",
@@ -163,10 +151,14 @@
}
],
"metadata": {
"kernelspec": {
"display_name": "",
"name": ""
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 2
"nbformat_minor": 4
}
@@ -6,7 +6,9 @@
"source": [
"## Rollback\n",
"\n",
"There are several strategies for handling concurrent runs in your graph. This notebook covers how to use the `rollback` option - please see the other how-to guides in the \"Double Texting\" directory to learn about the other methods.\n",
"This notebook assumes knowledge of what double-texting is, which you can learn about in the [double-texting conceptual guide](https://langchain-ai.github.io/langgraph/cloud/concepts/#double-texting).\n",
"\n",
"The guide covers the `rollback` option for double texting, which interrupts the prior run of the graph and starts a new one with the double-text. This option is very similar to the `interrupt` option, but in this case the first run is completely deleted from the database and cannot be restarted. Below is a quick example of using the `rollback` option.\n",
"\n",
"First, let's import our required packages and instantiate our client, assistant, and thread."
]
@@ -31,7 +33,7 @@
"outputs": [],
"source": [
"client = get_client()\n",
"assistant = await client.assistants.create(\"agent\")\n",
"assistant_id = \"agent\"\n",
"thread = await client.threads.create()"
]
},
@@ -44,13 +46,13 @@
"# the first run will be interrupted\n",
"rolled_back_run = await client.runs.create(\n",
" thread[\"thread_id\"],\n",
" assistant[\"assistant_id\"],\n",
" assistant_id,\n",
" input={\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf?\"}]},\n",
")\n",
"await asyncio.sleep(2)\n",
"run = await client.runs.create(\n",
" thread[\"thread_id\"],\n",
" assistant[\"assistant_id\"],\n",
" assistant_id,\n",
" input={\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in nyc?\"}]},\n",
" multitask_strategy=\"rollback\",\n",
")"
@@ -146,10 +148,14 @@
}
],
"metadata": {
"kernelspec": {
"display_name": "",
"name": ""
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 2
"nbformat_minor": 4
}
+26 -10
View File
@@ -7,9 +7,13 @@
"source": [
"# How to run multiple agents on the same thread\n",
"\n",
"In LangGraph API, a thread is not explicitly associated with a particular agent.\n",
"This means that you can run multiple agents on the same thread.\n",
"In this example, we will create two agents and then call them both on the same thread."
"In LangGraph Cloud, a thread is not explicitly associated with a particular agent.\n",
"This means that you can run multiple agents on the same thread, which allows a different\n",
"agent to continue from an initial agent's progress.\n",
"\n",
"In this example, we will create two agents and then call them both on the same thread.\n",
"You'll see that the second agent will respond using information from the [checkpoint](https://langchain-ai.github.io/langgraph/concepts/low_level/#checkpointer-state) generated in the thread\n",
"by the first agent as context."
]
},
{
@@ -23,11 +27,13 @@
"\n",
"client = get_client()\n",
"\n",
"openai_assistant = await client.assistants.create(graph_id=\"agent\", config={\"configurable\": {\"model_name\": \"openai\"}})\n",
"openai_assistant = await client.assistants.create(\n",
" graph_id=\"agent\", config={\"configurable\": {\"model_name\": \"openai\"}}\n",
")\n",
"\n",
"# There should always be a default assistant with no configuration\n",
"assistants = await client.assistants.search()\n",
"default_assistant = [a for a in assistants if not a['config']][0]"
"default_assistant = [a for a in assistants if not a[\"config\"]][0]"
]
},
{
@@ -35,7 +41,7 @@
"id": "4f10d346-69e6-44f4-8ff0-ef539ba938df",
"metadata": {},
"source": [
"We can see that these agents are different"
"We can see that these agents are different:"
]
},
{
@@ -95,7 +101,7 @@
"id": "5e655e61-c2ee-488a-90f6-6189c84841da",
"metadata": {},
"source": [
"We can now run it on the OpenAI assistant first."
"We can now run the OpenAI assistant on the thread first."
]
},
{
@@ -117,7 +123,12 @@
"source": [
"thread = await client.threads.create()\n",
"input = {\"messages\": [{\"role\": \"user\", \"content\": \"who made you?\"}]}\n",
"async for event in client.runs.stream(thread['thread_id'], openai_assistant['assistant_id'], input=input, stream_mode='updates'):\n",
"async for event in client.runs.stream(\n",
" thread[\"thread_id\"],\n",
" openai_assistant[\"assistant_id\"],\n",
" input=input,\n",
" stream_mode=\"updates\",\n",
"):\n",
" print(event)"
]
},
@@ -126,7 +137,7 @@
"id": "c53709e9-ddb2-4429-9042-456eb6c91244",
"metadata": {},
"source": [
"Now, we can run it on a different Anthropic-based assistant."
"Now, we can run it on a second Anthropic-based assistant and see that this second assistant is aware of the initial question, and can answer the question, `and you?`:"
]
},
{
@@ -147,7 +158,12 @@
],
"source": [
"input = {\"messages\": [{\"role\": \"user\", \"content\": \"and you?\"}]}\n",
"async for event in client.runs.stream(thread['thread_id'], default_assistant['assistant_id'], input=input, stream_mode='updates'):\n",
"async for event in client.runs.stream(\n",
" thread[\"thread_id\"],\n",
" default_assistant[\"assistant_id\"],\n",
" input=input,\n",
" stream_mode=\"updates\",\n",
"):\n",
" print(event)"
]
},
@@ -0,0 +1,147 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Stateless Runs\n",
"\n",
"Most of the time, you provide a `thread_id` to your client when you run your graph in order to keep track of prior runs through the persistent state implemented in LangGraph Cloud. However, if you have your own database to save runs and don't need to use the built in persistent state, you can create stateless runs.\n",
"\n",
"## Setup\n",
"\n",
"First, let's setup our client"
]
},
{
"cell_type": "code",
"execution_count": 106,
"metadata": {},
"outputs": [],
"source": [
"from langgraph_sdk import get_client\n",
"client = get_client()\n",
"assistants = await client.assistants.search()\n",
"assistants = [a for a in assistants if not a['config']]\n",
"assistant = assistants[0]\n",
"thread = await client.threads.create()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Stateless streaming\n",
"\n",
"We can stream the results of a stateless run in an almost identical fashion to how we stream from a run with the state attribute, but instead of passing a value to the `thread_id` parameter, we pass `None`:"
]
},
{
"cell_type": "code",
"execution_count": 107,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'agent': {'messages': [{'content': \"Hello Bagatur! It's nice to meet you. Thank you for introducing yourself and sharing your age. Is there anything specific you'd like to know or discuss? I'm here to help with any questions or topics you're interested in.\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-489ec573-1645-4ce2-a3b8-91b391d50a71', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n"
]
}
],
"source": [
"input = {\"messages\":[{\"role\": \"user\", \"content\": \"Hello! My name is Bagatur and I am 26 years old.\"}]}\n",
"\n",
"\n",
"async for chunk in client.runs.stream(\n",
" # Don't pass in a thread_id and the stream will be stateless\n",
" None,\n",
" assistant[\"assistant_id\"], # graph_id\n",
" input=input,\n",
" stream_mode=\"updates\",\n",
"):\n",
" if chunk.data and \"run_id\" not in chunk.data:\n",
" print(chunk.data)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Waiting for stateless results\n",
"\n",
"In addition to streaming, you can also wait for a stateless result by using the `.wait` function like follows:"
]
},
{
"cell_type": "code",
"execution_count": 108,
"metadata": {},
"outputs": [],
"source": [
"stateless_run_result = await client.runs.wait(\n",
" None,\n",
" assistant[\"assistant_id\"], # graph_id\n",
" input=input,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 109,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'messages': [{'content': 'Hello! My name is Bagatur and I am 26 years old.',\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {},\n",
" 'type': 'human',\n",
" 'name': None,\n",
" 'id': '5e088543-62c2-43de-9d95-6086ad7f8b48',\n",
" 'example': False},\n",
" {'content': \"Hello Bagatur! It's nice to meet you. Thank you for introducing yourself and sharing your age. Is there anything specific you'd like to know or discuss? I'm here to help with any questions or topics you'd like to explore.\",\n",
" 'additional_kwargs': {},\n",
" 'response_metadata': {},\n",
" 'type': 'ai',\n",
" 'name': None,\n",
" 'id': 'run-d6361e8d-4d4c-45bd-ba47-39520257f773',\n",
" 'example': False,\n",
" 'tool_calls': [],\n",
" 'invalid_tool_calls': [],\n",
" 'usage_metadata': None}]}"
]
},
"execution_count": 109,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"stateless_run_result"
]
}
],
"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.9"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
+176
View File
@@ -0,0 +1,176 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to stream debug events\n",
"This guide covers how to stream debug events from your graph (`stream_mode=\"debug\"`)."
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"from langgraph_sdk import get_client\n",
"\n",
"client = get_client()"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3',\n",
" 'created_at': '2024-06-21T22:10:27.696862+00:00',\n",
" 'updated_at': '2024-06-21T22:10:27.696862+00:00',\n",
" 'metadata': {}}"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# create thread\n",
"thread = await client.threads.create()\n",
"thread"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Streaming debug events produces responses containing `type` and `timestamp` keys. Debug events correspond to different steps in the graph's execution (e.g. `task`, `task_result`, `checkpoint`)."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Receiving new event of type: metadata...\n",
"{'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a'}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'checkpoint', 'timestamp': '2024-06-21T22:11:09.256850+00:00', 'step': -1, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'thread_ts': '1ef301b2-9a2e-6bb6-bfff-8423bcf47561', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a'}, 'values': {'messages': []}, 'metadata': {'source': 'input', 'step': -1, 'writes': {'messages': [{'role': 'human', 'content': \"What's the weather in SF?\"}]}}}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'checkpoint', 'timestamp': '2024-06-21T22:11:09.259723+00:00', 'step': 0, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'thread_ts': '1ef301b2-9a35-6c86-8000-f4a85315dbeb', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a'}, 'values': {'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}]}, 'metadata': {'source': 'loop', 'step': 0, 'writes': None}}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'task', 'timestamp': '2024-06-21T22:11:09.260021+00:00', 'step': 1, 'payload': {'id': '12ab1026-a551-5f96-9ad3-43424f094774', 'name': 'agent', 'input': {'some_bytes': None, 'some_byte_array': None, 'dict_with_bytes': None, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}], 'sleep': None}, 'triggers': ['start:agent']}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'task_result', 'timestamp': '2024-06-21T22:11:09.267632+00:00', 'step': 1, 'payload': {'id': '12ab1026-a551-5f96-9ad3-43424f094774', 'name': 'agent', 'result': [['some_bytes', 'c29tZV9ieXRlcw=='], ['some_byte_array', 'c29tZV9ieXRlX2FycmF5'], ['dict_with_bytes', {'more_bytes': 'bW9yZV9ieXRlcw=='}], ['messages', [{'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]]]}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'checkpoint', 'timestamp': '2024-06-21T22:11:09.268469+00:00', 'step': 1, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'thread_ts': '1ef301b2-9a4b-60ae-8001-dd378f965bf7', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a'}, 'values': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}, 'metadata': {'source': 'loop', 'step': 1, 'writes': {'agent': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}}}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'task', 'timestamp': '2024-06-21T22:11:09.268659+00:00', 'step': 2, 'payload': {'id': '494ad427-fe8d-5654-91e6-50495a2699f5', 'name': 'tool', 'input': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'sleep': None}, 'triggers': ['branch:agent:should_continue:tool']}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'task_result', 'timestamp': '2024-06-21T22:11:09.272916+00:00', 'step': 2, 'payload': {'id': '494ad427-fe8d-5654-91e6-50495a2699f5', 'name': 'tool', 'result': [['messages', [{'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '222ed3b8-450f-41cb-ac40-905def3c700a', 'tool_call_id': 'tool_call_id'}]]]}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'checkpoint', 'timestamp': '2024-06-21T22:11:09.273113+00:00', 'step': 2, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'thread_ts': '1ef301b2-9a56-6832-8002-8ab17e662980', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a'}, 'values': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '222ed3b8-450f-41cb-ac40-905def3c700a', 'tool_call_id': 'tool_call_id'}]}, 'metadata': {'source': 'loop', 'step': 2, 'writes': {'tool': {'messages': [{'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '222ed3b8-450f-41cb-ac40-905def3c700a', 'tool_call_id': 'tool_call_id'}]}}}}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'task', 'timestamp': '2024-06-21T22:11:09.273192+00:00', 'step': 3, 'payload': {'id': '677de327-99b7-5d97-9bbd-0092abb62d46', 'name': 'agent', 'input': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '222ed3b8-450f-41cb-ac40-905def3c700a', 'tool_call_id': 'tool_call_id'}], 'sleep': None}, 'triggers': ['tool']}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'task_result', 'timestamp': '2024-06-21T22:11:09.277262+00:00', 'step': 3, 'payload': {'id': '677de327-99b7-5d97-9bbd-0092abb62d46', 'name': 'agent', 'result': [['some_bytes', 'c29tZV9ieXRlcw=='], ['some_byte_array', 'c29tZV9ieXRlX2FycmF5'], ['dict_with_bytes', {'more_bytes': 'bW9yZV9ieXRlcw=='}], ['messages', [{'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-006e1758-b1ca-4c90-9ff3-d2e75b9ca9a7', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]]]}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'checkpoint', 'timestamp': '2024-06-21T22:11:09.277519+00:00', 'step': 3, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'thread_ts': '1ef301b2-9a61-6462-8003-1316d9875b7f', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a'}, 'values': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '222ed3b8-450f-41cb-ac40-905def3c700a', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-006e1758-b1ca-4c90-9ff3-d2e75b9ca9a7', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}, 'metadata': {'source': 'loop', 'step': 3, 'writes': {'agent': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-006e1758-b1ca-4c90-9ff3-d2e75b9ca9a7', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}}}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: end...\n",
"None\n",
"\n",
"\n",
"\n"
]
}
],
"source": [
"# create input\n",
"input = {\n",
" \"messages\": [\n",
" {\n",
" \"role\": \"human\",\n",
" \"content\": \"What's the weather in SF?\",\n",
" }\n",
" ]\n",
"}\n",
"\n",
"# stream events\n",
"async for chunk in client.runs.stream(\n",
" thread_id=thread[\"thread_id\"],\n",
" assistant_id=\"agent\",\n",
" input=input,\n",
" stream_mode=\"debug\",\n",
"):\n",
" print(f\"Receiving new event of type: {chunk.event}...\")\n",
" print(chunk.data)\n",
" print(\"\\n\\n\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "env",
"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
}
+313
View File
@@ -0,0 +1,313 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to stream events\n",
"This guide covers how to stream events from your graph (`stream_mode=\"events\"`). Depending on the use case and user experience of your LangGraph application, your application may process event types differently."
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [],
"source": [
"from langgraph_sdk import get_client\n",
"\n",
"client = get_client()"
]
},
{
"cell_type": "code",
"execution_count": 27,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'thread_id': '3f4c64e0-f792-4a5e-aa07-a4404e06e0bd',\n",
" 'created_at': '2024-06-24T22:16:29.301522+00:00',\n",
" 'updated_at': '2024-06-24T22:16:29.301522+00:00',\n",
" 'metadata': {}}"
]
},
"execution_count": 27,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# create thread\n",
"thread = await client.threads.create()\n",
"thread"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Streaming events produces responses containing an `event` key (in addition to other keys such as `data`). See the LangChain [`Runnable.astream_events()` reference](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.Runnable.html#langchain_core.runnables.base.Runnable.astream_events) for all event types."
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Receiving new event of type: metadata...\n",
"{'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8'}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_start', 'data': {'input': {'messages': [{'role': 'human', 'content': \"What's the weather in SF?\"}]}}, 'name': 'LangGraph', 'tags': [], 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'parent_ids': []}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_start', 'data': {}, 'name': 'agent', 'tags': ['graph:step:6'], 'run_id': '7bb08493-d507-4e28-b9e6-4a5eda9d04f0', 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 6, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_start', 'data': {'input': {'messages': [[{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '51f2874d-f8c7-4040-8b3b-8f15429a56ae', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-5f556aa0-26ea-42e2-b9e4-7ece3a00974e', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '1faf5dd0-ae97-4235-963f-5075083a027a', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-ae383611-6a42-475a-912a-09d5972e9e94', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c67e08e6-e7af-4c4a-aa5e-50c8340ae341', 'example': False}]]}}, 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'run_id': 'cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 6, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8', '7bb08493-d507-4e28-b9e6-4a5eda9d04f0']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_stream', 'data': {'chunk': {'content': 'b', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'AIMessageChunk', 'name': None, 'id': 'run-cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None, 'tool_call_chunks': []}}, 'run_id': 'cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 6, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8', '7bb08493-d507-4e28-b9e6-4a5eda9d04f0']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_stream', 'data': {'chunk': {'content': 'e', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'AIMessageChunk', 'name': None, 'id': 'run-cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None, 'tool_call_chunks': []}}, 'run_id': 'cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 6, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8', '7bb08493-d507-4e28-b9e6-4a5eda9d04f0']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_stream', 'data': {'chunk': {'content': 'g', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'AIMessageChunk', 'name': None, 'id': 'run-cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None, 'tool_call_chunks': []}}, 'run_id': 'cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 6, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8', '7bb08493-d507-4e28-b9e6-4a5eda9d04f0']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_stream', 'data': {'chunk': {'content': 'i', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'AIMessageChunk', 'name': None, 'id': 'run-cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None, 'tool_call_chunks': []}}, 'run_id': 'cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 6, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8', '7bb08493-d507-4e28-b9e6-4a5eda9d04f0']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_stream', 'data': {'chunk': {'content': 'n', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'AIMessageChunk', 'name': None, 'id': 'run-cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None, 'tool_call_chunks': []}}, 'run_id': 'cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 6, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8', '7bb08493-d507-4e28-b9e6-4a5eda9d04f0']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_end', 'data': {'output': {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, 'input': {'messages': [[{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '51f2874d-f8c7-4040-8b3b-8f15429a56ae', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-5f556aa0-26ea-42e2-b9e4-7ece3a00974e', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '1faf5dd0-ae97-4235-963f-5075083a027a', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-ae383611-6a42-475a-912a-09d5972e9e94', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c67e08e6-e7af-4c4a-aa5e-50c8340ae341', 'example': False}]]}}, 'run_id': 'cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 6, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8', '7bb08493-d507-4e28-b9e6-4a5eda9d04f0']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_start', 'data': {'input': {'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '51f2874d-f8c7-4040-8b3b-8f15429a56ae', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-5f556aa0-26ea-42e2-b9e4-7ece3a00974e', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '1faf5dd0-ae97-4235-963f-5075083a027a', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-ae383611-6a42-475a-912a-09d5972e9e94', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c67e08e6-e7af-4c4a-aa5e-50c8340ae341', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}}}, 'name': 'should_continue', 'tags': ['seq:step:3'], 'run_id': 'c7fe4d2d-3fb8-4e53-946d-03de13527853', 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 6, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8', '7bb08493-d507-4e28-b9e6-4a5eda9d04f0']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_end', 'data': {'output': 'tool', 'input': {'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '51f2874d-f8c7-4040-8b3b-8f15429a56ae', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-5f556aa0-26ea-42e2-b9e4-7ece3a00974e', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '1faf5dd0-ae97-4235-963f-5075083a027a', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-ae383611-6a42-475a-912a-09d5972e9e94', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c67e08e6-e7af-4c4a-aa5e-50c8340ae341', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}}}, 'run_id': 'c7fe4d2d-3fb8-4e53-946d-03de13527853', 'name': 'should_continue', 'tags': ['seq:step:3'], 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 6, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8', '7bb08493-d507-4e28-b9e6-4a5eda9d04f0']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': '7bb08493-d507-4e28-b9e6-4a5eda9d04f0', 'name': 'agent', 'tags': ['graph:step:6'], 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 6, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0}, 'data': {'chunk': {'messages': [{'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}}}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_end', 'data': {'output': {'messages': [{'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}}, 'input': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '51f2874d-f8c7-4040-8b3b-8f15429a56ae', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-5f556aa0-26ea-42e2-b9e4-7ece3a00974e', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '1faf5dd0-ae97-4235-963f-5075083a027a', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-ae383611-6a42-475a-912a-09d5972e9e94', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c67e08e6-e7af-4c4a-aa5e-50c8340ae341', 'example': False}], 'sleep': None}}, 'run_id': '7bb08493-d507-4e28-b9e6-4a5eda9d04f0', 'name': 'agent', 'tags': ['graph:step:6'], 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 6, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_start', 'data': {}, 'name': 'tool', 'tags': ['graph:step:7'], 'run_id': 'f044fd3d-7271-488f-b8aa-e01572ff9112', 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 7, 'langgraph_node': 'tool', 'langgraph_triggers': ['branch:agent:should_continue:tool'], 'langgraph_task_idx': 0}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': 'f044fd3d-7271-488f-b8aa-e01572ff9112', 'name': 'tool', 'tags': ['graph:step:7'], 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 7, 'langgraph_node': 'tool', 'langgraph_triggers': ['branch:agent:should_continue:tool'], 'langgraph_task_idx': 0}, 'data': {'chunk': {'messages': [{'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': None, 'tool_call_id': 'tool_call_id'}]}}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_end', 'data': {'output': {'messages': [{'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '1c9a16d2-5f0a-4eba-a0d2-240484a4ce7e', 'tool_call_id': 'tool_call_id'}]}, 'input': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '51f2874d-f8c7-4040-8b3b-8f15429a56ae', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-5f556aa0-26ea-42e2-b9e4-7ece3a00974e', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '1faf5dd0-ae97-4235-963f-5075083a027a', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-ae383611-6a42-475a-912a-09d5972e9e94', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c67e08e6-e7af-4c4a-aa5e-50c8340ae341', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'sleep': None}}, 'run_id': 'f044fd3d-7271-488f-b8aa-e01572ff9112', 'name': 'tool', 'tags': ['graph:step:7'], 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 7, 'langgraph_node': 'tool', 'langgraph_triggers': ['branch:agent:should_continue:tool'], 'langgraph_task_idx': 0}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_start', 'data': {}, 'name': 'agent', 'tags': ['graph:step:8'], 'run_id': '1f4f95d0-0ce1-4061-85d4-946446bbd3e5', 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 8, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_start', 'data': {'input': {'messages': [[{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '51f2874d-f8c7-4040-8b3b-8f15429a56ae', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-5f556aa0-26ea-42e2-b9e4-7ece3a00974e', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '1faf5dd0-ae97-4235-963f-5075083a027a', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-ae383611-6a42-475a-912a-09d5972e9e94', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c67e08e6-e7af-4c4a-aa5e-50c8340ae341', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '1c9a16d2-5f0a-4eba-a0d2-240484a4ce7e', 'tool_call_id': 'tool_call_id'}]]}}, 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'run_id': '028a68fb-6435-4b46-a156-c3326f73985c', 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 8, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8', '1f4f95d0-0ce1-4061-85d4-946446bbd3e5']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_stream', 'data': {'chunk': {'content': 'e', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'AIMessageChunk', 'name': None, 'id': 'run-028a68fb-6435-4b46-a156-c3326f73985c', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None, 'tool_call_chunks': []}}, 'run_id': '028a68fb-6435-4b46-a156-c3326f73985c', 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 8, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8', '1f4f95d0-0ce1-4061-85d4-946446bbd3e5']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_stream', 'data': {'chunk': {'content': 'n', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'AIMessageChunk', 'name': None, 'id': 'run-028a68fb-6435-4b46-a156-c3326f73985c', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None, 'tool_call_chunks': []}}, 'run_id': '028a68fb-6435-4b46-a156-c3326f73985c', 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 8, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8', '1f4f95d0-0ce1-4061-85d4-946446bbd3e5']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_stream', 'data': {'chunk': {'content': 'd', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'AIMessageChunk', 'name': None, 'id': 'run-028a68fb-6435-4b46-a156-c3326f73985c', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None, 'tool_call_chunks': []}}, 'run_id': '028a68fb-6435-4b46-a156-c3326f73985c', 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 8, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8', '1f4f95d0-0ce1-4061-85d4-946446bbd3e5']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_end', 'data': {'output': {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-028a68fb-6435-4b46-a156-c3326f73985c', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, 'input': {'messages': [[{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '51f2874d-f8c7-4040-8b3b-8f15429a56ae', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-5f556aa0-26ea-42e2-b9e4-7ece3a00974e', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '1faf5dd0-ae97-4235-963f-5075083a027a', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-ae383611-6a42-475a-912a-09d5972e9e94', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c67e08e6-e7af-4c4a-aa5e-50c8340ae341', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '1c9a16d2-5f0a-4eba-a0d2-240484a4ce7e', 'tool_call_id': 'tool_call_id'}]]}}, 'run_id': '028a68fb-6435-4b46-a156-c3326f73985c', 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 8, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8', '1f4f95d0-0ce1-4061-85d4-946446bbd3e5']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_start', 'data': {'input': {'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '51f2874d-f8c7-4040-8b3b-8f15429a56ae', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-5f556aa0-26ea-42e2-b9e4-7ece3a00974e', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '1faf5dd0-ae97-4235-963f-5075083a027a', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-ae383611-6a42-475a-912a-09d5972e9e94', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c67e08e6-e7af-4c4a-aa5e-50c8340ae341', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '1c9a16d2-5f0a-4eba-a0d2-240484a4ce7e', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-028a68fb-6435-4b46-a156-c3326f73985c', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}}}, 'name': 'should_continue', 'tags': ['seq:step:3'], 'run_id': 'f2b2dfaf-475d-422b-8bf5-02a31bcc7d1a', 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 8, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8', '1f4f95d0-0ce1-4061-85d4-946446bbd3e5']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_end', 'data': {'output': '__end__', 'input': {'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '51f2874d-f8c7-4040-8b3b-8f15429a56ae', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-5f556aa0-26ea-42e2-b9e4-7ece3a00974e', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '1faf5dd0-ae97-4235-963f-5075083a027a', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-ae383611-6a42-475a-912a-09d5972e9e94', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c67e08e6-e7af-4c4a-aa5e-50c8340ae341', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '1c9a16d2-5f0a-4eba-a0d2-240484a4ce7e', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-028a68fb-6435-4b46-a156-c3326f73985c', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}}}, 'run_id': 'f2b2dfaf-475d-422b-8bf5-02a31bcc7d1a', 'name': 'should_continue', 'tags': ['seq:step:3'], 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 8, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8', '1f4f95d0-0ce1-4061-85d4-946446bbd3e5']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': '1f4f95d0-0ce1-4061-85d4-946446bbd3e5', 'name': 'agent', 'tags': ['graph:step:8'], 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 8, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0}, 'data': {'chunk': {'messages': [{'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-028a68fb-6435-4b46-a156-c3326f73985c', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}}}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_end', 'data': {'output': {'messages': [{'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-028a68fb-6435-4b46-a156-c3326f73985c', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}}, 'input': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '51f2874d-f8c7-4040-8b3b-8f15429a56ae', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-5f556aa0-26ea-42e2-b9e4-7ece3a00974e', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '1faf5dd0-ae97-4235-963f-5075083a027a', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-ae383611-6a42-475a-912a-09d5972e9e94', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c67e08e6-e7af-4c4a-aa5e-50c8340ae341', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '1c9a16d2-5f0a-4eba-a0d2-240484a4ce7e', 'tool_call_id': 'tool_call_id'}], 'sleep': None}}, 'run_id': '1f4f95d0-0ce1-4061-85d4-946446bbd3e5', 'name': 'agent', 'tags': ['graph:step:8'], 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 8, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0}, 'parent_ids': ['1ef301a5-b867-67de-9e9e-a32e53c5b1f8']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_end', 'data': {'output': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '51f2874d-f8c7-4040-8b3b-8f15429a56ae', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-5f556aa0-26ea-42e2-b9e4-7ece3a00974e', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '1faf5dd0-ae97-4235-963f-5075083a027a', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-ae383611-6a42-475a-912a-09d5972e9e94', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c67e08e6-e7af-4c4a-aa5e-50c8340ae341', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-cb1b98c1-c9e2-4a30-9d7a-38fa1f6224bd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '1c9a16d2-5f0a-4eba-a0d2-240484a4ce7e', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-028a68fb-6435-4b46-a156-c3326f73985c', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}, 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'name': 'LangGraph', 'tags': [], 'metadata': {'graph_id': 'agent', 'created_by': 'system', 'run_id': '1ef301a5-b867-67de-9e9e-a32e53c5b1f8', 'user_id': '', 'thread_id': '7196a3aa-763c-4a8d-bfda-12fbfe1cd727', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'parent_ids': []}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: end...\n",
"None\n",
"\n",
"\n",
"\n"
]
}
],
"source": [
"# create input\n",
"input = {\n",
" \"messages\": [\n",
" {\n",
" \"role\": \"human\",\n",
" \"content\": \"What's the weather in SF?\",\n",
" }\n",
" ]\n",
"}\n",
"\n",
"# stream events\n",
"async for chunk in client.runs.stream(\n",
" thread_id=thread[\"thread_id\"],\n",
" assistant_id=\"agent\",\n",
" input=input,\n",
" stream_mode=\"events\",\n",
"):\n",
" print(f\"Receiving new event of type: {chunk.event}...\")\n",
" print(chunk.data)\n",
" print(\"\\n\\n\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Token-by-Token Streaming\n",
"\n",
"Token-by-token streaming can be implemented with the `events` streaming mode. The `on_chat_model_stream` event type should be processed to stream LLM responses token-by-token."
]
},
{
"cell_type": "code",
"execution_count": 28,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"b\n",
"be\n",
"beg\n",
"begi\n",
"begin\n",
"begine\n",
"beginen\n",
"beginend\n"
]
}
],
"source": [
"# create input\n",
"input = {\n",
" \"messages\": [\n",
" {\n",
" \"role\": \"human\",\n",
" \"content\": \"What's the weather in SF?\",\n",
" }\n",
" ]\n",
"}\n",
"\n",
"llm_response = \"\"\n",
"\n",
"# stream token-by-token\n",
"async for chunk in client.runs.stream(\n",
" thread_id=thread[\"thread_id\"],\n",
" assistant_id=\"agent\",\n",
" input=input,\n",
" stream_mode=\"events\",\n",
"):\n",
" if (\n",
" chunk.event == \"events\" and\n",
" chunk.data[\"event\"] == \"on_chat_model_stream\"\n",
" ):\n",
" llm_response += chunk.data[\"data\"][\"chunk\"][\"content\"]\n",
" print(llm_response)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "env",
"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
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,457 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# How to configure multiple streaming modes at the same time\n",
"\n",
"This guide covers how to configure multiple streaming modes at the same time."
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"from langgraph_sdk import get_client\n",
"\n",
"client = get_client()"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4',\n",
" 'created_at': '2024-06-24T21:30:07.980789+00:00',\n",
" 'updated_at': '2024-06-24T21:30:07.980789+00:00',\n",
" 'metadata': {}}"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# create thread\n",
"thread = await client.threads.create()\n",
"thread"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"When configuring multiple streaming modes for a run, responses for each respective mode will be produced. In the following example, note that a `list` of modes (`messages`, `events`, `debug`) is passed to the `stream_mode` parameter and the response contains `events`, `debug`, `messages/complete`, `messages/metadata`, and `messages/partial` event types."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Receiving new event of type: metadata...\n",
"{'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25'}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_start', 'data': {'input': {'messages': [{'role': 'human', 'content': \"What's the weather in SF?\"}]}}, 'name': 'LangGraph', 'tags': [], 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'parent_ids': []}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'checkpoint', 'timestamp': '2024-06-24T21:34:06.116009+00:00', 'step': -1, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'thread_ts': '1ef32717-bc7c-6daa-bfff-6b9027c1a50e', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25'}, 'values': {'messages': []}, 'metadata': {'source': 'input', 'step': -1, 'writes': {'messages': [{'role': 'human', 'content': \"What's the weather in SF?\"}]}}}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'name': 'LangGraph', 'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'data': {'chunk': ['debug', {'type': 'checkpoint', 'timestamp': '2024-06-24T21:34:06.116009+00:00', 'step': -1, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'thread_ts': '1ef32717-bc7c-6daa-bfff-6b9027c1a50e', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25'}, 'values': {'messages': []}, 'metadata': {'source': 'input', 'step': -1, 'writes': {'messages': [{'role': 'human', 'content': \"What's the weather in SF?\"}]}}}}]}, 'parent_ids': []}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'name': 'LangGraph', 'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'data': {'chunk': ['values', {'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}]}]}, 'parent_ids': []}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: messages/complete...\n",
"[{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}]\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'checkpoint', 'timestamp': '2024-06-24T21:34:06.117924+00:00', 'step': 0, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'thread_ts': '1ef32717-bc81-68c8-8000-4e18ae7d67a5', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25'}, 'values': {'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}]}, 'metadata': {'source': 'loop', 'step': 0, 'writes': None}}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'name': 'LangGraph', 'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'data': {'chunk': ['debug', {'type': 'checkpoint', 'timestamp': '2024-06-24T21:34:06.117924+00:00', 'step': 0, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'thread_ts': '1ef32717-bc81-68c8-8000-4e18ae7d67a5', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25'}, 'values': {'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}]}, 'metadata': {'source': 'loop', 'step': 0, 'writes': None}}}]}, 'parent_ids': []}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'task', 'timestamp': '2024-06-24T21:34:06.118042+00:00', 'step': 1, 'payload': {'id': '212ed9c2-a454-50c5-a202-12066bbbe7b8', 'name': 'agent', 'input': {'some_bytes': None, 'some_byte_array': None, 'dict_with_bytes': None, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}], 'sleep': None}, 'triggers': ['start:agent']}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'name': 'LangGraph', 'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'data': {'chunk': ['debug', {'type': 'task', 'timestamp': '2024-06-24T21:34:06.118042+00:00', 'step': 1, 'payload': {'id': '212ed9c2-a454-50c5-a202-12066bbbe7b8', 'name': 'agent', 'input': {'some_bytes': None, 'some_byte_array': None, 'dict_with_bytes': None, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}], 'sleep': None}, 'triggers': ['start:agent']}}]}, 'parent_ids': []}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_start', 'data': {}, 'name': 'agent', 'tags': ['graph:step:1'], 'run_id': '72b74d24-5792-48da-a887-102100d6e2c0', 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 1, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_start', 'data': {'input': {'messages': [[{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}]]}}, 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'run_id': '2424dd6d-5cf5-4244-8d98-357640ce6e12', 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 1, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25', '72b74d24-5792-48da-a887-102100d6e2c0']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_stream', 'data': {'chunk': {'content': 'b', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'AIMessageChunk', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None, 'tool_call_chunks': []}}, 'run_id': '2424dd6d-5cf5-4244-8d98-357640ce6e12', 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 1, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25', '72b74d24-5792-48da-a887-102100d6e2c0']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: messages/metadata...\n",
"{'run-2424dd6d-5cf5-4244-8d98-357640ce6e12': {'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 1, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: messages/partial...\n",
"[{'content': 'b', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_stream', 'data': {'chunk': {'content': 'e', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'AIMessageChunk', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None, 'tool_call_chunks': []}}, 'run_id': '2424dd6d-5cf5-4244-8d98-357640ce6e12', 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 1, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25', '72b74d24-5792-48da-a887-102100d6e2c0']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: messages/partial...\n",
"[{'content': 'be', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_stream', 'data': {'chunk': {'content': 'g', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'AIMessageChunk', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None, 'tool_call_chunks': []}}, 'run_id': '2424dd6d-5cf5-4244-8d98-357640ce6e12', 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 1, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25', '72b74d24-5792-48da-a887-102100d6e2c0']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: messages/partial...\n",
"[{'content': 'beg', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_stream', 'data': {'chunk': {'content': 'i', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'AIMessageChunk', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None, 'tool_call_chunks': []}}, 'run_id': '2424dd6d-5cf5-4244-8d98-357640ce6e12', 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 1, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25', '72b74d24-5792-48da-a887-102100d6e2c0']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: messages/partial...\n",
"[{'content': 'begi', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_stream', 'data': {'chunk': {'content': 'n', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'AIMessageChunk', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None, 'tool_call_chunks': []}}, 'run_id': '2424dd6d-5cf5-4244-8d98-357640ce6e12', 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 1, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25', '72b74d24-5792-48da-a887-102100d6e2c0']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: messages/partial...\n",
"[{'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_end', 'data': {'output': {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, 'input': {'messages': [[{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}]]}}, 'run_id': '2424dd6d-5cf5-4244-8d98-357640ce6e12', 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 1, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25', '72b74d24-5792-48da-a887-102100d6e2c0']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_start', 'data': {'input': {'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}}}, 'name': 'should_continue', 'tags': ['seq:step:3'], 'run_id': '227afb0f-f909-4d54-a042-556ca6d98a69', 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 1, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25', '72b74d24-5792-48da-a887-102100d6e2c0']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_end', 'data': {'output': 'tool', 'input': {'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}}}, 'run_id': '227afb0f-f909-4d54-a042-556ca6d98a69', 'name': 'should_continue', 'tags': ['seq:step:3'], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 1, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25', '72b74d24-5792-48da-a887-102100d6e2c0']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': '72b74d24-5792-48da-a887-102100d6e2c0', 'name': 'agent', 'tags': ['graph:step:1'], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 1, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0}, 'data': {'chunk': {'messages': [{'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}}}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_end', 'data': {'output': {'messages': [{'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}}, 'input': {'some_bytes': None, 'some_byte_array': None, 'dict_with_bytes': None, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}], 'sleep': None}}, 'run_id': '72b74d24-5792-48da-a887-102100d6e2c0', 'name': 'agent', 'tags': ['graph:step:1'], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 1, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'task_result', 'timestamp': '2024-06-24T21:34:06.124350+00:00', 'step': 1, 'payload': {'id': '212ed9c2-a454-50c5-a202-12066bbbe7b8', 'name': 'agent', 'result': [['some_bytes', 'c29tZV9ieXRlcw=='], ['some_byte_array', 'c29tZV9ieXRlX2FycmF5'], ['dict_with_bytes', {'more_bytes': 'bW9yZV9ieXRlcw=='}], ['messages', [{'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]]]}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'name': 'LangGraph', 'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'data': {'chunk': ['debug', {'type': 'task_result', 'timestamp': '2024-06-24T21:34:06.124350+00:00', 'step': 1, 'payload': {'id': '212ed9c2-a454-50c5-a202-12066bbbe7b8', 'name': 'agent', 'result': [['some_bytes', 'c29tZV9ieXRlcw=='], ['some_byte_array', 'c29tZV9ieXRlX2FycmF5'], ['dict_with_bytes', {'more_bytes': 'bW9yZV9ieXRlcw=='}], ['messages', [{'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]]]}}]}, 'parent_ids': []}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'name': 'LangGraph', 'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'data': {'chunk': ['values', {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}]}, 'parent_ids': []}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: messages/complete...\n",
"[{'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'checkpoint', 'timestamp': '2024-06-24T21:34:06.124510+00:00', 'step': 1, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'thread_ts': '1ef32717-bc91-6a34-8001-26353c117c25', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25'}, 'values': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}, 'metadata': {'source': 'loop', 'step': 1, 'writes': {'agent': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}}}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'name': 'LangGraph', 'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'data': {'chunk': ['debug', {'type': 'checkpoint', 'timestamp': '2024-06-24T21:34:06.124510+00:00', 'step': 1, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'thread_ts': '1ef32717-bc91-6a34-8001-26353c117c25', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25'}, 'values': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}, 'metadata': {'source': 'loop', 'step': 1, 'writes': {'agent': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}}}}]}, 'parent_ids': []}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'task', 'timestamp': '2024-06-24T21:34:06.124572+00:00', 'step': 2, 'payload': {'id': '44139125-a1be-57c2-9cb2-19eb62bbaf2f', 'name': 'tool', 'input': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'sleep': None}, 'triggers': ['branch:agent:should_continue:tool']}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'name': 'LangGraph', 'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'data': {'chunk': ['debug', {'type': 'task', 'timestamp': '2024-06-24T21:34:06.124572+00:00', 'step': 2, 'payload': {'id': '44139125-a1be-57c2-9cb2-19eb62bbaf2f', 'name': 'tool', 'input': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'sleep': None}, 'triggers': ['branch:agent:should_continue:tool']}}]}, 'parent_ids': []}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_start', 'data': {}, 'name': 'tool', 'tags': ['graph:step:2'], 'run_id': '91575720-886e-485e-ae2d-d6817e5346bf', 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 2, 'langgraph_node': 'tool', 'langgraph_triggers': ['branch:agent:should_continue:tool'], 'langgraph_task_idx': 0}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': '91575720-886e-485e-ae2d-d6817e5346bf', 'name': 'tool', 'tags': ['graph:step:2'], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 2, 'langgraph_node': 'tool', 'langgraph_triggers': ['branch:agent:should_continue:tool'], 'langgraph_task_idx': 0}, 'data': {'chunk': {'messages': [{'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': None, 'tool_call_id': 'tool_call_id'}]}}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_end', 'data': {'output': {'messages': [{'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '639ca779-403d-4915-a066-327e1f634c8b', 'tool_call_id': 'tool_call_id'}]}, 'input': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'sleep': None}}, 'run_id': '91575720-886e-485e-ae2d-d6817e5346bf', 'name': 'tool', 'tags': ['graph:step:2'], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 2, 'langgraph_node': 'tool', 'langgraph_triggers': ['branch:agent:should_continue:tool'], 'langgraph_task_idx': 0}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'task_result', 'timestamp': '2024-06-24T21:34:06.126828+00:00', 'step': 2, 'payload': {'id': '44139125-a1be-57c2-9cb2-19eb62bbaf2f', 'name': 'tool', 'result': [['messages', [{'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '639ca779-403d-4915-a066-327e1f634c8b', 'tool_call_id': 'tool_call_id'}]]]}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'name': 'LangGraph', 'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'data': {'chunk': ['debug', {'type': 'task_result', 'timestamp': '2024-06-24T21:34:06.126828+00:00', 'step': 2, 'payload': {'id': '44139125-a1be-57c2-9cb2-19eb62bbaf2f', 'name': 'tool', 'result': [['messages', [{'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '639ca779-403d-4915-a066-327e1f634c8b', 'tool_call_id': 'tool_call_id'}]]]}}]}, 'parent_ids': []}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'name': 'LangGraph', 'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'data': {'chunk': ['values', {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '639ca779-403d-4915-a066-327e1f634c8b', 'tool_call_id': 'tool_call_id'}]}]}, 'parent_ids': []}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: messages/complete...\n",
"[{'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '639ca779-403d-4915-a066-327e1f634c8b', 'tool_call_id': 'tool_call_id'}]\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'checkpoint', 'timestamp': '2024-06-24T21:34:06.126966+00:00', 'step': 2, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'thread_ts': '1ef32717-bc97-6a06-8002-8e9ffc1ea75a', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25'}, 'values': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '639ca779-403d-4915-a066-327e1f634c8b', 'tool_call_id': 'tool_call_id'}]}, 'metadata': {'source': 'loop', 'step': 2, 'writes': {'tool': {'messages': [{'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '639ca779-403d-4915-a066-327e1f634c8b', 'tool_call_id': 'tool_call_id'}]}}}}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'name': 'LangGraph', 'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'data': {'chunk': ['debug', {'type': 'checkpoint', 'timestamp': '2024-06-24T21:34:06.126966+00:00', 'step': 2, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'thread_ts': '1ef32717-bc97-6a06-8002-8e9ffc1ea75a', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25'}, 'values': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '639ca779-403d-4915-a066-327e1f634c8b', 'tool_call_id': 'tool_call_id'}]}, 'metadata': {'source': 'loop', 'step': 2, 'writes': {'tool': {'messages': [{'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '639ca779-403d-4915-a066-327e1f634c8b', 'tool_call_id': 'tool_call_id'}]}}}}}]}, 'parent_ids': []}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'task', 'timestamp': '2024-06-24T21:34:06.127034+00:00', 'step': 3, 'payload': {'id': 'f1ccf371-63b3-5268-a837-7f360a93c4ec', 'name': 'agent', 'input': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '639ca779-403d-4915-a066-327e1f634c8b', 'tool_call_id': 'tool_call_id'}], 'sleep': None}, 'triggers': ['tool']}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'name': 'LangGraph', 'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'data': {'chunk': ['debug', {'type': 'task', 'timestamp': '2024-06-24T21:34:06.127034+00:00', 'step': 3, 'payload': {'id': 'f1ccf371-63b3-5268-a837-7f360a93c4ec', 'name': 'agent', 'input': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '639ca779-403d-4915-a066-327e1f634c8b', 'tool_call_id': 'tool_call_id'}], 'sleep': None}, 'triggers': ['tool']}}]}, 'parent_ids': []}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_start', 'data': {}, 'name': 'agent', 'tags': ['graph:step:3'], 'run_id': 'b7d0900c-bfc2-43e4-b760-99bbc5bad84e', 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 3, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_start', 'data': {'input': {'messages': [[{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '639ca779-403d-4915-a066-327e1f634c8b', 'tool_call_id': 'tool_call_id'}]]}}, 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'run_id': '0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 3, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25', 'b7d0900c-bfc2-43e4-b760-99bbc5bad84e']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_stream', 'data': {'chunk': {'content': 'e', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'AIMessageChunk', 'name': None, 'id': 'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None, 'tool_call_chunks': []}}, 'run_id': '0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 3, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25', 'b7d0900c-bfc2-43e4-b760-99bbc5bad84e']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: messages/metadata...\n",
"{'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575': {'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 3, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: messages/partial...\n",
"[{'content': 'e', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_stream', 'data': {'chunk': {'content': 'n', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'AIMessageChunk', 'name': None, 'id': 'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None, 'tool_call_chunks': []}}, 'run_id': '0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 3, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25', 'b7d0900c-bfc2-43e4-b760-99bbc5bad84e']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: messages/partial...\n",
"[{'content': 'en', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_stream', 'data': {'chunk': {'content': 'd', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'AIMessageChunk', 'name': None, 'id': 'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None, 'tool_call_chunks': []}}, 'run_id': '0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 3, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25', 'b7d0900c-bfc2-43e4-b760-99bbc5bad84e']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: messages/partial...\n",
"[{'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chat_model_end', 'data': {'output': {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, 'input': {'messages': [[{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '639ca779-403d-4915-a066-327e1f634c8b', 'tool_call_id': 'tool_call_id'}]]}}, 'run_id': '0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'name': 'FakeListChatModel', 'tags': ['seq:step:1'], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 3, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0, 'ls_model_type': 'chat'}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25', 'b7d0900c-bfc2-43e4-b760-99bbc5bad84e']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_start', 'data': {'input': {'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '639ca779-403d-4915-a066-327e1f634c8b', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}}}, 'name': 'should_continue', 'tags': ['seq:step:3'], 'run_id': '8af814e9-8136-4aab-acbc-dffc5bcafdfd', 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 3, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25', 'b7d0900c-bfc2-43e4-b760-99bbc5bad84e']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_end', 'data': {'output': '__end__', 'input': {'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '639ca779-403d-4915-a066-327e1f634c8b', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}}}, 'run_id': '8af814e9-8136-4aab-acbc-dffc5bcafdfd', 'name': 'should_continue', 'tags': ['seq:step:3'], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 3, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25', 'b7d0900c-bfc2-43e4-b760-99bbc5bad84e']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': 'b7d0900c-bfc2-43e4-b760-99bbc5bad84e', 'name': 'agent', 'tags': ['graph:step:3'], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 3, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0}, 'data': {'chunk': {'messages': [{'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}}}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_end', 'data': {'output': {'messages': [{'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}}, 'input': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '639ca779-403d-4915-a066-327e1f634c8b', 'tool_call_id': 'tool_call_id'}], 'sleep': None}}, 'run_id': 'b7d0900c-bfc2-43e4-b760-99bbc5bad84e', 'name': 'agent', 'tags': ['graph:step:3'], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 3, 'langgraph_node': 'agent', 'langgraph_triggers': ['tool'], 'langgraph_task_idx': 0}, 'parent_ids': ['1ef32717-bc30-6cf2-8a26-33f63567bc25']}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'task_result', 'timestamp': '2024-06-24T21:34:06.133991+00:00', 'step': 3, 'payload': {'id': 'f1ccf371-63b3-5268-a837-7f360a93c4ec', 'name': 'agent', 'result': [['some_bytes', 'c29tZV9ieXRlcw=='], ['some_byte_array', 'c29tZV9ieXRlX2FycmF5'], ['dict_with_bytes', {'more_bytes': 'bW9yZV9ieXRlcw=='}], ['messages', [{'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]]]}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'name': 'LangGraph', 'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'data': {'chunk': ['debug', {'type': 'task_result', 'timestamp': '2024-06-24T21:34:06.133991+00:00', 'step': 3, 'payload': {'id': 'f1ccf371-63b3-5268-a837-7f360a93c4ec', 'name': 'agent', 'result': [['some_bytes', 'c29tZV9ieXRlcw=='], ['some_byte_array', 'c29tZV9ieXRlX2FycmF5'], ['dict_with_bytes', {'more_bytes': 'bW9yZV9ieXRlcw=='}], ['messages', [{'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]]]}}]}, 'parent_ids': []}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'name': 'LangGraph', 'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'data': {'chunk': ['values', {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '639ca779-403d-4915-a066-327e1f634c8b', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}]}, 'parent_ids': []}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: messages/complete...\n",
"[{'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'checkpoint', 'timestamp': '2024-06-24T21:34:06.134190+00:00', 'step': 3, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'thread_ts': '1ef32717-bca9-6418-8003-8d0d0b06845c', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25'}, 'values': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '639ca779-403d-4915-a066-327e1f634c8b', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}, 'metadata': {'source': 'loop', 'step': 3, 'writes': {'agent': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}}}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_stream', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'name': 'LangGraph', 'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'data': {'chunk': ['debug', {'type': 'checkpoint', 'timestamp': '2024-06-24T21:34:06.134190+00:00', 'step': 3, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'thread_ts': '1ef32717-bca9-6418-8003-8d0d0b06845c', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25'}, 'values': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '639ca779-403d-4915-a066-327e1f634c8b', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}, 'metadata': {'source': 'loop', 'step': 3, 'writes': {'agent': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}}}}]}, 'parent_ids': []}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: events...\n",
"{'event': 'on_chain_end', 'data': {'output': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': \"What's the weather in SF?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '7da1bafa-f53c-4df8-ba63-8dd517140b9f', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-2424dd6d-5cf5-4244-8d98-357640ce6e12', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '639ca779-403d-4915-a066-327e1f634c8b', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-0f2ef0a1-0fc7-445c-9df4-55e8bb284575', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}, 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'name': 'LangGraph', 'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef32717-bc30-6cf2-8a26-33f63567bc25', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'parent_ids': []}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: end...\n",
"None\n",
"\n",
"\n",
"\n"
]
}
],
"source": [
"# create input\n",
"input = {\n",
" \"messages\": [\n",
" {\n",
" \"role\": \"human\",\n",
" \"content\": \"What's the weather in SF?\",\n",
" }\n",
" ]\n",
"}\n",
"\n",
"# stream events with multiple streaming modes\n",
"async for chunk in client.runs.stream(\n",
" thread_id=thread[\"thread_id\"],\n",
" assistant_id=\"agent\",\n",
" input=input,\n",
" stream_mode=[\"messages\", \"events\", \"debug\"],\n",
"):\n",
" print(f\"Receiving new event of type: {chunk.event}...\")\n",
" print(chunk.data)\n",
" print(\"\\n\\n\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "env",
"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
}
+20 -61
View File
@@ -5,16 +5,16 @@
"id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53",
"metadata": {},
"source": [
"# How to stream updates from your graph\n",
"# How to stream state updates of your graph\n",
"\n",
"There are multiple different streaming modes.\n",
"LangGraph Cloud supports multiple streaming modes. The main ones are:\n",
"\n",
"- `values`: This streaming mode streams back values of the graph. This is the **full state of the graph** after each node is called.\n",
"- `updates`: This streaming mode streams back updates to the graph. This is the **update to the state of the graph** after each node is called.\n",
"- `messages`: This streaming mode streams back messages - both complete messages (at the end of a node) as well as **tokens** for any messages generated inside a node. This mode is primarily meant for powering chat applications.\n",
"\n",
"\n",
"This notebook covers `streaming_mode=\"updates\"`."
"This guide covers `stream_mode=\"updates\"`."
]
},
{
@@ -40,75 +40,29 @@
{
"cell_type": "code",
"execution_count": 3,
"id": "4947e9bc-111f-4991-8c41-1041da9bf0ba",
"id": "230c0464-a6e5-420f-9e38-ca514e5634ce",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[{'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n",
" 'graph_id': 'agent',\n",
" 'config': {},\n",
" 'created_at': '2024-05-18T00:19:39.688822+00:00',\n",
" 'updated_at': '2024-05-18T00:19:39.688822+00:00',\n",
" 'metadata': {'created_by': 'system'}}]"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"outputs": [],
"source": [
"# There should always be a default assistant with no configuration\n",
"assistants = await client.assistants.search()\n",
"assistants = [a for a in assistants if not a['config']]\n",
"assistants"
"assistant_id = \"agent\""
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "230c0464-a6e5-420f-9e38-ca514e5634ce",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n",
" 'graph_id': 'agent',\n",
" 'config': {},\n",
" 'created_at': '2024-05-18T00:19:39.688822+00:00',\n",
" 'updated_at': '2024-05-18T00:19:39.688822+00:00',\n",
" 'metadata': {'created_by': 'system'}}"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"assistant = assistants[0]\n",
"assistant"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "56aa5159-5583-4134-9210-709b969bda6f",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'thread_id': '1eee9bd4-ec61-4300-a38a-2e13b8925d39',\n",
" 'created_at': '2024-05-18T19:57:36.509105+00:00',\n",
" 'updated_at': '2024-05-18T19:57:36.509105+00:00',\n",
"{'thread_id': '979e3c89-a702-4882-87c2-7a59a250ce16',\n",
" 'created_at': '2024-06-21T15:22:07.453100+00:00',\n",
" 'updated_at': '2024-06-21T15:22:07.453100+00:00',\n",
" 'metadata': {}}"
]
},
"execution_count": 5,
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
@@ -120,7 +74,7 @@
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": 5,
"id": "147c3f98-f889-4f05-a090-6b31f2a0b291",
"metadata": {},
"outputs": [
@@ -130,13 +84,13 @@
"[]"
]
},
"execution_count": 6,
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"runs = await client.runs.list(thread['thread_id'])\n",
"runs = await client.runs.list(thread[\"thread_id\"])\n",
"runs"
]
},
@@ -180,7 +134,12 @@
],
"source": [
"input = {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in la\"}]}\n",
"async for chunk in client.runs.stream(thread['thread_id'], assistant['assistant_id'], input=input, stream_mode=\"updates\", ):\n",
"async for chunk in client.runs.stream(\n",
" thread[\"thread_id\"],\n",
" assistant_id,\n",
" input=input,\n",
" stream_mode=\"updates\",\n",
"):\n",
" print(f\"Receiving new event of type: {chunk.event}...\")\n",
" print(chunk.data)\n",
" print(\"\\n\\n\")"
@@ -211,7 +170,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.1"
"version": "3.12.3"
}
},
"nbformat": 4,
+8 -54
View File
@@ -5,16 +5,16 @@
"id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53",
"metadata": {},
"source": [
"# How to stream values from your graph\n",
"# How to stream full state of your graph\n",
"\n",
"There are multiple different streaming modes.\n",
"LangGraph Cloud supports multiple streaming modes. The main ones are:\n",
"\n",
"- `values`: This streaming mode streams back values of the graph. This is the **full state of the graph** after each node is called.\n",
"- `updates`: This streaming mode streams back updates to the graph. This is the **update to the state of the graph** after each node is called.\n",
"- `messages`: This streaming mode streams back messages - both complete messages (at the end of a node) as well as **tokens** for any messages generated inside a node. This mode is primarily meant for powering chat applications.\n",
"\n",
"\n",
"This notebook covers `streaming_mode=\"values\"`."
"This guide covers `stream_mode=\"values\"`."
]
},
{
@@ -40,57 +40,11 @@
{
"cell_type": "code",
"execution_count": 3,
"id": "4947e9bc-111f-4991-8c41-1041da9bf0ba",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[{'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n",
" 'graph_id': 'agent',\n",
" 'config': {},\n",
" 'created_at': '2024-05-18T00:19:39.688822+00:00',\n",
" 'updated_at': '2024-05-18T00:19:39.688822+00:00',\n",
" 'metadata': {'created_by': 'system'}}]"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# There should always be a default assistant with no configuration\n",
"assistants = await client.assistants.search()\n",
"assistants = [a for a in assistants if not a['config']]\n",
"assistants"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "230c0464-a6e5-420f-9e38-ca514e5634ce",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n",
" 'graph_id': 'agent',\n",
" 'config': {},\n",
" 'created_at': '2024-05-18T00:19:39.688822+00:00',\n",
" 'updated_at': '2024-05-18T00:19:39.688822+00:00',\n",
" 'metadata': {'created_by': 'system'}}"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"outputs": [],
"source": [
"assistant = assistants[0]\n",
"assistant"
"assistant_id = \"agent\""
]
},
{
@@ -139,7 +93,7 @@
"source": [
"input = {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in la\"}]}\n",
"thread = await client.threads.create()\n",
"async for chunk in client.runs.stream(thread['thread_id'], assistant['assistant_id'], input=input):\n",
"async for chunk in client.runs.stream(thread[\"thread_id\"], assistant_id, input=input):\n",
" print(f\"Receiving new event of type: {chunk.event}...\")\n",
" print(chunk.data)\n",
" print(\"\\n\\n\")"
@@ -163,7 +117,7 @@
"input = {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in la\"}]}\n",
"thread = await client.threads.create()\n",
"final_answer = None\n",
"async for chunk in client.runs.stream(thread['thread_id'], assistant['assistant_id'], input=input):\n",
"async for chunk in client.runs.stream(thread[\"thread_id\"], assistant_id, input=input):\n",
" if chunk.event == \"values\":\n",
" final_answer = chunk.data"
]
@@ -250,7 +204,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.1"
"version": "3.12.3"
}
},
"nbformat": 4,
+72
View File
@@ -0,0 +1,72 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Use Webhooks\n",
"\n",
"You may wish to use webhooks in your client, especially when using async streams in case you want to update something in your service once the API call to Langgraph Cloud has finished running. To do so, you will need to expose an endpoint that can accept POST requests, and then pass it to your API request in the \"webhook\" parameter.\n",
"\n",
"Currently, the SDK has not exposed this endpoint but you can access it through curl commands as follows.\n",
"\n",
"The following endpoints accept `webhook` as a parameter: \n",
"\n",
"- Create Run -> POST /thread/{thread_id}/runs\n",
"- Create Thread Cron -> POST /thread/{thread_id}/runs/crons\n",
"- Stream Run -> POST /thread/{thread_id}/runs/stream\n",
"- Wait Run -> POST /thread/{thread_id}/runs/wait\n",
"- Create Cron -> POST /runs/crons\n",
"- Stream Run Stateless -> POST /runs/stream\n",
"- Wait Run Stateless -> POST /runs/wait\n",
"\n",
"The following example uses a url from a public website that allows users to create free webhooks, but you should pass in the webhook that you wish to use. "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"curl --request POST \\\n",
" --url http://localhost:8123/threads/b76d1e94-f251-40e3-8933-796d775cdb4c/runs/stream \\\n",
" --header 'Content-Type: application/json' \\\n",
" --data '{\n",
" \"assistant_id\": \"fe096781-5601-53d2-b2f6-0d3403f7e9ca\",\n",
" \"input\" : {\"messages\":[{\"role\": \"user\", \"content\": \"Hello! My name is Bagatur and I am 26 years old.\"}]},\n",
" \"metadata\": {},\n",
" \"config\": {\n",
" \"configurable\": {}\n",
" },\n",
" \"multitask_strategy\": \"reject\",\n",
" \"stream_mode\": [\n",
" \"values\"\n",
" ],\n",
" \"webhook\": \"https://webhook.site/6ca33471-dd65-4103-a851-0a252dae0f2a\"\n",
"}'"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To check that this worked as intended, we can go to the website where our webhook was created and confirm that it received a POST request:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![Webhook response](./img/webhook_results.png)"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
+245
View File
@@ -0,0 +1,245 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "992c4695-ec4f-428d-bd05-fb3b5fbd70f4",
"metadata": {},
"source": [
"# How to add human-in-the-loop processes to the prebuilt ReAct agent\n",
"\n",
"This tutorial will show how to add human-in-the-loop processes to the prebuilt ReAct agent. Please see [this tutorial](./create-react-agent.ipynb) for how to get started with the prebuilt ReAct agent\n",
"\n",
"You can add a a breakpoint before tools are called by passing `interrupt_before=[\"tools\"]` to `create_react_agent`. Note that you need to be using a checkpointer for this to work."
]
},
{
"cell_type": "markdown",
"id": "7be3889f-3c17-4fa1-bd2b-84114a2c7247",
"metadata": {},
"source": [
"## Setup"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "a213e11a-5c62-4ddb-a707-490d91add383",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "23a1885c-04ab-4750-aefa-105891fddf3e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"OPENAI_API_KEY: ········\n"
]
}
],
"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\")\n",
"\n",
"# Recommended\n",
"_set_env(\"LANGCHAIN_API_KEY\")\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"Create ReAct Agent Tutorial\""
]
},
{
"cell_type": "markdown",
"id": "03c0f089-070c-4cd4-87e0-6c51f2477b82",
"metadata": {},
"source": [
"## Code"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "7a154152-973e-4b5d-aa13-48c617744a4c",
"metadata": {},
"outputs": [],
"source": [
"# First we initialize the model we want to use.\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
"\n",
"\n",
"# For this tutorial we will use custom tool that returns pre-defined values for weather in two cities (NYC & SF)\n",
"\n",
"from typing import Literal\n",
"\n",
"from langchain_core.tools import tool\n",
"\n",
"\n",
"@tool\n",
"def get_weather(city: Literal[\"nyc\", \"sf\"]):\n",
" \"\"\"Use this to get weather information.\"\"\"\n",
" if city == \"nyc\":\n",
" return \"It might be cloudy in nyc\"\n",
" elif city == \"sf\":\n",
" return \"It's always sunny in sf\"\n",
" else:\n",
" raise AssertionError(\"Unknown city\")\n",
"\n",
"\n",
"tools = [get_weather]\n",
"\n",
"# We need a checkpointer to enable human-in-the-loop patterns\n",
"from langgraph.checkpoint import MemorySaver\n",
"\n",
"memory = MemorySaver()\n",
"\n",
"# Define the graph\n",
"\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"graph = create_react_agent(model, tools=tools, interrupt_before=[\"tools\"], checkpointer=memory)"
]
},
{
"cell_type": "markdown",
"id": "00407425-506d-4ffd-9c86-987921d8c844",
"metadata": {},
"source": [
"## Usage\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "16636975-5f2d-4dc7-ab8e-d0bea0830a28",
"metadata": {},
"outputs": [],
"source": [
"def print_stream(stream):\n",
" for s in stream:\n",
" message = s[\"messages\"][-1]\n",
" if isinstance(message, tuple):\n",
" print(message)\n",
" else:\n",
" message.pretty_print()"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "9ffff6c3-a4f5-47c9-b51d-97caaee85cd6",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"What's the weather in SF?\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_0OMmuTLec9t8kxMVkllZCSxo)\n",
" Call ID: call_0OMmuTLec9t8kxMVkllZCSxo\n",
" Args:\n",
" city: sf\n"
]
}
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"42\"}}\n",
"inputs = {\"messages\": [(\"user\", \"What's the weather in SF?\")]}\n",
"\n",
"print_stream(graph.stream(inputs, config, stream_mode=\"values\"))"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "3decf001-7228-4ed5-8779-2b9ed98a74ea",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Next step: ('tools',)\n"
]
}
],
"source": [
"snapshot = graph.get_state(config)\n",
"print(\"Next step: \", snapshot.next)"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "83148e08-63e8-49e5-a08b-02dc907bed1d",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: get_weather\n",
"\n",
"It's always sunny in sf\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"The weather in San Francisco is currently sunny.\n"
]
}
],
"source": [
"print_stream(graph.stream(None, config, stream_mode=\"values\"))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6f6f8965-b016-4e25-be63-31c00fc0a6de",
"metadata": {},
"outputs": [],
"source": []
}
],
"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.1"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+255
View File
@@ -0,0 +1,255 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "992c4695-ec4f-428d-bd05-fb3b5fbd70f4",
"metadata": {},
"source": [
"# How to add memory to the prebuilt ReAct agent\n",
"\n",
"This tutorial will show how to add memory to the prebuilt ReAct agent. Please see [this tutorial](./create-react-agent.ipynb) for how to get started with the prebuilt ReAct agent\n",
"\n",
"All we need to do to enable memory is pass in a checkpointer to `create_react_agents`"
]
},
{
"cell_type": "markdown",
"id": "7be3889f-3c17-4fa1-bd2b-84114a2c7247",
"metadata": {},
"source": [
"## Setup"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "a213e11a-5c62-4ddb-a707-490d91add383",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "23a1885c-04ab-4750-aefa-105891fddf3e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"OPENAI_API_KEY: ········\n"
]
}
],
"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\")\n",
"\n",
"# Recommended\n",
"_set_env(\"LANGCHAIN_API_KEY\")\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"Create ReAct Agent Tutorial\""
]
},
{
"cell_type": "markdown",
"id": "03c0f089-070c-4cd4-87e0-6c51f2477b82",
"metadata": {},
"source": [
"## Code"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "7a154152-973e-4b5d-aa13-48c617744a4c",
"metadata": {},
"outputs": [],
"source": [
"# First we initialize the model we want to use.\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
"\n",
"\n",
"# For this tutorial we will use custom tool that returns pre-defined values for weather in two cities (NYC & SF)\n",
"\n",
"from typing import Literal\n",
"\n",
"from langchain_core.tools import tool\n",
"\n",
"\n",
"@tool\n",
"def get_weather(city: Literal[\"nyc\", \"sf\"]):\n",
" \"\"\"Use this to get weather information.\"\"\"\n",
" if city == \"nyc\":\n",
" return \"It might be cloudy in nyc\"\n",
" elif city == \"sf\":\n",
" return \"It's always sunny in sf\"\n",
" else:\n",
" raise AssertionError(\"Unknown city\")\n",
"\n",
"\n",
"tools = [get_weather]\n",
"\n",
"# We can add \"chat memory\" to the graph with LangGraph's checkpointer\n",
"# to retain the chat context between interactions\n",
"from langgraph.checkpoint import MemorySaver\n",
"\n",
"memory = MemorySaver()\n",
"\n",
"# Define the graph\n",
"\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"graph = create_react_agent(model, tools=tools, checkpointer=memory)"
]
},
{
"cell_type": "markdown",
"id": "00407425-506d-4ffd-9c86-987921d8c844",
"metadata": {},
"source": [
"## Usage\n",
"\n",
"Let's interact with it multiple times to show that it can remember"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "16636975-5f2d-4dc7-ab8e-d0bea0830a28",
"metadata": {},
"outputs": [],
"source": [
"def print_stream(stream):\n",
" for s in stream:\n",
" message = s[\"messages\"][-1]\n",
" if isinstance(message, tuple):\n",
" print(message)\n",
" else:\n",
" message.pretty_print()"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "9ffff6c3-a4f5-47c9-b51d-97caaee85cd6",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"What's the weather in NYC?\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_mdovy4yXSSYrmSlnlVSUacVn)\n",
" Call ID: call_mdovy4yXSSYrmSlnlVSUacVn\n",
" Args:\n",
" city: nyc\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: get_weather\n",
"\n",
"It might be cloudy in nyc\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"The weather in NYC might be cloudy.\n"
]
}
],
"source": [
"config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
"inputs = {\"messages\": [(\"user\", \"What's the weather in NYC?\")]}\n",
"\n",
"print_stream(graph.stream(inputs, config=config, stream_mode=\"values\"))"
]
},
{
"cell_type": "markdown",
"id": "838a043f-90ad-4e69-9d1d-6e22db2c346c",
"metadata": {},
"source": [
"Notice that when we pass the same the same thread ID, the chat history is preserved"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "187479f9-32fa-4611-9487-cf816ba2e147",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"What's it known for?\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"New York City (NYC) is known for many things, including:\n",
"\n",
"1. **Landmarks and Attractions**: The Statue of Liberty, Times Square, Central Park, Empire State Building, and Brooklyn Bridge.\n",
"2. **Cultural Institutions**: Broadway theaters, Metropolitan Museum of Art, Museum of Modern Art (MoMA), and the American Museum of Natural History.\n",
"3. **Diverse Neighborhoods**: Areas like Chinatown, Little Italy, Harlem, and Greenwich Village.\n",
"4. **Financial Hub**: Wall Street and the New York Stock Exchange.\n",
"5. **Cuisine**: A melting pot of global cuisines, famous for its pizza, bagels, and street food.\n",
"6. **Media and Entertainment**: Home to major media companies, TV networks, and film studios.\n",
"7. **Fashion**: A global fashion capital, hosting New York Fashion Week.\n",
"8. **Sports**: Teams like the New York Yankees, New York Mets, New York Knicks, and New York Rangers.\n",
"9. **Public Transportation**: An extensive subway system and iconic yellow taxis.\n",
"10. **Events**: New Year's Eve celebration in Times Square, Macy's Thanksgiving Day Parade, and various cultural festivals.\n"
]
}
],
"source": [
"inputs = {\"messages\": [(\"user\", \"What's it known for?\")]}\n",
"print_stream(graph.stream(inputs, config=config, stream_mode=\"values\"))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3decf001-7228-4ed5-8779-2b9ed98a74ea",
"metadata": {},
"outputs": [],
"source": []
}
],
"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.1"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,207 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "992c4695-ec4f-428d-bd05-fb3b5fbd70f4",
"metadata": {},
"source": [
"# How to add a custom system prompt to the prebuilt ReAct agent\n",
"\n",
"This tutorial will show how to add a custom system prompt to the prebuilt ReAct agent. Please see [this tutorial](./create-react-agent.ipynb) for how to get started with the prebuilt ReAct agent\n",
"\n",
"You can add a custom system prompt by passing a string to the `messages_modifier` param."
]
},
{
"cell_type": "markdown",
"id": "7be3889f-3c17-4fa1-bd2b-84114a2c7247",
"metadata": {},
"source": [
"## Setup"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "a213e11a-5c62-4ddb-a707-490d91add383",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "23a1885c-04ab-4750-aefa-105891fddf3e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"OPENAI_API_KEY: ········\n"
]
}
],
"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\")\n",
"\n",
"# Recommended\n",
"_set_env(\"LANGCHAIN_API_KEY\")\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"Create ReAct Agent Tutorial\""
]
},
{
"cell_type": "markdown",
"id": "03c0f089-070c-4cd4-87e0-6c51f2477b82",
"metadata": {},
"source": [
"## Code"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "7a154152-973e-4b5d-aa13-48c617744a4c",
"metadata": {},
"outputs": [],
"source": [
"# First we initialize the model we want to use.\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
"\n",
"\n",
"# For this tutorial we will use custom tool that returns pre-defined values for weather in two cities (NYC & SF)\n",
"\n",
"from typing import Literal\n",
"\n",
"from langchain_core.tools import tool\n",
"\n",
"\n",
"@tool\n",
"def get_weather(city: Literal[\"nyc\", \"sf\"]):\n",
" \"\"\"Use this to get weather information.\"\"\"\n",
" if city == \"nyc\":\n",
" return \"It might be cloudy in nyc\"\n",
" elif city == \"sf\":\n",
" return \"It's always sunny in sf\"\n",
" else:\n",
" raise AssertionError(\"Unknown city\")\n",
"\n",
"\n",
"tools = [get_weather]\n",
"\n",
"# We can add our system prompt here\n",
"\n",
"prompt = \"Respond in Italian\"\n",
"\n",
"# Define the graph\n",
"\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"graph = create_react_agent(model, tools=tools, messages_modifier=prompt)"
]
},
{
"cell_type": "markdown",
"id": "00407425-506d-4ffd-9c86-987921d8c844",
"metadata": {},
"source": [
"## Usage\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "16636975-5f2d-4dc7-ab8e-d0bea0830a28",
"metadata": {},
"outputs": [],
"source": [
"def print_stream(stream):\n",
" for s in stream:\n",
" message = s[\"messages\"][-1]\n",
" if isinstance(message, tuple):\n",
" print(message)\n",
" else:\n",
" message.pretty_print()"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "9ffff6c3-a4f5-47c9-b51d-97caaee85cd6",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"What's the weather in NYC?\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_b02uzBRrIm2uciJa8zDXCDxT)\n",
" Call ID: call_b02uzBRrIm2uciJa8zDXCDxT\n",
" Args:\n",
" city: nyc\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: get_weather\n",
"\n",
"It might be cloudy in nyc\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"A New York potrebbe essere nuvoloso.\n"
]
}
],
"source": [
"inputs = {\"messages\": [(\"user\", \"What's the weather in NYC?\")]}\n",
"\n",
"print_stream(graph.stream(inputs, stream_mode=\"values\"))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3decf001-7228-4ed5-8779-2b9ed98a74ea",
"metadata": {},
"outputs": [],
"source": []
}
],
"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.1"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+44 -421
View File
@@ -5,7 +5,7 @@
"id": "992c4695-ec4f-428d-bd05-fb3b5fbd70f4",
"metadata": {},
"source": [
"# How to create a ReAct agent"
"# How to use the prebuilt ReAct agent"
]
},
{
@@ -16,7 +16,14 @@
"In this how-to we'll create a simple [ReAct](https://arxiv.org/abs/2210.03629) agent app that can check the weather. The app consists of an agent (LLM) and tools. As we interact with the app, we will first call the agent (LLM) to decide if we should use tools. Then we will run a loop: \n",
"\n",
"1. If the agent said to take an action (i.e. call tool), we'll run the tools and pass the results back to the agent\n",
"2. If the agent did not ask to run tools, we will finish (respond to the user)"
"2. If the agent did not ask to run tools, we will finish (respond to the user)\n",
"\n",
"<div class=\"admonition warning\">\n",
" <p class=\"admonition-title\">Prebuilt Agent</p>\n",
" <p>\n",
"Please note that here will we use a prebuilt agent. One of the big benefits of LangGraph is that you can easily create your own agent architectures. So while it's fine to start here to build an agent quickly, we would strongly recommend learning how to build your own agent so that you can take full advantage of LangGraph.\n",
" </p>\n",
"</div> "
]
},
{
@@ -75,40 +82,29 @@
"id": "03c0f089-070c-4cd4-87e0-6c51f2477b82",
"metadata": {},
"source": [
"## How to create a simple ReAct agent with `create_react_agent`"
]
},
{
"cell_type": "markdown",
"id": "91efaf20-711b-4b94-837e-dcef10d8abdd",
"metadata": {},
"source": [
"In our example we'll use `ChatOpenAI` as our agent and a custom tool that returns pre-defined values for weather in two cities (NYC & SF)"
]
},
{
"cell_type": "markdown",
"id": "6238f7d4-7da1-4e48-bc5e-a592280271e0",
"metadata": {},
"source": [
"### Define model and tools"
"## Code"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "029c838a-c6e7-4679-9a8b-953703fe3041",
"execution_count": 1,
"id": "7a154152-973e-4b5d-aa13-48c617744a4c",
"metadata": {},
"outputs": [],
"source": [
"from typing import Literal\n",
"\n",
"from langchain_core.tools import tool\n",
"# First we initialize the model we want to use.\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"model = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n",
"\n",
"\n",
"# For this tutorial we will use custom tool that returns pre-defined values for weather in two cities (NYC & SF)\n",
"\n",
"from typing import Literal\n",
"\n",
"from langchain_core.tools import tool\n",
"\n",
"\n",
"@tool\n",
"def get_weather(city: Literal[\"nyc\", \"sf\"]):\n",
" \"\"\"Use this to get weather information.\"\"\"\n",
@@ -120,32 +116,29 @@
" raise AssertionError(\"Unknown city\")\n",
"\n",
"\n",
"tools = [get_weather]"
]
},
{
"cell_type": "markdown",
"id": "47570e6c-655b-45ae-ad4a-b278e58f1b94",
"metadata": {},
"source": [
"### Define the graph"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "3d2df9e7-4c2c-494a-9249-5a2ee32eca3e",
"metadata": {},
"outputs": [],
"source": [
"tools = [get_weather]\n",
"\n",
"\n",
"# Define the graph\n",
"\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"graph = create_react_agent(model, tools=tools)"
]
},
{
"cell_type": "markdown",
"id": "00407425-506d-4ffd-9c86-987921d8c844",
"metadata": {},
"source": [
"## Usage\n",
"\n",
"First, let's visualize the graph we just created"
]
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": 4,
"id": "fa16de4c-aac0-4ff4-ab69-60d399f75423",
"metadata": {},
"outputs": [
@@ -168,7 +161,7 @@
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": 5,
"id": "16636975-5f2d-4dc7-ab8e-d0bea0830a28",
"metadata": {},
"outputs": [],
@@ -192,7 +185,7 @@
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": 6,
"id": "9ffff6c3-a4f5-47c9-b51d-97caaee85cd6",
"metadata": {},
"outputs": [
@@ -205,8 +198,8 @@
"what is the weather in sf\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_g6w9lHn3fxYo2ABE3Ihhprbr)\n",
" Call ID: call_g6w9lHn3fxYo2ABE3Ihhprbr\n",
" get_weather (call_jgO5OOUnugRkhRi3wAOHl8Et)\n",
" Call ID: call_jgO5OOUnugRkhRi3wAOHl8Et\n",
" Args:\n",
" city: sf\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
@@ -234,7 +227,7 @@
},
{
"cell_type": "code",
"execution_count": 9,
"execution_count": 7,
"id": "187479f9-32fa-4611-9487-cf816ba2e147",
"metadata": {},
"outputs": [
@@ -255,383 +248,13 @@
"inputs = {\"messages\": [(\"user\", \"who built you?\")]}\n",
"print_stream(graph.stream(inputs, stream_mode=\"values\"))"
]
},
{
"cell_type": "markdown",
"id": "82b06aa3-6414-48e6-ba11-07bf065a316a",
"metadata": {},
"source": [
"## How to add system prompt to `create_react_agent`"
]
},
{
"cell_type": "markdown",
"id": "a3823699-1050-40fc-a8cc-ecbdc03c1cbe",
"metadata": {},
"source": [
"There are several ways to customize prompt, all of which are controlled by `messages_modifier` param. You can pass:\n",
"- system message string / `SystemMessage` that will be prepended to the list of messages\n",
"- a function that takes a list of messages and transforms them into an output that can be passed to the language model"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "23ea0a31-3e6b-433b-a832-036655579ebe",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"What's your name? And what's the weather in SF?\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_PGwTiytTVAAvNKWp4nznPi4s)\n",
" Call ID: call_PGwTiytTVAAvNKWp4nznPi4s\n",
" Args:\n",
" city: sf\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: get_weather\n",
"\n",
"It's always sunny in sf\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"My name is Fred. The weather in San Francisco is currently sunny.\n"
]
}
],
"source": [
"system_prompt = \"You are a helpful bot named Fred.\"\n",
"graph = create_react_agent(model, tools, messages_modifier=system_prompt)\n",
"\n",
"inputs = {\"messages\": [(\"user\", \"What's your name? And what's the weather in SF?\")]}\n",
"print_stream(graph.stream(inputs, stream_mode=\"values\"))"
]
},
{
"cell_type": "markdown",
"id": "d8e797ce-74bf-4a30-a388-08104138fd52",
"metadata": {},
"source": [
"We can also add a more complex prompt for the LLM:"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "23c40ab3-c574-47be-a80f-654e9035cd6d",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"What's my name? And what's the weather in SF?\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_scpzIjdK3l411zcEn2T00Xm0)\n",
" Call ID: call_scpzIjdK3l411zcEn2T00Xm0\n",
" Args:\n",
" city: sf\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: get_weather\n",
"\n",
"It's always sunny in sf\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Your name is Joe. The weather in San Francisco is always sunny.\n"
]
}
],
"source": [
"from langchain_core.prompts import ChatPromptTemplate\n",
"\n",
"prompt = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\"system\", \"You are a helpful bot named Fred.\"),\n",
" (\"user\", \"My name is Joe\"),\n",
" (\"placeholder\", \"{messages}\"),\n",
" ]\n",
")\n",
"\n",
"\n",
"def modify_messages(messages: list):\n",
" # You can do more complex modifications here\n",
" return prompt.invoke({\"messages\": messages})\n",
"\n",
"\n",
"graph = create_react_agent(model, tools, messages_modifier=modify_messages)\n",
"\n",
"inputs = {\"messages\": [(\"user\", \"What's my name? And what's the weather in SF?\")]}\n",
"print_stream(graph.stream(inputs, stream_mode=\"values\"))"
]
},
{
"cell_type": "markdown",
"id": "0af418b8-bdd3-4e10-a552-9d8d2a409a57",
"metadata": {},
"source": [
"## How to add memory to `create_react_agent`"
]
},
{
"cell_type": "markdown",
"id": "4eef6365-530b-4d33-9874-41273ebee510",
"metadata": {},
"source": [
"We can add \"chat memory\" to the graph with LangGraph's checkpointer, to retain the chat context between interactions"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "c43d071a-a91e-4646-a035-b3fed9cd9f0c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"What's the weather in NYC?\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_C5yEOD1GhlVPX9Gc5nnqgUXF)\n",
" Call ID: call_C5yEOD1GhlVPX9Gc5nnqgUXF\n",
" Args:\n",
" city: nyc\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: get_weather\n",
"\n",
"It might be cloudy in nyc\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"The weather in NYC might be cloudy.\n"
]
}
],
"source": [
"from langgraph.checkpoint import MemorySaver\n",
"\n",
"graph = create_react_agent(model, tools, checkpointer=MemorySaver())\n",
"\n",
"config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
"inputs = {\"messages\": [(\"user\", \"What's the weather in NYC?\")]}\n",
"\n",
"print_stream(graph.stream(inputs, config=config, stream_mode=\"values\"))"
]
},
{
"cell_type": "markdown",
"id": "4541ea58-ee6a-4685-a988-c915a42284e6",
"metadata": {},
"source": [
"Notice that when we pass the same the same thread ID, the chat history is preserved"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "b509417b-70f3-4736-b838-5d1729ed759e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"What's it known for?\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"New York City (NYC) is known for many things, including:\n",
"\n",
"1. **Landmarks and Attractions**: \n",
" - **Statue of Liberty**: A symbol of freedom and democracy.\n",
" - **Times Square**: Known for its bright lights, Broadway theaters, and bustling atmosphere.\n",
" - **Central Park**: A large urban park offering various recreational activities.\n",
" - **Empire State Building**: An iconic skyscraper with an observation deck offering panoramic views of the city.\n",
" - **Brooklyn Bridge**: A historic bridge connecting Manhattan and Brooklyn.\n",
"\n",
"2. **Cultural Diversity**: NYC is a melting pot of cultures, languages, and cuisines, making it one of the most diverse cities in the world.\n",
"\n",
"3. **Arts and Entertainment**: \n",
" - **Broadway**: Renowned for its world-class theater productions.\n",
" - **Museums**: Such as the Metropolitan Museum of Art, the Museum of Modern Art (MoMA), and the American Museum of Natural History.\n",
" - **Music and Nightlife**: A vibrant scene with numerous music venues, bars, and clubs.\n",
"\n",
"4. **Financial Hub**: \n",
" - **Wall Street**: The financial district is home to the New York Stock Exchange and numerous financial institutions.\n",
"\n",
"5. **Fashion and Shopping**: \n",
" - **Fifth Avenue**: Known for its high-end shopping.\n",
" - **Fashion Week**: One of the major fashion events held twice a year.\n",
"\n",
"6. **Cuisine**: \n",
" - **Diverse Food Scene**: From street food like hot dogs and pretzels to fine dining and international cuisines.\n",
" - **Famous Foods**: New York-style pizza, bagels, and cheesecake.\n",
"\n",
"7. **Media and Publishing**: \n",
" - Home to major media companies, newspapers like The New York Times, and numerous publishing houses.\n",
"\n",
"8. **Sports**: \n",
" - Home to several major sports teams, including the New York Yankees (baseball), New York Mets (baseball), New York Knicks (basketball), Brooklyn Nets (basketball), New York Giants (football), and New York Jets (football).\n",
"\n",
"9. **Education and Research**: \n",
" - Prestigious institutions like Columbia University, New York University (NYU), and Rockefeller University.\n",
"\n",
"10. **Public Transportation**: \n",
" - An extensive subway system, buses, and taxis that make getting around the city convenient.\n",
"\n",
"NYC is a city that never sleeps, offering endless opportunities for exploration and experiences.\n"
]
}
],
"source": [
"inputs = {\"messages\": [(\"user\", \"What's it known for?\")]}\n",
"print_stream(graph.stream(inputs, config=config, stream_mode=\"values\"))"
]
},
{
"cell_type": "markdown",
"id": "ef765ab6-3ea0-4de0-8737-de8f9d9add40",
"metadata": {},
"source": [
"And if we pass a different thread ID, the chat history is reset"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "1bb09dbe-bbba-4e8f-b44c-b18bd2ed96ff",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"What's it known for?\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Could you please specify what \"it\" refers to? Are you asking about a specific city, person, object, or something else?\n"
]
}
],
"source": [
"inputs = {\"messages\": [(\"user\", \"What's it known for?\")]}\n",
"print_stream(\n",
" graph.stream(\n",
" inputs, config={\"configurable\": {\"thread_id\": 2}}, stream_mode=\"values\"\n",
" )\n",
")"
]
},
{
"cell_type": "markdown",
"id": "42e730aa-8fac-48a5-9c67-b51393665469",
"metadata": {},
"source": [
"## How to add human-in-the-loop to `create_react_agent`"
]
},
{
"cell_type": "markdown",
"id": "6314e49c-9143-45a8-871b-8bd3f489425c",
"metadata": {},
"source": [
"Let's add an interrupt to let the user confirm before LLM takes an action:"
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "8cd2cbcc-8a1f-443e-9cc9-dc18cd0ceac6",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"What's the weather in SF?\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_I7B9YW4ENth7QXYzDpiIoLCE)\n",
" Call ID: call_I7B9YW4ENth7QXYzDpiIoLCE\n",
" Args:\n",
" city: sf\n"
]
}
],
"source": [
"graph = create_react_agent(\n",
" model, tools, interrupt_before=[\"tools\"], checkpointer=MemorySaver()\n",
")\n",
"\n",
"config = {\"configurable\": {\"thread_id\": \"42\"}}\n",
"inputs = {\"messages\": [(\"user\", \"What's the weather in SF?\")]}\n",
"\n",
"print_stream(graph.stream(inputs, config, stream_mode=\"values\"))"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "493d4963-b27b-46ef-8474-1527b08c1c03",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Next step: ('tools',)\n"
]
}
],
"source": [
"snapshot = graph.get_state(config)\n",
"print(\"Next step: \", snapshot.next)"
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "7ad01f3a-e838-4230-ab07-2f850bb7e7cb",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: get_weather\n",
"\n",
"It's always sunny in sf\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"The weather in San Francisco is currently sunny.\n"
]
}
],
"source": [
"print_stream(graph.stream(None, config, stream_mode=\"values\"))"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "langgraph-example-dev",
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "langgraph-example-dev"
"name": "python3"
},
"language_info": {
"codemirror_mode": {
@@ -643,7 +266,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
"version": "3.11.1"
}
},
"nbformat": 4,
+2 -2
View File
@@ -5,7 +5,7 @@
"id": "e327e9bd-effc-4bee-a875-1c383c17f43d",
"metadata": {},
"source": [
"# How to extract structured output with re-prompting\n",
"# Complex data extraction with function calling\n",
"\n",
"Function calling is a core primitive for integrating LLMs within your software stack. We use it throughout the LangGraph docs, since developing with function calling (aka tool usage) tends to be much more stress-free than the traditional way of writing custom string parsers.\n",
"\n",
@@ -1036,7 +1036,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.2"
"version": "3.11.1"
}
},
"nbformat": 4,
+1 -1
View File
@@ -577,7 +577,7 @@
"\n",
"There are multiple options, and the ideal option may depend on the specifics of your application and capabilities of your chosen LLM. Note that many chat models require that messages with tool calls be immediately followed by a tool message containing the result of the tool call. So our intervention may:\n",
"\n",
"1. Update the parameters of the tool call before proceeding normally (see this [how-to guide](https://langchain-ai.github.io/langgraph/how-tos/time-travel/#pause-before-tools) for an example);\n",
"1. Update the parameters of the tool call before proceeding normally (see this [how-to guide](./human_in_the_loop/breakpoints.ipynb) for an example);\n",
"2. Add a tool message to the conversation history indicating the user's desired intervention (see an example [here](https://langchain-ai.github.io/langgraph/tutorials/customer-support/customer-support/#state-assistant));\n",
"3. Catch the tool call message, replacing it with a `AIMessage` asking for verification and only adding the tool call message to the conversation history if approved.\n",
"\n",
@@ -0,0 +1,339 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53",
"metadata": {},
"source": [
"# How to add breakpoints\n",
"\n",
"When creating LangGraph agents, it is often nice to add a human-in-the-loop component.\n",
"This can be helpful when giving them access to tools.\n",
"Often in these situations you may want to manually approve an action before taking.\n",
"\n",
"This can be in several ways, but the primary supported way is to add an \"interrupt\" before a node is executed.\n",
"This interrupts execution at that node.\n",
"You can then resume from that spot to continue. "
]
},
{
"cell_type": "markdown",
"id": "7cbd446a-808f-4394-be92-d45ab818953c",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"First we need to install the packages required"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_anthropic"
]
},
{
"cell_type": "markdown",
"id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d",
"metadata": {},
"source": [
"Next, we need to set API keys for Anthropic (the LLM we will use)"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"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(\"ANTHROPIC_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c",
"metadata": {},
"source": [
"Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "3333b771",
"metadata": {},
"source": [
"## Build the agent\n",
"\n",
"We can now build the agent. We will build a relatively simple ReAct-style agent that does tool calling. We will use Anthropic's models and a fake tool (just for demo purposes)."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "6098e5cb",
"metadata": {},
"outputs": [],
"source": [
"# Set up the state\n",
"from langgraph.graph import MessagesState\n",
"\n",
"# Set up the tool\n",
"from langchain_core.tools import tool\n",
"from langgraph.prebuilt import ToolExecutor\n",
"\n",
"@tool\n",
"def search(query: str):\n",
" \"\"\"Call to surf the web.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" # Don't let the LLM know this though 😊\n",
" return [\n",
" \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n",
" ]\n",
"\n",
"\n",
"tools = [search]\n",
"tool_executor = ToolExecutor(tools)\n",
"\n",
"# Set up the model\n",
"from langchain_anthropic import ChatAnthropic\n",
"\n",
"model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n",
"model = model.bind_tools(tools)\n",
"\n",
"# Define nodes and conditional edges\n",
"\n",
"from langchain_core.messages import ToolMessage\n",
"\n",
"from langgraph.prebuilt import ToolInvocation\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def should_continue(state):\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" # If there is no function call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"end\"\n",
" # Otherwise if there is, we continue\n",
" else:\n",
" return \"continue\"\n",
"\n",
"\n",
"# Define the function that calls the model\n",
"def call_model(state):\n",
" messages = state[\"messages\"]\n",
" response = model.invoke(messages)\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define the function to execute tools\n",
"def call_tool(state):\n",
" messages = state[\"messages\"]\n",
" # Based on the continue condition\n",
" # we know the last message involves a function call\n",
" last_message = messages[-1]\n",
" # We construct an ToolInvocation from the function_call\n",
" tool_call = last_message.tool_calls[0]\n",
" action = ToolInvocation(\n",
" tool=tool_call[\"name\"],\n",
" tool_input=tool_call[\"args\"],\n",
" )\n",
" # We call the tool_executor and get back a response\n",
" response = tool_executor.invoke(action)\n",
" # We use the response to create a ToolMessage\n",
" tool_message = ToolMessage(\n",
" content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n",
" )\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [tool_message]}\n",
"\n",
"# Build the graph\n",
"\n",
"from langgraph.graph import END, StateGraph\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(MessagesState)\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",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# 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",
"\n",
"# Set up memory\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"memory = MemorySaver()\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",
"\n",
"# We add in `interrupt_before=[\"action\"]`\n",
"# This will add a breakpoint before the `action` node is called\n",
"app = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"
]
},
{
"cell_type": "markdown",
"id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159",
"metadata": {},
"source": [
"## Interacting with the Agent\n",
"\n",
"We can now interact with the agent and see that it stops before calling a tool.\n"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "cfd140f0-a5a6-4697-8115-322242f197b5",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"search for the weather in sf now\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"[{'text': \"Certainly! I'll search for the current weather in San Francisco for you. Let me use the search function to find this information.\", 'type': 'text'}, {'id': 'toolu_017HamT7ubS5RXGCL7CS3t7F', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}]\n",
"Tool Calls:\n",
" search (toolu_017HamT7ubS5RXGCL7CS3t7F)\n",
" Call ID: toolu_017HamT7ubS5RXGCL7CS3t7F\n",
" Args:\n",
" query: current weather in San Francisco\n"
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"thread = {\"configurable\": {\"thread_id\": \"3\"}}\n",
"inputs = [HumanMessage(content=\"search for the weather in sf now\")]\n",
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "1bca3814-db08-4b0b-8c0c-95b6c5440c81",
"metadata": {},
"source": [
"**Resume**\n",
"\n",
"We can now call the agent again with no inputs to continue, ie. run the tool as requested.\n",
"\n",
"Running an interrupted graph with `None` in the inputs means to \"proceed as if the interruption didn't occur.\""
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "51923913-20f7-4ee1-b9ba-d01f5fb2869b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: search\n",
"\n",
"[\"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"]\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Based on the search results, I can provide you with information about the current weather in San Francisco:\n",
"\n",
"The weather in San Francisco right now is sunny. It's a beautiful day in the city!\n",
"\n",
"It's worth noting that the search result included an unusual comment about Gemini, which isn't directly related to the weather. This appears to be an unrelated piece of information or possibly part of a horoscope that was included in the search results.\n",
"\n",
"If you'd like more specific details about the temperature, wind conditions, or forecast for the coming days, please let me know, and I'd be happy to search for that additional information for you.\n"
]
}
],
"source": [
"for event in app.stream(None, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
}
],
"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.1"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,425 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53",
"metadata": {},
"source": [
"# How to edit graph state\n",
"\n",
"When creating LangGraph agents, it is often nice to add a human-in-the-loop component.\n",
"This can be helpful when giving them access to tools.\n",
"Often in these situations you may want to edit the graph state before continuing (for example, to edit what tool is being called, or how it is being called).\n",
"\n",
"This can be in several ways, but the primary supported way is to add an \"interrupt\" before a node is executed.\n",
"This interrupts execution at that node.\n",
"You can then use `update_state` to update the state, and then resume from that spot to continue. "
]
},
{
"cell_type": "markdown",
"id": "7cbd446a-808f-4394-be92-d45ab818953c",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"First we need to install the packages required"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_anthropic"
]
},
{
"cell_type": "markdown",
"id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d",
"metadata": {},
"source": [
"Next, we need to set API keys for Anthropic (the LLM we will use)"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"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(\"ANTHROPIC_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c",
"metadata": {},
"source": [
"Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "3333b771",
"metadata": {},
"source": [
"## Build the agent\n",
"\n",
"We can now build the agent. We will build a relatively simple ReAct-style agent that does tool calling. We will use Anthropic's models and a fake tool (just for demo purposes)."
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "6098e5cb",
"metadata": {},
"outputs": [],
"source": [
"# Set up the state\n",
"from langgraph.graph import MessagesState\n",
"\n",
"# Set up the tool\n",
"from langchain_core.tools import tool\n",
"from langgraph.prebuilt import ToolExecutor\n",
"\n",
"@tool\n",
"def search(query: str):\n",
" \"\"\"Call to surf the web.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" # Don't let the LLM know this though 😊\n",
" return [\n",
" f\"I looked up: {query}. Result: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n",
" ]\n",
"\n",
"\n",
"tools = [search]\n",
"tool_executor = ToolExecutor(tools)\n",
"\n",
"# Set up the model\n",
"from langchain_anthropic import ChatAnthropic\n",
"\n",
"model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n",
"model = model.bind_tools(tools)\n",
"\n",
"# Define nodes and conditional edges\n",
"\n",
"from langchain_core.messages import ToolMessage\n",
"\n",
"from langgraph.prebuilt import ToolInvocation\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def should_continue(state):\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" # If there is no function call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"end\"\n",
" # Otherwise if there is, we continue\n",
" else:\n",
" return \"continue\"\n",
"\n",
"\n",
"# Define the function that calls the model\n",
"def call_model(state):\n",
" messages = state[\"messages\"]\n",
" response = model.invoke(messages)\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define the function to execute tools\n",
"def call_tool(state):\n",
" messages = state[\"messages\"]\n",
" # Based on the continue condition\n",
" # we know the last message involves a function call\n",
" last_message = messages[-1]\n",
" # We construct an ToolInvocation from the function_call\n",
" tool_call = last_message.tool_calls[0]\n",
" action = ToolInvocation(\n",
" tool=tool_call[\"name\"],\n",
" tool_input=tool_call[\"args\"],\n",
" )\n",
" # We call the tool_executor and get back a response\n",
" response = tool_executor.invoke(action)\n",
" # We use the response to create a ToolMessage\n",
" tool_message = ToolMessage(\n",
" content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n",
" )\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [tool_message]}\n",
"\n",
"# Build the graph\n",
"\n",
"from langgraph.graph import END, StateGraph\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(MessagesState)\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",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# 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",
"\n",
"# Set up memory\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"memory = MemorySaver()\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",
"\n",
"# We add in `interrupt_before=[\"action\"]`\n",
"# This will add a breakpoint before the `action` node is called\n",
"app = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"
]
},
{
"cell_type": "markdown",
"id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159",
"metadata": {},
"source": [
"## Interacting with the Agent\n",
"\n",
"We can now interact with the agent and see that it stops before calling a tool.\n"
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "cfd140f0-a5a6-4697-8115-322242f197b5",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"search for the weather in sf now\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"[{'text': \"Certainly! I'll search for the current weather in San Francisco for you. Let me use the search function to find this information.\", 'type': 'text'}, {'id': 'toolu_011s1G2cKjKkkkJTcWb3ze5m', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}]\n",
"Tool Calls:\n",
" search (toolu_011s1G2cKjKkkkJTcWb3ze5m)\n",
" Call ID: toolu_011s1G2cKjKkkkJTcWb3ze5m\n",
" Args:\n",
" query: current weather in San Francisco\n"
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"thread = {\"configurable\": {\"thread_id\": \"3\"}}\n",
"inputs = [HumanMessage(content=\"search for the weather in sf now\")]\n",
"for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "78e3f5b9-9700-42b1-863f-c404861f8620",
"metadata": {},
"source": [
"**Edit**\n",
"\n",
"We can now update the state accordingly. Let's modify the tool call to have the query `\"current weather in SF\"`."
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "1aa7b1b9-9322-4815-bc0d-eb083870ac15",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'configurable': {'thread_id': '3',\n",
" 'thread_ts': '1ef3102f-e345-627e-8002-bfdc440474fe'}}"
]
},
"execution_count": 20,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# First, lets get the current state\n",
"current_state = app.get_state(thread)\n",
"\n",
"# Let's now get the last message in the state\n",
"# This is the one with the tool calls that we want to update\n",
"last_message = current_state.values['messages'][-1]\n",
"\n",
"# Let's now update the args for that tool call\n",
"last_message.tool_calls[0]['args'] = {'query': 'current weather in SF'}\n",
"\n",
"# Let's now call `update_state` to pass in this message in the `messages` key\n",
"# This will get treated as any other update to the state\n",
"# It will get passed to the reducer function for the `messages` key\n",
"# That reducer function will use the ID of the message to update it\n",
"# It's important that it has the right ID! Otherwise it would get appended\n",
"# as a new message\n",
"app.update_state(thread, {\"messages\": last_message})"
]
},
{
"cell_type": "markdown",
"id": "0dcc5457-1ba1-4cba-ac41-da5c67cc67e5",
"metadata": {},
"source": [
"Let's now check the current state of the app to make sure it got updated accordingly"
]
},
{
"cell_type": "code",
"execution_count": 21,
"id": "a3fcf2bd-f881-49fe-b20e-ad16e6819bc6",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[{'name': 'search',\n",
" 'args': {'query': 'current weather in SF'},\n",
" 'id': 'toolu_011s1G2cKjKkkkJTcWb3ze5m'}]"
]
},
"execution_count": 21,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"current_state = app.get_state(thread).values['messages'][-1].tool_calls\n",
"current_state"
]
},
{
"cell_type": "markdown",
"id": "1bca3814-db08-4b0b-8c0c-95b6c5440c81",
"metadata": {},
"source": [
"**Resume**\n",
"\n",
"We can now call the agent again with no inputs to continue, ie. run the tool as requested. We can see from the logs that it passes in the update args to the tool."
]
},
{
"cell_type": "code",
"execution_count": 22,
"id": "51923913-20f7-4ee1-b9ba-d01f5fb2869b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: search\n",
"\n",
"[\"I looked up: current weather in SF. Result: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"]\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Based on the search results, I can provide you with information about the current weather in San Francisco:\n",
"\n",
"The weather in San Francisco is currently sunny. This means it's a clear day with plenty of sunshine, which is quite typical for San Francisco, especially during certain times of the year.\n",
"\n",
"It's worth noting that San Francisco's weather can be quite variable, even within the city itself, due to its unique microclimate and geography. While it's sunny now, it's always a good idea to be prepared for potential changes in weather, as the city is known for its foggy conditions that can roll in quickly, especially near the coast.\n",
"\n",
"The search result also included an unusual astrological reference about Geminis, but that's not relevant to the weather information you requested. If you need any more specific details about the weather, such as temperature, wind speed, or forecast for the coming days, please let me know, and I'd be happy to search for that information as well.\n"
]
}
],
"source": [
"for event in app.stream(None, thread, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "78780afe-409d-46cd-a734-e82538cdd8de",
"metadata": {},
"outputs": [],
"source": []
}
],
"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.1"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,619 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53",
"metadata": {},
"source": [
"# How to view and update past graph state\n",
"\n",
"Once you start [checkpointing](../persistence.ipynb) your graphs, you can easily **get** or **update** the state of the agent at any point in time. This permits a few things:\n",
"\n",
"1. You can surface a state during an interrupt to a user to let them accept an action.\n",
"2. You can **rewind** the graph to reproduce or avoid issues.\n",
"3. You can **modify** the state to embed your agent into a larger system, or to let the user better control its actions.\n",
"\n",
"The key methods used for this functionality are:\n",
"\n",
"- [get_state](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.graph.CompiledGraph.get_state): fetch the values from the target config\n",
"- [update_state](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.graph.CompiledGraph.update_state): apply the given values to the target state\n",
"\n",
"**Note:** this requires passing in a checkpointer.\n",
"\n",
"Below is a quick example."
]
},
{
"cell_type": "markdown",
"id": "7cbd446a-808f-4394-be92-d45ab818953c",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"First we need to install the packages required"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_anthropic"
]
},
{
"cell_type": "markdown",
"id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d",
"metadata": {},
"source": [
"Next, we need to set API keys for Anthropic (the LLM we will use)"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"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(\"ANTHROPIC_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c",
"metadata": {},
"source": [
"Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "e36f89e5",
"metadata": {},
"source": [
"## Build the agent\n",
"\n",
"We can now build the agent. We will build a relatively simple ReAct-style agent that does tool calling. We will use Anthropic's models and a fake tool (just for demo purposes)."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "f5319e01",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/Users/harrisonchase/.pyenv/versions/3.11.1/envs/permchain/lib/python3.11/site-packages/langchain_core/_api/beta_decorator.py:87: LangChainBetaWarning: The method `ChatAnthropic.bind_tools` is in beta. It is actively being worked on, so the API may change.\n",
" warn_beta(\n"
]
}
],
"source": [
"# Set up the state\n",
"from langgraph.graph import MessagesState\n",
"\n",
"# Set up the tool\n",
"from langchain_core.tools import tool\n",
"from langgraph.prebuilt import ToolExecutor\n",
"\n",
"@tool\n",
"def search(query: str):\n",
" \"\"\"Call to surf the web.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" # Don't let the LLM know this though 😊\n",
" return [\n",
" f\"I looked up: {query}. Result: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n",
" ]\n",
"\n",
"\n",
"tools = [search]\n",
"tool_executor = ToolExecutor(tools)\n",
"\n",
"# Set up the model\n",
"from langchain_anthropic import ChatAnthropic\n",
"\n",
"model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n",
"model = model.bind_tools(tools)\n",
"\n",
"# Define nodes and conditional edges\n",
"\n",
"from langchain_core.messages import ToolMessage\n",
"\n",
"from langgraph.prebuilt import ToolInvocation\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def should_continue(state):\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" # If there is no function call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"end\"\n",
" # Otherwise if there is, we continue\n",
" else:\n",
" return \"continue\"\n",
"\n",
"\n",
"# Define the function that calls the model\n",
"def call_model(state):\n",
" messages = state[\"messages\"]\n",
" response = model.invoke(messages)\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define the function to execute tools\n",
"def call_tool(state):\n",
" messages = state[\"messages\"]\n",
" # Based on the continue condition\n",
" # we know the last message involves a function call\n",
" last_message = messages[-1]\n",
" # We construct an ToolInvocation from the function_call\n",
" tool_call = last_message.tool_calls[0]\n",
" action = ToolInvocation(\n",
" tool=tool_call[\"name\"],\n",
" tool_input=tool_call[\"args\"],\n",
" )\n",
" # We call the tool_executor and get back a response\n",
" response = tool_executor.invoke(action)\n",
" # We use the response to create a ToolMessage\n",
" tool_message = ToolMessage(\n",
" content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n",
" )\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [tool_message]}\n",
"\n",
"# Build the graph\n",
"\n",
"from langgraph.graph import END, StateGraph\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(MessagesState)\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",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# 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",
"\n",
"# Set up memory\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"memory = MemorySaver()\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(checkpointer=memory)"
]
},
{
"cell_type": "markdown",
"id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159",
"metadata": {},
"source": [
"## Interacting with the Agent\n",
"\n",
"We can now interact with the agent. Let's ask it for the weather in SF.\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "cfd140f0-a5a6-4697-8115-322242f197b5",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"Use the search tool to look up the weather in SF\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"[{'text': \"Certainly! I'll use the search tool to look up the weather in San Francisco for you. Let me do that right away.\", 'type': 'text'}, {'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7', 'input': {'query': 'weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}]\n",
"Tool Calls:\n",
" search (toolu_01M1JGmPF1wjJRxY2SYsCYS7)\n",
" Call ID: toolu_01M1JGmPF1wjJRxY2SYsCYS7\n",
" Args:\n",
" query: weather in San Francisco\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: search\n",
"\n",
"[\"I looked up: weather in San Francisco. Result: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"]\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Based on the search results, I can provide you with information about the weather in San Francisco:\n",
"\n",
"The current weather in San Francisco is sunny. This means it's a clear day with plenty of sunshine, which is quite common for San Francisco, especially during certain times of the year.\n",
"\n",
"However, there's an interesting additional note in the search result that seems to be unrelated to the weather itself. It mentions something about Geminis, which appears to be a reference to astrology. This part of the result doesn't provide any relevant weather information, so we'll focus on the actual weather report.\n",
"\n",
"To summarize:\n",
"- Current weather in San Francisco: Sunny\n",
"- Sky conditions: Clear\n",
"\n",
"Keep in mind that San Francisco's weather can change quickly due to its unique microclimate, influenced by the bay and ocean. Even on sunny days, it's always a good idea to be prepared for potential fog or cooler temperatures, especially near the coast or in the evenings.\n",
"\n",
"Is there any specific information about the San Francisco weather you'd like to know more about, such as temperature, wind conditions, or forecast for the coming days?\n"
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
"input_message = HumanMessage(content=\"Use the search tool to look up the weather in SF\")\n",
"for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "1c38c505-6cee-427f-9dcd-493a2ade7ebb",
"metadata": {},
"source": [
"## Checking history\n",
"\n",
"Let's browse the history of this thread, from start to finish."
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "8578a66d-6489-4e03-8c23-fd0530278455",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"StateSnapshot(values={'messages': []}, next=('__start__',), config={'configurable': {'thread_id': '1', 'thread_ts': '1ef3103d-f96d-65f2-bfff-f32837888e44'}}, metadata={'source': 'input', 'step': -1, 'writes': {'messages': [HumanMessage(content='Use the search tool to look up the weather in SF')]}}, created_at='2024-06-23T01:56:57.764168+00:00', parent_config=None)\n",
"--\n",
"StateSnapshot(values={'messages': [HumanMessage(content='Use the search tool to look up the weather in SF', id='8948e4e1-0d86-4dfd-929d-48da47fc0a18')]}, next=('agent',), config={'configurable': {'thread_id': '1', 'thread_ts': '1ef3103d-f974-6b0e-8000-99038b849969'}}, metadata={'source': 'loop', 'step': 0, 'writes': None}, created_at='2024-06-23T01:56:57.767174+00:00', parent_config=None)\n",
"--\n",
"StateSnapshot(values={'messages': [HumanMessage(content='Use the search tool to look up the weather in SF', id='8948e4e1-0d86-4dfd-929d-48da47fc0a18'), AIMessage(content=[{'text': \"Certainly! I'll use the search tool to look up the weather in San Francisco for you. Let me do that right away.\", 'type': 'text'}, {'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7', 'input': {'query': 'weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], response_metadata={'id': 'msg_01JGRvXQBPCGbXxHHHj5mSQs', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 363, 'output_tokens': 82}}, id='run-3ec0086e-31c5-43e3-86cc-badaf14dd001-0', tool_calls=[{'name': 'search', 'args': {'query': 'weather in San Francisco'}, 'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7'}])]}, next=('action',), config={'configurable': {'thread_id': '1', 'thread_ts': '1ef3103e-072f-68f2-8001-29ebe76a1f5e'}}, metadata={'source': 'loop', 'step': 1, 'writes': {'agent': {'messages': [AIMessage(content=[{'text': \"Certainly! I'll use the search tool to look up the weather in San Francisco for you. Let me do that right away.\", 'type': 'text'}, {'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7', 'input': {'query': 'weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], response_metadata={'id': 'msg_01JGRvXQBPCGbXxHHHj5mSQs', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 363, 'output_tokens': 82}}, id='run-3ec0086e-31c5-43e3-86cc-badaf14dd001-0', tool_calls=[{'name': 'search', 'args': {'query': 'weather in San Francisco'}, 'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7'}])]}}}, created_at='2024-06-23T01:56:59.206868+00:00', parent_config=None)\n",
"--\n",
"StateSnapshot(values={'messages': [HumanMessage(content='Use the search tool to look up the weather in SF', id='8948e4e1-0d86-4dfd-929d-48da47fc0a18'), AIMessage(content=[{'text': \"Certainly! I'll use the search tool to look up the weather in San Francisco for you. Let me do that right away.\", 'type': 'text'}, {'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7', 'input': {'query': 'weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], response_metadata={'id': 'msg_01JGRvXQBPCGbXxHHHj5mSQs', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 363, 'output_tokens': 82}}, id='run-3ec0086e-31c5-43e3-86cc-badaf14dd001-0', tool_calls=[{'name': 'search', 'args': {'query': 'weather in San Francisco'}, 'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7'}]), ToolMessage(content='[\"I looked up: weather in San Francisco. Result: It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini 😈.\"]', name='search', id='0e86ac06-d5a2-47f6-b6ff-6f221eac9f68', tool_call_id='toolu_01M1JGmPF1wjJRxY2SYsCYS7')]}, next=('agent',), config={'configurable': {'thread_id': '1', 'thread_ts': '1ef3103e-0739-6762-8002-1ff85304da93'}}, metadata={'source': 'loop', 'step': 2, 'writes': {'action': {'messages': [ToolMessage(content='[\"I looked up: weather in San Francisco. Result: It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini 😈.\"]', name='search', id='0e86ac06-d5a2-47f6-b6ff-6f221eac9f68', tool_call_id='toolu_01M1JGmPF1wjJRxY2SYsCYS7')]}}}, created_at='2024-06-23T01:56:59.210922+00:00', parent_config=None)\n",
"--\n",
"StateSnapshot(values={'messages': [HumanMessage(content='Use the search tool to look up the weather in SF', id='8948e4e1-0d86-4dfd-929d-48da47fc0a18'), AIMessage(content=[{'text': \"Certainly! I'll use the search tool to look up the weather in San Francisco for you. Let me do that right away.\", 'type': 'text'}, {'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7', 'input': {'query': 'weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], response_metadata={'id': 'msg_01JGRvXQBPCGbXxHHHj5mSQs', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 363, 'output_tokens': 82}}, id='run-3ec0086e-31c5-43e3-86cc-badaf14dd001-0', tool_calls=[{'name': 'search', 'args': {'query': 'weather in San Francisco'}, 'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7'}]), ToolMessage(content='[\"I looked up: weather in San Francisco. Result: It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini 😈.\"]', name='search', id='0e86ac06-d5a2-47f6-b6ff-6f221eac9f68', tool_call_id='toolu_01M1JGmPF1wjJRxY2SYsCYS7'), AIMessage(content=\"Based on the search results, I can provide you with information about the weather in San Francisco:\\n\\nThe current weather in San Francisco is sunny. This means it's a clear day with plenty of sunshine, which is quite common for San Francisco, especially during certain times of the year.\\n\\nHowever, there's an interesting additional note in the search result that seems to be unrelated to the weather itself. It mentions something about Geminis, which appears to be a reference to astrology. This part of the result doesn't provide any relevant weather information, so we'll focus on the actual weather report.\\n\\nTo summarize:\\n- Current weather in San Francisco: Sunny\\n- Sky conditions: Clear\\n\\nKeep in mind that San Francisco's weather can change quickly due to its unique microclimate, influenced by the bay and ocean. Even on sunny days, it's always a good idea to be prepared for potential fog or cooler temperatures, especially near the coast or in the evenings.\\n\\nIs there any specific information about the San Francisco weather you'd like to know more about, such as temperature, wind conditions, or forecast for the coming days?\", response_metadata={'id': 'msg_01Y79Nyvq6vP57sDCZugtXHq', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 492, 'output_tokens': 239}}, id='run-ca257cec-308d-4860-8254-cf26118b56c2-0')]}, next=(), config={'configurable': {'thread_id': '1', 'thread_ts': '1ef3103e-312a-6e54-8003-bfa2548298b2'}}, metadata={'source': 'loop', 'step': 3, 'writes': {'agent': {'messages': [AIMessage(content=\"Based on the search results, I can provide you with information about the weather in San Francisco:\\n\\nThe current weather in San Francisco is sunny. This means it's a clear day with plenty of sunshine, which is quite common for San Francisco, especially during certain times of the year.\\n\\nHowever, there's an interesting additional note in the search result that seems to be unrelated to the weather itself. It mentions something about Geminis, which appears to be a reference to astrology. This part of the result doesn't provide any relevant weather information, so we'll focus on the actual weather report.\\n\\nTo summarize:\\n- Current weather in San Francisco: Sunny\\n- Sky conditions: Clear\\n\\nKeep in mind that San Francisco's weather can change quickly due to its unique microclimate, influenced by the bay and ocean. Even on sunny days, it's always a good idea to be prepared for potential fog or cooler temperatures, especially near the coast or in the evenings.\\n\\nIs there any specific information about the San Francisco weather you'd like to know more about, such as temperature, wind conditions, or forecast for the coming days?\", response_metadata={'id': 'msg_01Y79Nyvq6vP57sDCZugtXHq', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 492, 'output_tokens': 239}}, id='run-ca257cec-308d-4860-8254-cf26118b56c2-0')]}}}, created_at='2024-06-23T01:57:03.608964+00:00', parent_config=None)\n",
"--\n"
]
}
],
"source": [
"all_states = []\n",
"for state in app.get_state_history(config):\n",
" print(state)\n",
" all_states.append(state)\n",
" print(\"--\")"
]
},
{
"cell_type": "markdown",
"id": "0ec41c37-7c09-4cc7-8475-bf373fe66584",
"metadata": {},
"source": [
"## Replay a state\n",
"\n",
"We can go back to any of these states and restart the agent from there! Let's go back to right before the tool call gets executed."
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "02250602-8c4a-4fb5-bd6c-d0b9046e8699",
"metadata": {},
"outputs": [],
"source": [
"to_replay = all_states[2]"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "21e7fc18-6fd9-4e11-a84b-e0325c9640c8",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'messages': [HumanMessage(content='Use the search tool to look up the weather in SF', id='8948e4e1-0d86-4dfd-929d-48da47fc0a18'),\n",
" AIMessage(content=[{'text': \"Certainly! I'll use the search tool to look up the weather in San Francisco for you. Let me do that right away.\", 'type': 'text'}, {'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7', 'input': {'query': 'weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], response_metadata={'id': 'msg_01JGRvXQBPCGbXxHHHj5mSQs', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 363, 'output_tokens': 82}}, id='run-3ec0086e-31c5-43e3-86cc-badaf14dd001-0', tool_calls=[{'name': 'search', 'args': {'query': 'weather in San Francisco'}, 'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7'}])]}"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"to_replay.values"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "d4b01634-0041-4632-8d1f-5464580e54f5",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"('action',)"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"to_replay.next"
]
},
{
"cell_type": "markdown",
"id": "29da43ea-9295-43e2-b164-0eb28d96749c",
"metadata": {},
"source": [
"To replay from this place we just need to pass its config back to the agent. Notice that it just resumes from right where it left all - making a tool call."
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "e986f94f-706f-4b6f-b3c4-f95483b9e9b8",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'messages': [ToolMessage(content='[\"I looked up: weather in San Francisco. Result: It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini 😈.\"]', name='search', tool_call_id='toolu_01M1JGmPF1wjJRxY2SYsCYS7')]}\n",
"{'messages': [AIMessage(content='Based on the search results, I can provide you with information about the weather in San Francisco:\\n\\nThe current weather in San Francisco is sunny. This means it\\'s a clear day with plenty of sunshine, which is quite typical for San Francisco, especially during certain times of the year.\\n\\nHowever, there\\'s an interesting and somewhat humorous addition to the weather report. It mentions, \"but you better look out if you\\'re a Gemini 😈.\" This appears to be a playful reference to astrology, suggesting that Geminis might have some challenges or interesting experiences today. Of course, this is not a scientific weather forecast and is likely just added for entertainment value.\\n\\nTo summarize:\\n1. The weather in San Francisco is currently sunny.\\n2. It\\'s a good day to be outside and enjoy the clear skies.\\n3. The playful astrological reference for Geminis is just for fun and not part of the actual weather conditions.\\n\\nIs there anything else you\\'d like to know about the weather in San Francisco or any other information you need?', response_metadata={'id': 'msg_01Dv7fci5B1w6QZH7kwa8iV5', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 492, 'output_tokens': 228}}, id='run-56df996d-5d30-4d25-a224-7fe153f86494-0')]}\n"
]
}
],
"source": [
"for event in app.stream(None, to_replay.config):\n",
" for v in event.values():\n",
" print(v)"
]
},
{
"cell_type": "markdown",
"id": "59910951-fae1-4475-8511-f622439b590d",
"metadata": {},
"source": [
"## Branch off a past state\n",
"\n",
"Using LangGraph's checkpointing, you can do more than just replay past states. You can branch off previous locations to let the agent explore alternate trajectories or to let a user \"version control\" changes in a workflow.\n",
"\n",
"Let's show how to do this to edit the state at a particular point in time. Let's update the state to change the input to the tool"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "fbd5ad3b-5363-4ab7-ac63-b04668bc998f",
"metadata": {},
"outputs": [],
"source": [
"# Let's now get the last message in the state\n",
"# This is the one with the tool calls that we want to update\n",
"last_message = to_replay.values['messages'][-1]\n",
"\n",
"# Let's now update the args for that tool call\n",
"last_message.tool_calls[0]['args'] = {'query': 'current weather in SF'}\n",
"\n",
"branch_config = app.update_state(\n",
" to_replay.config, {\"messages\": [last_message]},\n",
")"
]
},
{
"cell_type": "markdown",
"id": "bced65eb-2158-43e6-a9e3-3b047c8d418e",
"metadata": {},
"source": [
"We can then invoke with this new `branch_config` to resume running from here with changed state. We can see from the log that the tool was called with different input."
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "9a92d3da-62e2-45a2-8545-e4f6a64e0ffe",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'messages': [ToolMessage(content='[\"I looked up: current weather in SF. Result: It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini 😈.\"]', name='search', tool_call_id='toolu_01M1JGmPF1wjJRxY2SYsCYS7')]}\n",
"{'messages': [AIMessage(content='Thank you for providing me with the search results. Based on the information I received, I can tell you about the current weather in San Francisco:\\n\\n1. It\\'s currently sunny in San Francisco.\\n\\nThis means you can expect clear skies and pleasant weather conditions. It\\'s a great day to be outdoors or enjoy activities in the city.\\n\\nInterestingly, the search result also included an unusual astrological reference:\\n\\n2. There was a playful warning for Geminis, suggesting they should \"look out.\"\\n\\nPlease note that the astrological comment is likely just for fun and not related to the actual weather conditions. It\\'s probably part of a horoscope or similar content that was included in the search results.\\n\\nIs there anything else you\\'d like to know about the weather in San Francisco or any other information you need?', response_metadata={'id': 'msg_01NG3SwButddYs3ui3KxuUU9', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 492, 'output_tokens': 178}}, id='run-019ec434-9e81-4335-a463-7dbb97d6a318-0')]}\n"
]
}
],
"source": [
"for event in app.stream(None, branch_config):\n",
" for v in event.values():\n",
" print(v)"
]
},
{
"cell_type": "markdown",
"id": "511e319e-d10d-4b04-a4e0-fc4f3d87cb23",
"metadata": {},
"source": [
"Alternatively, we could update the state to not even call a tool!"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "01abb480-df55-4eba-a2be-cf9372b60b54",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import AIMessage\n",
"\n",
"# Let's now get the last message in the state\n",
"# This is the one with the tool calls that we want to update\n",
"last_message = to_replay.values['messages'][-1]\n",
"\n",
"# Let's now get the ID for the last message, and create a new message with that ID.\n",
"new_message = AIMessage(content=\"its warm!\", id=last_message.id)\n",
"\n",
"branch_config = app.update_state(\n",
" to_replay.config, {\"messages\": [new_message]},\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "1a7cfcd4-289e-419e-8b49-dfaef4f88641",
"metadata": {},
"outputs": [],
"source": [
"branch_state = app.get_state(branch_config)"
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "5198f9c1-d2d4-458a-993d-3caa55810b1e",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'messages': [HumanMessage(content='Use the search tool to look up the weather in SF', id='8948e4e1-0d86-4dfd-929d-48da47fc0a18'),\n",
" AIMessage(content='its warm!', id='run-3ec0086e-31c5-43e3-86cc-badaf14dd001-0')]}"
]
},
"execution_count": 20,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"branch_state.values"
]
},
{
"cell_type": "code",
"execution_count": 21,
"id": "5d89d55d-db84-4c2d-828b-64a29a69947b",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"()"
]
},
"execution_count": 21,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"branch_state.next"
]
},
{
"cell_type": "markdown",
"id": "cc168c90-a374-4280-a9a6-8bc232dbb006",
"metadata": {},
"source": [
"You can see the snapshot was updated and now correctly reflects that there is no next step."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "74a7a5ed-0c14-4883-a16b-d70aaf40f7ea",
"metadata": {},
"outputs": [],
"source": []
}
],
"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.1"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,440 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53",
"metadata": {},
"source": [
"# How to wait for user input\n",
"\n",
"One of the main human-in-the-loop interaction patterns is waiting for human input. A key use case involves asking the user clarifying questions. One way to accomplish this is simply go to the END node and exit the graph. Then, any user response comes back in as fresh invocation of the graph. This is basically just creating a chatbot architecture.\n",
"\n",
"The issue with this is it is tough to resume back in a particular point in the graph. Often times the agent is halfway through some process, and just needs a bit of a user input. Although it is possible to design your graph in such a way where you have a `conditional_entry_point` to route user messages back to the right place, that is not super scalable (as it essentially involves having a routing function that can end up almost anywhere).\n",
"\n",
"A separate way to do this is to have a node explicitly for getting user input. This is easy to implement in a notebook setting - you just put an `input()` call in the node. But that isn't exactly production ready.\n",
"\n",
"Luckily, LangGraph makes it possible to do similar things in a production way. The basic idea is:\n",
"\n",
"- Set up a node that represents human input. This can have specific incoming/outgoing edges (as you desire). There shouldn't actually be any logic inside this node.\n",
"- Add a breakpoint before the node. This will stop the graph before this node executes (which is good, because there's no real logic in it anyways)\n",
"- Use `.update_state` to update the state of the graph. Pass in whatever human response you get. The key here is to use the `as_node` parameter to apply this update **as if you were that node**. This will have the effect of making it so that when you resume execution next it resumes as if that node just acted, and not from the beginning.\n",
"\n",
"**Note:** this requires passing in a checkpointer.\n",
"\n",
"Below is a quick example."
]
},
{
"cell_type": "markdown",
"id": "7cbd446a-808f-4394-be92-d45ab818953c",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"First we need to install the packages required"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install --quiet -U langgraph langchain_anthropic"
]
},
{
"cell_type": "markdown",
"id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d",
"metadata": {},
"source": [
"Next, we need to set API keys for Anthropic (the LLM we will use)"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
"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(\"ANTHROPIC_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c",
"metadata": {},
"source": [
"Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
"metadata": {},
"outputs": [],
"source": [
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"_set_env(\"LANGCHAIN_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "e36f89e5",
"metadata": {},
"source": [
"## Build the agent\n",
"\n",
"We can now build the agent. We will build a relatively simple ReAct-style agent that does tool calling. We will use Anthropic's models and a fake tool (just for demo purposes)."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "f5319e01",
"metadata": {},
"outputs": [],
"source": [
"# Set up the state\n",
"from langgraph.graph import MessagesState\n",
"\n",
"# Set up the tool\n",
"# We will have one real tool - a search tool\n",
"# We'll also have one \"fake\" tool - a \"ask_human\" tool\n",
"# Here we define any ACTUAL tools\n",
"from langchain_core.tools import tool\n",
"from langgraph.prebuilt import ToolExecutor\n",
"\n",
"@tool\n",
"def search(query: str):\n",
" \"\"\"Call to surf the web.\"\"\"\n",
" # This is a placeholder for the actual implementation\n",
" # Don't let the LLM know this though 😊\n",
" return [\n",
" f\"I looked up: {query}. Result: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n",
" ]\n",
"\n",
"\n",
"tools = [search]\n",
"tool_executor = ToolExecutor(tools)\n",
"\n",
"# Set up the model\n",
"from langchain_anthropic import ChatAnthropic\n",
"\n",
"model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n",
"\n",
"\n",
"# We are going \"bind\" all tools to the model\n",
"# We have the ACTUAL tools from above, but we also need a mock tool to ask a human\n",
"# Since `bind_tools` takes in tools but also just tool definitions,\n",
"# We can define a tool definition for `ask_human`\n",
"\n",
"from langchain_core.pydantic_v1 import BaseModel\n",
"\n",
"class AskHuman(BaseModel):\n",
" \"\"\"Ask the human a question\"\"\"\n",
" question: str\n",
"\n",
"\n",
"model = model.bind_tools(tools + [AskHuman])\n",
"\n",
"# Define nodes and conditional edges\n",
"\n",
"from langchain_core.messages import ToolMessage\n",
"\n",
"from langgraph.prebuilt import ToolInvocation\n",
"\n",
"\n",
"# Define the function that determines whether to continue or not\n",
"def should_continue(state):\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" # If there is no function call, then we finish\n",
" if not last_message.tool_calls:\n",
" return \"end\"\n",
" # If tool call is asking Human, we return that node\n",
" # You could also add logic here to let some system know that there's something that requires Human input\n",
" # For example, send a slack message, etc\n",
" elif last_message.tool_calls[0]['name'] == \"AskHuman\":\n",
" return \"ask_human\"\n",
" # Otherwise if there is, we continue\n",
" else:\n",
" return \"continue\"\n",
"\n",
"\n",
"# Define the function that calls the model\n",
"def call_model(state):\n",
" messages = state[\"messages\"]\n",
" response = model.invoke(messages)\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [response]}\n",
"\n",
"\n",
"# Define the function to execute tools\n",
"def call_tool(state):\n",
" messages = state[\"messages\"]\n",
" # Based on the continue condition\n",
" # we know the last message involves a function call\n",
" last_message = messages[-1]\n",
" # We construct an ToolInvocation from the function_call\n",
" tool_call = last_message.tool_calls[0]\n",
" action = ToolInvocation(\n",
" tool=tool_call[\"name\"],\n",
" tool_input=tool_call[\"args\"],\n",
" )\n",
" # We call the tool_executor and get back a response\n",
" response = tool_executor.invoke(action)\n",
" # We use the response to create a ToolMessage\n",
" tool_message = ToolMessage(\n",
" content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n",
" )\n",
" # We return a list, because this will get added to the existing list\n",
" return {\"messages\": [tool_message]}\n",
"\n",
"# We define a fake node to ask the human\n",
"def ask_human(state):\n",
" pass\n",
"\n",
"# Build the graph\n",
"\n",
"from langgraph.graph import END, StateGraph\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(MessagesState)\n",
"\n",
"# Define the three nodes we will cycle between\n",
"workflow.add_node(\"agent\", call_model)\n",
"workflow.add_node(\"action\", call_tool)\n",
"workflow.add_node(\"ask_human\", ask_human)\n",
"\n",
"# Set the entrypoint as `agent`\n",
"# This means that this node is the first one called\n",
"workflow.set_entry_point(\"agent\")\n",
"\n",
"# We now add a conditional edge\n",
"workflow.add_conditional_edges(\n",
" # First, we define the start node. We use `agent`.\n",
" # This means these are the edges taken after the `agent` node is called.\n",
" \"agent\",\n",
" # Next, we pass in the function that will determine which node is called next.\n",
" should_continue,\n",
" # Finally we pass in a mapping.\n",
" # The keys are strings, and the values are other nodes.\n",
" # END is a special node marking that the graph should finish.\n",
" # What will happen is we will call `should_continue`, and then the output of that\n",
" # will be matched against the keys in this mapping.\n",
" # Based on which one it matches, that node will then be called.\n",
" {\n",
" # If `tools`, then we call the tool node.\n",
" \"continue\": \"action\",\n",
" # We may ask the human\n",
" \"ask_human\": \"ask_human\",\n",
" # Otherwise we finish.\n",
" \"end\": END,\n",
" },\n",
")\n",
"\n",
"# 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",
"\n",
"# After we get back the human response, we go back to the agent\n",
"workflow.add_edge(\"ask_human\", \"agent\")\n",
"\n",
"# Set up memory\n",
"from langgraph.checkpoint.memory import MemorySaver\n",
"\n",
"memory = MemorySaver()\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",
"# We add a breakpoint BEFORE the `ask_human` node so it never executes\n",
"app = workflow.compile(checkpointer=memory, interrupt_before=['ask_human'])"
]
},
{
"cell_type": "markdown",
"id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159",
"metadata": {},
"source": [
"## Interacting with the Agent\n",
"\n",
"We can now interact with the agent. Let's ask it to ask the user where they are, then tell them the weather. This should make it use the `ask_human` tool first, then use the normal tool.\n"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "cfd140f0-a5a6-4697-8115-322242f197b5",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"Use the search tool to ask the user where they are, then look up the weather there\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"[{'text': \"Certainly! I'll use the AskHuman function to ask the user where they are, and then I'll use the search function to look up the weather for that location. Let's start by asking the user about their location.\", 'type': 'text'}, {'id': 'toolu_01VbhJQTHN44bzfWHwPh2KK6', 'input': {'question': 'Where are you currently located?'}, 'name': 'AskHuman', 'type': 'tool_use'}]\n",
"Tool Calls:\n",
" AskHuman (toolu_01VbhJQTHN44bzfWHwPh2KK6)\n",
" Call ID: toolu_01VbhJQTHN44bzfWHwPh2KK6\n",
" Args:\n",
" question: Where are you currently located?\n"
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"config = {\"configurable\": {\"thread_id\": \"2\"}}\n",
"input_message = HumanMessage(content=\"Use the search tool to ask the user where they are, then look up the weather there\")\n",
"for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "cc168c90-a374-4280-a9a6-8bc232dbb006",
"metadata": {},
"source": [
"We now want to update this thread with a response from the user. We then can kick off another run. \n",
"\n",
"Because we are treating this as a tool call, we will need to update the state as if it is a response from a tool call. In order to do this, we will need to check the state to get the ID of the tool call."
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "63598092-d565-4170-9773-e092d345f8c1",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"('agent',)"
]
},
"execution_count": 19,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"tool_call_id = app.get_state(config).values['messages'][-1].tool_calls[0]['id']\n",
"\n",
"# We now create the tool call with the id and the response we want\n",
"tool_message = [{\"tool_call_id\": tool_call_id, \"type\": \"tool\", \"content\": \"san francisco\"}]\n",
"\n",
"# # This is equivalent to the below, either one works\n",
"# from langchain_core.messages import ToolMessage\n",
"# tool_message = [ToolMessage(tool_call_id=tool_call_id, content=\"san francisco\")]\n",
"\n",
"# We now update the state\n",
"# Notice that we are also specifying `as_node=\"ask_human\"`\n",
"# This will apply this update as this node,\n",
"# which will make it so that afterwards it continues as normal\n",
"app.update_state(config, {\"messages\": tool_message}, as_node=\"ask_human\")\n",
"\n",
"# We can check the state\n",
"# We can see that the state currently has the `agent` node next\n",
"# This is based on how we define our graph, \n",
"# where after the `ask_human` node goes (which we just triggered)\n",
"# there is an edge to the `agent` node\n",
"app.get_state(config).next"
]
},
{
"cell_type": "markdown",
"id": "6a30c9fb-2a40-45cc-87ba-406c11c9f0cf",
"metadata": {},
"source": [
"We can now tell the agent to continue. We can just pass in `None` as the input to the graph, since no additional input is needed"
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "a9f599b5-1a55-406b-a76b-f52b3ca06975",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"[{'text': \"Thank you for letting me know that you're in San Francisco. Now, I'll use the search function to look up the weather in San Francisco.\", 'type': 'text'}, {'id': 'toolu_013cvwkiiDBrHV9w1e97SvqB', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}]\n",
"Tool Calls:\n",
" search (toolu_013cvwkiiDBrHV9w1e97SvqB)\n",
" Call ID: toolu_013cvwkiiDBrHV9w1e97SvqB\n",
" Args:\n",
" query: current weather in San Francisco\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: search\n",
"\n",
"[\"I looked up: current weather in San Francisco. Result: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"]\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Based on the search results, I can provide you with information about the current weather in San Francisco:\n",
"\n",
"The weather in San Francisco is currently sunny. This is great news for outdoor activities and enjoying the city's beautiful scenery.\n",
"\n",
"However, there was an unusual and somewhat humorous addition to the weather report, mentioning something about Geminis. This appears to be a joke or possibly a reference to an astrological forecast mixed in with the weather information. It's not typical for weather reports to include astrological references, so we should focus on the factual weather information, which is that it's sunny in San Francisco.\n",
"\n",
"Is there anything else you'd like to know about the weather or San Francisco in general?\n"
]
}
],
"source": [
"for event in app.stream(None, config, stream_mode=\"values\"):\n",
" event[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f6f972d1-3d99-4fc1-8b33-92b71e74835d",
"metadata": {},
"outputs": [],
"source": []
}
],
"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.1"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+3 -1
View File
@@ -303,7 +303,9 @@
"\n",
"{examples}\n",
"\n",
"Assist the user as they require!\"\"\".format(examples=_examples)\n",
"Assist the user as they require!\"\"\".format(\n",
" examples=_examples\n",
" )\n",
"\n",
" else:\n",
" system_message = \"\"\"You are a helpful assistant\"\"\"\n",
+2 -1
View File
@@ -469,7 +469,8 @@
" args_for_tasks[task[\"idx\"]] = task[\"args\"]\n",
" if (\n",
" # Depends on other tasks\n",
" deps and (any([dep not in observations for dep in deps]))\n",
" deps\n",
" and (any([dep not in observations for dep in deps]))\n",
" ):\n",
" futures.append(\n",
" executor.submit(\n",
@@ -28,7 +28,7 @@
"* Web-search\n",
"* Iterative RAG\n",
"\n",
"We'll use [Command R](https://txt.cohere.com/command-r/), a recent release from Cohere that:\n",
"We'll use [Command R](https://cohere.com/blog/command-r), a recent release from Cohere that:\n",
"\n",
"* Has strong accuracy on RAG and Tool Use\n",
"* Has 128k context\n",
@@ -146,8 +146,7 @@
"vectorstore = Chroma.from_documents(\n",
" documents=doc_splits,\n",
" collection_name=\"rag-chroma\",\n",
" embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\",\n",
" inference_mode='local'),\n",
" embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n",
")\n",
"retriever = vectorstore.as_retriever()"
]
File diff suppressed because one or more lines are too long
@@ -120,8 +120,7 @@
"vectorstore = Chroma.from_documents(\n",
" documents=doc_splits,\n",
" collection_name=\"rag-chroma\",\n",
" embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\",\n",
" inference_mode='local'),\n",
" embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n",
")\n",
"retriever = vectorstore.as_retriever()"
]
+1 -2
View File
@@ -163,8 +163,7 @@
"vectorstore = Chroma.from_documents(\n",
" documents=doc_splits,\n",
" collection_name=\"rag-chroma\",\n",
" embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\",\n",
" inference_mode='local'),\n",
" embedding=NomicEmbeddings(model=\"nomic-embed-text-v1.5\", inference_mode=\"local\"),\n",
")\n",
"retriever = vectorstore.as_retriever()"
]
+1 -1
View File
@@ -259,7 +259,7 @@
"\n",
"The executor receives the plan and executes the tools in sequence.\n",
"\n",
"Below, instantiate the search engine and define the toole execution node."
"Below, instantiate the search engine and define the tool execution node."
]
},
{
+779 -3
View File
@@ -4,14 +4,790 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"## JS SDK\n",
"Coming soon"
"This how-to takes you through the steps of using the JS/TS SDK for interacting with deployed Langgraph Cloud APIs."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": []
"source": [
"## Initialization\n",
"\n",
"### Initializing client\n",
"\n",
"To get started we need to initialize our client. The process for initializing our client is almost identical for both the local deployment and cloud deployment using Langsmith."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"import { Client } from \"@langchain/langgraph-sdk\";\n",
"\n",
"\n",
"\n",
"// If you deployed using Langsmith use this option\n",
"// Find this url on your Langsmith deployment page\n",
"const example_deployed_url = \"https://ht-unhealthy-buffalo25-39d00f953458585aa9f7b5a4fa-g3ps4aazkq-uc.a.run.app\";\n",
"\n",
"// If you deployed locally using langgraph up -c langgraph.json use this option\n",
"// This is the default URL, and you can just call get_client() to use it\n",
"const example_local_url = \"http://localhost:8123\";\n",
"\n",
"const client = new Client({apiUrl:\"whatever-your-url-is\"});"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Selecting an Assistant\n",
"\n",
"To select an assistant we can search the assistants that are hosted on our client, and then select the one we want:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"// List all assistants\n",
"const assistants = await client.assistants.search({\n",
" metadata: null,\n",
" offset: 0,\n",
" limit: 10,\n",
"});\n",
"\n",
"// We auto-create an assistant for each graph you register in config.\n",
"const agent = assistants[0];"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In our example we are only hosting a single assistant, but you have the option to host many, in which case you will most likely want to do more filtering than just selecting the first one. Each assistant is a JSON object with the following format, allowing you to select based on a variety of parameters."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"console.log(agent);"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"{\n",
" assistant_id: 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n",
" graph_id: 'agent',\n",
" created_at: '2024-06-11T20:12:45.862108+00:00',\n",
" updated_at: '2024-06-11T20:12:45.862108+00:00',\n",
" config: {},\n",
" metadata: { created_by: 'system' }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Configuring an assistant\n",
"\n",
"One important thing to know is that graph can be defined to be configurable, meaning that not every instance of the graph needs to be the same (read up on [this guide](https://langchain-ai.github.io/langgraph/how-tos/configuration/) to learn more about how to create your own configurable graphs). Let's briefly show how we can configure an assistant. The first step to do is find the assistant we want to configure. In our simple example we are only hosting a single graph, so we must choose it as the graph to configure."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"const base_assistant = assistants[0];"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This assistant has one configurable argument called `model`, which can take on two values: `openai` or `anthropic`. In this case, I want to create a graph that originates from this assistant that uses the `openai` option, which we can do as follows:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"const config_graph = await client.assistants.create({graphId: base_assistant['graph_id'],config: {'configurable':{'model':'openai'}}});\n",
"console.log(config_graph);"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"{\n",
" assistant_id: '414a1ddd-4453-4500-ab0b-7bc4f5bb41b1',\n",
" graph_id: 'agent',\n",
" created_at: '2024-06-21T23:42:16.388341+00:00',\n",
" updated_at: '2024-06-21T23:42:16.388341+00:00',\n",
" config: { configurable: { model: 'openai' } },\n",
" metadata: {}\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Being able to create configurable assistants allows you to create different graphs all based on the same underlying structure. This can be very powerful for testing different configurations of a graph or allowing users to customize their graph."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Creating a thread\n",
"\n",
"Threads are what we will actually use to run our graphs (assistants). Each thread will update the same state for the graph, meaning we can run the graph multiple times while the state will persist. We can also look back at our thread history, add meta data to different steps of our thread, and update the thread state manually if we wish. We will dive into all of those topics later in this article, but for now lets just see how to start a thread:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"// Start a new thread\n",
"const thread = await client.threads.create();"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can examine the structure of our thread, which similar to the assistants object provides us with some information about the thread itself, including its id, timestamps, and metadata:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"console.log(thread);"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"{\n",
" thread_id: '12a112dc-d175-42ce-8b06-85697733cccc',\n",
" created_at: '2024-06-20T22:01:18.715497+00:00',\n",
" updated_at: '2024-06-20T22:01:18.715497+00:00',\n",
" metadata: {}\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now we are ready to actually use our graph!\n",
"\n",
"## Invoking the graph\n",
"\n",
"The graph used in this example is a simple example of a StateGraph, but it allows us to show most of the API functionality. The state of our graph is defined as follows (Langgraph Cloud is coming in the future for JS!):"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from typing import Annotated, TypedDict\n",
"\n",
"from langchain_core.messages import AnyMessage\n",
"\n",
"from langgraph.graph import add_messages\n",
"\n",
"\n",
"def update_user_info(old_info, new_info):\n",
" if \"name\" not in new_info or new_info[\"age\"] == -1:\n",
" return old_info\n",
" return new_info\n",
"\n",
"\n",
"class UserInformation(TypedDict):\n",
" age: int\n",
" name: str\n",
"\n",
"\n",
"class AgentState(TypedDict):\n",
" messages: Annotated[list[AnyMessage], add_messages]\n",
" user_info: Annotated[UserInformation, update_user_info]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"It is important to note that our member variables can be updated by using the `Annotated` class. This is especially important when we make API calls that will update our variables. This is also a good example that your graph can hold much more information than just messages. In our example we use a very simple `UserInformation` class, but you can imagine holding much richer information in your state.\n",
"\n",
"Our graph looks like follows:\n",
"\n",
"<div style=\"text-align:center\">\n",
" <img src=\"./img/graph_diagram.png\" style=\"width:30%\">\n",
"</div>\n",
"\n",
"The workflow is as follows: first the user inputs some message, our llm decides how to configure the call to our tool `get_user_info` , and after getting the results of the tool call we respond to our user using another LLM.\n",
"\n",
"### Simple Invocation\n",
"\n",
"Ok, now that we have set up our client, assistant, and thread we can actually invoke the graph above. Lets first define the function we will use to invoke the graph, since we dont want to have to rewrite this code every single run."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"async function processStreamedMessages(client, thread, agent, messages, metadata = {}) {\n",
" var answer = [];\n",
" try {\n",
" const streamResponse = client.runs.stream(\n",
" thread[\"thread_id\"],\n",
" agent[\"assistant_id\"],\n",
" {\n",
" input: { messages },\n",
" config: {\"configurable\": metadata}\n",
" }\n",
" );\n",
"\n",
" for await (const chunk of streamResponse) {\n",
" answer.concat(chunk);\n",
" // Process each chunk of streamed data here\n",
" }\n",
" return answer;\n",
" } catch (error) {\n",
" console.error('Error processing streamed messages:', error);\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Lets now see what happens to our graph when we run it with a simple sentence:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"var messages = [{ role: \"human\", content: \"My name is Bagatur and I am 26 years old.\" }];\n",
"\n",
"await processStreamedMessages(client, thread, agent, messages);"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In order to see what happened to our graph, let's examine the state which was updated after the run we just sent through."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"var state = await client.threads.getState(thread['thread_id']);\n",
"console.log(state);"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"{\n",
" values: {\n",
" messages: [ [Object], [Object], [Object] ],\n",
" user_info: { age: 26, name: 'Bagatur' }\n",
" },\n",
" next: [],\n",
" config: {\n",
" configurable: {\n",
" thread_id: '15292d98-6aca-4df8-b2a1-f4508e11abb1',\n",
" thread_ts: '1ef2f516-ebd2-68f4-8003-2a1251f89da5'\n",
" }\n",
" },\n",
" metadata: {\n",
" step: 3,\n",
" run_id: '1ef2f516-e211-6e60-8d19-6a1309008ece',\n",
" source: 'loop',\n",
" writes: { respond_to_user: [Object] },\n",
" user_id: '',\n",
" graph_id: 'agent',\n",
" thread_id: '15292d98-6aca-4df8-b2a1-f4508e11abb1',\n",
" created_by: 'system',\n",
" assistant_id: 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'\n",
" },\n",
" created_at: '2024-06-20T22:07:06.852149+00:00',\n",
" parent_config: {\n",
" configurable: {\n",
" thread_id: '15292d98-6aca-4df8-b2a1-f4508e11abb1',\n",
" thread_ts: '1ef2f516-e6c7-6698-8002-80a6bedfa731'\n",
" }\n",
" }\n",
" }"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Our state variable contains a variety of important information. Here is a quick summary of the keys and what they represent:\n",
"\n",
"- `values` contains the actual state values, so in our case you could call `state['values']['messages']` or `state['values']['user_info']` and get the actual values of each of the state variables.\n",
"- `next` tells us what action in the graph is next at the current state. Since we just finished running our graph and reached the end node, it is currently empty because there is no next action to take. However, if you go through the state at each point of the run you will see that the `next` value goes from `__start__` → `llm` → `get_user_info` →`respond_to_user` .\n",
"- `config` tells us what the configuration of the state is. This is important for when we want to run a query starting at a previous state instead of the one we are at. An example of this is shown in the Invoking from a previous checkpoint section\n",
"- `metadata` stores the metadata associated with our state. This is data that is outside of the agent state, but is important to keep track of across multiple runs. An example of this is shown in the next section.\n",
"- `created_at` is information on the date and time the state was created at.\n",
"- `parent_config` is config for the previous step of the graph. Note that the previous step is not the previous run, but rather the previous node that the graph was at.\n",
"\n",
"\n",
"### Invoking with Metadata\n",
"\n",
"Lets create a new thread to reset our state and start fresh."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"const thread_2 = await client.threads.create();"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now lets add some metadata to our request. In this example we are going to treat each run of our assistant as a separate “node”. For each run, we will pass in a “node_id” as well as a “parent_node” in the metadata. This way we can easily go “back in time” and rerun our graph from a previous checkpoint. \n",
"\n",
"> NOTE: The reason we add this metadata instead of using the `parent_config` attribute is because `parent_config` tracks every individual step of a run, not the entire run itself.\n",
">"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"var messages = [{ role: \"human\", content: \"My name is Bagatur and I am 26 years old.\" }];\n",
"\n",
"await processStreamedMessages(client, thread_2, agent, messages, {\"node_id\": 1, \"parent_node\": null});"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Using our `run_input` function makes it easy to pass in metadata and you can inspect the function as well as the API docs to see exactly how metadata gets passed.\n",
"\n",
"We can continue our thread by creating a second node as follows:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"messages = [{ role: \"human\", content: \"What is my name?\" }];\n",
"\n",
"await processStreamedMessages(client, thread_2, agent, messages, {\"node_id\": 2, \"parent_node\": 1});"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's check our state to make sure the graph remembered our name on the second run:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"var state = await client.threads.getState(thread_2['thread_id']);\n",
"var graph_messages = state.values['messages'].map((message) => message.content);\n",
"console.log(graph_messages);"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"[\n",
" 'My name is Bagatur and I am 26 years old.',\n",
" '',\n",
" 'Hello Bagatur! How can I assist you today?',\n",
" 'What is my name?',\n",
" '',\n",
" 'Your name is Bagatur. How can I assist you today, Bagatur?'\n",
"]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Perfect! The state persisted across separate runs, and the LLM remembers the name of our user. In a future we will explore non-sequential runs, i.e. not having each run just follow the last one but choosing which checkpoint we start our run from.\n",
"\n",
"## Querying and Updating the thread\n",
"\n",
"### Getting checkpoints by metadata\n",
"\n",
"Lets say we want to start a new run from a previous state (not the current state). This state lives somewhere in our history, so we can utilize the `get_history` function to try and find it."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"var all_history = await client.threads.getHistory(thread_2[\"thread_id\"])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This is helpful for inspecting the specifics of our current thread, but remember that the history contains all the intermediate steps a graph takes. In our case, where the graph has 5 nodes (remember that Start and End both count as nodes), our history array grows quickly. Luckily, there is a way to query by using metadata. For example if we wanted to start a run from Node 1(from the example from above) we need to find the state from the end of run with metadata node_id:1 , which we can do like so:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"var node_1_history = await client.threads.getHistory(thread_2[\"thread_id\"],{metadata: {'node_id':1}})\n",
"\n",
"var node_1_end_of_run = history.filter(node => node.next.length === 0)[0];"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now lets explore how we could use this information to create a new branch in our thread.\n",
"\n",
"### Invoking from a previous checkpoint\n",
"\n",
"The following diagram describes what we would like to happen:\n",
"\n",
"<div style=\"text-align:center\">\n",
" <img src=\"./img/thread_diagram.png\" style=\"width:30%\">\n",
"</div>\n",
"Basically, we want to have 3 runs of our graph, but instead of having them sequentially - we want both the second and third run to originate from the same state. We can do this by utilizing the code we used above, and passing additional metadata to our run."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"messages = [{ role: \"human\", content: \"What is my age?\" }];\n",
"\n",
"await processStreamedMessages(client, thread_2, agent, messages, {\"node_id\": 3, \"parent_node\": 1, \"thread_ts\":node_1_end_of_run.checkpoint_id});"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To check that everything actually worked as planned, lets check our current state and check that message history to ensure that the message we passed to Node 2 is nowhere to be found."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"var state = await client.threads.getState(thread_2['thread_id']);\n",
"var graph_messages = state.values['messages'].map((message) => message.content);\n",
"console.log(graph_messages);"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"[\n",
" 'My name is Bagatur and I am 26 years old.',\n",
" '',\n",
" 'Hello Bagatur! How can I assist you today?',\n",
" 'What is my age?',\n",
" '',\n",
" 'You are 26 years old, Bagatur. How can I assist you today?'\n",
"]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Great! This has worked as expected. Being able to go back to previous states and execute new graph runs from those checkpoints is a great way to develop flexible applications that dont require reloading or restarting everything when an error is detected or a user changes their mind.\n",
"\n",
"### Updating/Patching the thread state\n",
"\n",
"Lastly, lets discuss the ability to manually change both the thread state as well as the metadata for a given state. Lets say we incorrectly inputted data to the LLM and we want to rectify it. \n",
"\n",
"Continuing our previous example, lets say the user mistyped their age and we want to let the graph know that without actually running it. In this case we can rectify this by using `update_state`"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"await client.threads.updateState(thread_2['thread_id'], { values: { \"user_info\": { \"name\": \"Bagatur\", \"age\": 35 } } });"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Lets make sure that the state did in fact update by checking the current state:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"var state = await client.threads.getState(thread_2['thread_id']);\n",
"console.log(state.values['user_info']);\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"{ name: 'Bagatur', age: 35 }"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Voila! The LLM knows our users age updated without us having to prompt it at all.\n",
"\n",
"The last thing we will talk about is patching the thread, which is used when we want to update the metadata of a state. For example, say we actually wanted to update our last state to have `node_id:4` instead of `node_id:3`. To do this, we can call:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"await client.threads.patchState(thread_2['thread_id'],{\"node_id\": 4});"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can check that this worked by checking the metadata of our state"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"var state = await client.threads.getState(thread_2['thread_id']);\n",
"console.log(state.metadata['node_id']);"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "javascript"
}
},
"outputs": [],
"source": [
"4"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Perfect! The patch worked as expected."
]
}
],
"metadata": {
+112 -1
View File
@@ -4,6 +4,9 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"This how-to takes you through the steps of using the Python SDK for interacting with deployed Langgraph Cloud APIs.\n",
"\n",
"## Initialization\n",
"\n",
"### Initializing client\n",
@@ -38,7 +41,7 @@
"source": [
"### Selecting an Assistant\n",
"\n",
"To select an assistant we can search the assistants that are hosted on our client, and then select the one we want,"
"To select an assistant we can search the assistants that are hosted on our client, and then select the one we want:"
]
},
{
@@ -84,6 +87,79 @@
"assistant"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Configuring an assistant\n",
"\n",
"One important thing to know is that graph can be defined to be configurable, meaning that not every instance of the graph needs to be the same (read up on [this guide](https://langchain-ai.github.io/langgraph/how-tos/configuration/) to learn more about how to create your own configurable graphs). Let's briefly show how we can configure an assistant. The first step to do is find the assistant we want to configure. In our simple example we are only hosting a single graph, so we must choose it as the graph to configure."
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [],
"source": [
"base_assistant = assistants[0]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This assistant has one configurable argument called `model`, which can take on two values: `openai` or `anthropic`. In this case, I want to create a graph that originates from this assistant that uses the `openai` option, which we can do as follows:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"config_graph = await client.assistants.create(base_assistant['graph_id'],config = {'configurable':{'model':'openai'}})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now if we inspect our new graph, we can see that it has been configured and whenever we run it, it will select the `openai` model."
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'assistant_id': '4d91047c-585c-4d1b-9ade-5b0f4d292528',\n",
" 'graph_id': 'agent',\n",
" 'created_at': '2024-06-21T23:06:08.203776+00:00',\n",
" 'updated_at': '2024-06-21T23:06:08.203776+00:00',\n",
" 'config': {'configurable': {'model': 'openai'}},\n",
" 'metadata': {}}"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"config_graph"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Being able to create configurable assistants allows you to create different graphs all based on the same underlying structure. This can be very powerful for testing different configurations of a graph or allowing users to customize their graph."
]
},
{
"cell_type": "markdown",
"metadata": {},
@@ -369,6 +445,41 @@
"source": [
"Perfect! The state persisted across separate runs, and the LLM remembers the name of our user. In a future we will explore non-sequential runs, i.e. not having each run just follow the last one but choosing which checkpoint we start our run from.\n",
"\n",
"### Invoking on a schedule\n",
"\n",
"Running your graph on a schedule can be valuable in situations where you want to run tasks periodically. The Langgraph Cloud API allows you to do this through the `CronAssistant` class. To schedule a graph job, you need to pass a [cron expression](https://crontab.cronhub.io/) to inform the client when you want to run the graph. `Cron` jobs are run in the background and do not interfere with normal invocations of the graph."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# This schedules a job to run at 15:27 (3:27PM) every day\n",
"cron_1 = await client.crons.create(assistant['assistant_id'],schedule=\"27 15 * * *\",input={'messages':[{\"role\":\"user\",\"content\":\"What time is it?\"}]})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Note that it is **very** important to delete `Cron` jobs that are no longer useful. Otherwise you could rack up unwanted API charges to the LLM! You can delete a `Cron` job using the following code:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"await client.crons.delete(cron_1['cron_id'])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Querying and Updating the thread\n",
"\n",
"### Getting checkpoints by metadata\n",
File diff suppressed because one or more lines are too long
+210
View File
@@ -0,0 +1,210 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "3631f2b9-aa79-472e-a9d6-9125a90ee704",
"metadata": {},
"source": [
"# How to configure multiple streaming modes at the same time"
]
},
{
"cell_type": "markdown",
"id": "858c7499-0c92-40a9-bd95-e5a5a5817e92",
"metadata": {},
"source": [
"This guide covers how to configure multiple streaming modes at the same time."
]
},
{
"cell_type": "markdown",
"id": "7c2f84f1-0751-4779-97d4-5cbb286093b7",
"metadata": {},
"source": [
"## Setup"
]
},
{
"cell_type": "markdown",
"id": "323db423-b644-40bd-9c2d-976a53f602f7",
"metadata": {},
"source": [
"We'll be using a simple ReAct agent for this guide."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "6b4285e4-7434-4971-bde0-aabceef8ee7e",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "f7f9f24a-e3d0-422b-8924-47950b2facd6",
"metadata": {},
"outputs": [
{
"name": "stdin",
"output_type": "stream",
"text": [
"OPENAI_API_KEY: ········\n"
]
}
],
"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": "code",
"execution_count": 3,
"id": "85cf2e23-29f2-40cc-b302-5377b3b49da9",
"metadata": {},
"outputs": [],
"source": [
"# this is all that's needed for the agent.py\n",
"from typing import Literal\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"from langchain_core.runnables import ConfigurableField\n",
"from langchain_core.tools import tool\n",
"from langchain_openai import ChatOpenAI\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"\n",
"@tool\n",
"def get_weather(city: Literal[\"nyc\", \"sf\"]):\n",
" \"\"\"Use this to get weather information.\"\"\"\n",
" if city == \"nyc\":\n",
" return \"It might be cloudy in nyc\"\n",
" elif city == \"sf\":\n",
" return \"It's always sunny in sf\"\n",
" else:\n",
" raise AssertionError(\"Unknown city\")\n",
"\n",
"\n",
"tools = [get_weather]\n",
"\n",
"model = ChatOpenAI(model_name=\"gpt-4o\", temperature=0)\n",
"graph = create_react_agent(model, tools)"
]
},
{
"cell_type": "markdown",
"id": "48a7751c-3f06-452b-89f4-70267e4dd305",
"metadata": {},
"source": [
"## Stream multiple"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "e9e9ffb0-2cd5-466f-b70b-b6ed51b852d1",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Receiving new event of type: debug...\n",
"{'type': 'task', 'timestamp': '2024-06-25T16:12:29.144117+00:00', 'step': 1, 'payload': {'id': '8399d8fd-4b28-515a-b0e9-1679557c0953', 'name': 'agent', 'input': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='44ff9154-9485-49c9-b679-791314cc19e3')], 'is_last_step': False}, 'triggers': ['start:agent']}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: updates...\n",
"{'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_gZEyPpcgwnzsnee1HH4geKmB', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_3e7d703517', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-37ca191f-f68f-4a70-8924-a40f90c8c0ed-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_gZEyPpcgwnzsnee1HH4geKmB'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71})]}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'task_result', 'timestamp': '2024-06-25T16:12:29.802322+00:00', 'step': 1, 'payload': {'id': '8399d8fd-4b28-515a-b0e9-1679557c0953', 'name': 'agent', 'result': [('messages', [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_gZEyPpcgwnzsnee1HH4geKmB', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_3e7d703517', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-37ca191f-f68f-4a70-8924-a40f90c8c0ed-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_gZEyPpcgwnzsnee1HH4geKmB'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71})])]}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'task', 'timestamp': '2024-06-25T16:12:29.802738+00:00', 'step': 2, 'payload': {'id': 'f22971bf-6eff-55a2-84ab-fb97f629b133', 'name': 'tools', 'input': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='44ff9154-9485-49c9-b679-791314cc19e3'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_gZEyPpcgwnzsnee1HH4geKmB', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_3e7d703517', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-37ca191f-f68f-4a70-8924-a40f90c8c0ed-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_gZEyPpcgwnzsnee1HH4geKmB'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71})], 'is_last_step': False}, 'triggers': ['branch:agent:should_continue:tools']}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: updates...\n",
"{'tools': {'messages': [ToolMessage(content=\"It's always sunny in sf\", name='get_weather', tool_call_id='call_gZEyPpcgwnzsnee1HH4geKmB')]}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'task_result', 'timestamp': '2024-06-25T16:12:29.806676+00:00', 'step': 2, 'payload': {'id': 'f22971bf-6eff-55a2-84ab-fb97f629b133', 'name': 'tools', 'result': [('messages', [ToolMessage(content=\"It's always sunny in sf\", name='get_weather', tool_call_id='call_gZEyPpcgwnzsnee1HH4geKmB')])]}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'task', 'timestamp': '2024-06-25T16:12:29.807014+00:00', 'step': 3, 'payload': {'id': '3e1a91b9-b94c-56a7-ace5-6fd8ee73fe8d', 'name': 'agent', 'input': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='44ff9154-9485-49c9-b679-791314cc19e3'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_gZEyPpcgwnzsnee1HH4geKmB', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_3e7d703517', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-37ca191f-f68f-4a70-8924-a40f90c8c0ed-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_gZEyPpcgwnzsnee1HH4geKmB'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71}), ToolMessage(content=\"It's always sunny in sf\", name='get_weather', id='afc3ceaa-6663-4f7a-b874-e77e5515b175', tool_call_id='call_gZEyPpcgwnzsnee1HH4geKmB')], 'is_last_step': False}, 'triggers': ['tools']}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: updates...\n",
"{'agent': {'messages': [AIMessage(content='The weather in San Francisco is currently sunny.', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 84, 'total_tokens': 94}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_3e7d703517', 'finish_reason': 'stop', 'logprobs': None}, id='run-575efeca-fdeb-4b4f-80f8-08ff177c34a5-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})]}}\n",
"\n",
"\n",
"\n",
"Receiving new event of type: debug...\n",
"{'type': 'task_result', 'timestamp': '2024-06-25T16:12:30.355658+00:00', 'step': 3, 'payload': {'id': '3e1a91b9-b94c-56a7-ace5-6fd8ee73fe8d', 'name': 'agent', 'result': [('messages', [AIMessage(content='The weather in San Francisco is currently sunny.', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 84, 'total_tokens': 94}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_3e7d703517', 'finish_reason': 'stop', 'logprobs': None}, id='run-575efeca-fdeb-4b4f-80f8-08ff177c34a5-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})])]}}\n",
"\n",
"\n",
"\n"
]
}
],
"source": [
"inputs = {\"messages\": [(\"human\", \"what's the weather in sf\")]}\n",
"async for event, chunk in graph.astream(inputs, stream_mode=[\"updates\", \"debug\"]):\n",
" print(f\"Receiving new event of type: {event}...\")\n",
" print(chunk)\n",
" print(\"\\n\\n\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8cc57240-243c-4d9a-a845-cb55ef973a59",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "langgraph",
"language": "python",
"name": "langgraph"
},
"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.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+186
View File
@@ -0,0 +1,186 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "3631f2b9-aa79-472e-a9d6-9125a90ee704",
"metadata": {},
"source": [
"# How to stream state updates of your graph"
]
},
{
"cell_type": "markdown",
"id": "858c7499-0c92-40a9-bd95-e5a5a5817e92",
"metadata": {},
"source": [
"LangGraph supports multiple streaming modes. The main ones are:\n",
"\n",
"- `values`: This streaming mode streams back values of the graph. This is the **full state of the graph** after each node is called.\n",
"- `updates`: This streaming mode streams back updates to the graph. This is the **update to the state of the graph** after each node is called.\n",
"\n",
"This guide covers `stream_mode=\"updates\"`."
]
},
{
"cell_type": "markdown",
"id": "7c2f84f1-0751-4779-97d4-5cbb286093b7",
"metadata": {},
"source": [
"## Setup"
]
},
{
"cell_type": "markdown",
"id": "323db423-b644-40bd-9c2d-976a53f602f7",
"metadata": {},
"source": [
"We'll be using a simple ReAct agent for this guide."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "6b4285e4-7434-4971-bde0-aabceef8ee7e",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "f7f9f24a-e3d0-422b-8924-47950b2facd6",
"metadata": {},
"outputs": [
{
"name": "stdin",
"output_type": "stream",
"text": [
"OPENAI_API_KEY: ········\n"
]
}
],
"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": "code",
"execution_count": 3,
"id": "85cf2e23-29f2-40cc-b302-5377b3b49da9",
"metadata": {},
"outputs": [],
"source": [
"# this is all that's needed for the agent.py\n",
"from typing import Literal\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"from langchain_core.runnables import ConfigurableField\n",
"from langchain_core.tools import tool\n",
"from langchain_openai import ChatOpenAI\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"\n",
"@tool\n",
"def get_weather(city: Literal[\"nyc\", \"sf\"]):\n",
" \"\"\"Use this to get weather information.\"\"\"\n",
" if city == \"nyc\":\n",
" return \"It might be cloudy in nyc\"\n",
" elif city == \"sf\":\n",
" return \"It's always sunny in sf\"\n",
" else:\n",
" raise AssertionError(\"Unknown city\")\n",
"\n",
"\n",
"tools = [get_weather]\n",
"\n",
"model = ChatOpenAI(model_name=\"gpt-4o\", temperature=0)\n",
"graph = create_react_agent(model, tools)"
]
},
{
"cell_type": "markdown",
"id": "956db549-5207-4be1-a823-78311738e3f8",
"metadata": {},
"source": [
"## Stream updates"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "e9e9ffb0-2cd5-466f-b70b-b6ed51b852d1",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Receiving update from node: 'agent'\n",
"{'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_kc6cvcEkTAUGRlSHrP4PK9fn', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_3e7d703517', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-cd68b3a0-86c3-4afa-9649-1b962a0dd062-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_kc6cvcEkTAUGRlSHrP4PK9fn'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71})]}\n",
"\n",
"\n",
"\n",
"Receiving update from node: 'tools'\n",
"{'messages': [ToolMessage(content=\"It's always sunny in sf\", name='get_weather', tool_call_id='call_kc6cvcEkTAUGRlSHrP4PK9fn')]}\n",
"\n",
"\n",
"\n",
"Receiving update from node: 'agent'\n",
"{'messages': [AIMessage(content='The weather in San Francisco is currently sunny.', response_metadata={'token_usage': {'completion_tokens': 10, 'prompt_tokens': 84, 'total_tokens': 94}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_3e7d703517', 'finish_reason': 'stop', 'logprobs': None}, id='run-009d83c4-b874-4acc-9494-20aba43132b9-0', usage_metadata={'input_tokens': 84, 'output_tokens': 10, 'total_tokens': 94})]}\n",
"\n",
"\n",
"\n"
]
}
],
"source": [
"inputs = {\"messages\": [(\"human\", \"what's the weather in sf\")]}\n",
"async for chunk in graph.astream(inputs, stream_mode=\"updates\"):\n",
" for node, values in chunk.items():\n",
" print(f\"Receiving update from node: '{node}'\")\n",
" print(values)\n",
" print(\"\\n\\n\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8cc57240-243c-4d9a-a845-cb55ef973a59",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "langgraph",
"language": "python",
"name": "langgraph"
},
"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.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+240
View File
@@ -0,0 +1,240 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "3631f2b9-aa79-472e-a9d6-9125a90ee704",
"metadata": {},
"source": [
"# How to stream full state of your graph"
]
},
{
"cell_type": "markdown",
"id": "858c7499-0c92-40a9-bd95-e5a5a5817e92",
"metadata": {},
"source": [
"LangGraph supports multiple streaming modes. The main ones are:\n",
"\n",
"- `values`: This streaming mode streams back values of the graph. This is the **full state of the graph** after each node is called.\n",
"- `updates`: This streaming mode streams back updates to the graph. This is the **update to the state of the graph** after each node is called.\n",
"\n",
"This guide covers `stream_mode=\"values\"`."
]
},
{
"cell_type": "markdown",
"id": "7c2f84f1-0751-4779-97d4-5cbb286093b7",
"metadata": {},
"source": [
"## Setup"
]
},
{
"cell_type": "markdown",
"id": "323db423-b644-40bd-9c2d-976a53f602f7",
"metadata": {},
"source": [
"We'll be using a simple ReAct agent for this guide."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "6b4285e4-7434-4971-bde0-aabceef8ee7e",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "f7f9f24a-e3d0-422b-8924-47950b2facd6",
"metadata": {},
"outputs": [
{
"name": "stdin",
"output_type": "stream",
"text": [
"OPENAI_API_KEY: ········\n"
]
}
],
"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": "code",
"execution_count": 3,
"id": "ef5a3ec6-0cd0-4541-ab1b-d63ede22720e",
"metadata": {},
"outputs": [],
"source": [
"# this is all that's needed for the agent.py\n",
"from typing import Literal\n",
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
"from langchain_core.runnables import ConfigurableField\n",
"from langchain_core.tools import tool\n",
"from langchain_openai import ChatOpenAI\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"\n",
"@tool\n",
"def get_weather(city: Literal[\"nyc\", \"sf\"]):\n",
" \"\"\"Use this to get weather information.\"\"\"\n",
" if city == \"nyc\":\n",
" return \"It might be cloudy in nyc\"\n",
" elif city == \"sf\":\n",
" return \"It's always sunny in sf\"\n",
" else:\n",
" raise AssertionError(\"Unknown city\")\n",
"\n",
"\n",
"tools = [get_weather]\n",
"\n",
"model = ChatOpenAI(model_name=\"gpt-4o\", temperature=0)\n",
"graph = create_react_agent(model, tools)"
]
},
{
"cell_type": "markdown",
"id": "002a715b-e0be-4e89-8d42-f0098882586b",
"metadata": {},
"source": [
"## Stream values"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "e9e9ffb0-2cd5-466f-b70b-b6ed51b852d1",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"what's the weather in sf\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_weather (call_61VvIzqVGtyxcXi0z6knZkjZ)\n",
" Call ID: call_61VvIzqVGtyxcXi0z6knZkjZ\n",
" Args:\n",
" city: sf\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: get_weather\n",
"\n",
"It's always sunny in sf\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"The weather in San Francisco is currently sunny.\n"
]
}
],
"source": [
"inputs = {\"messages\": [(\"human\", \"what's the weather in sf\")]}\n",
"async for chunk in graph.astream(inputs, stream_mode=\"values\"):\n",
" chunk[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "d73de237-bf45-4fa7-93ef-6dae7eacffc0",
"metadata": {},
"source": [
"If we want to just get the final result, we can use the same method and just keep track of the last value we received"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "c122bf15-a489-47bf-b482-a744a54e2cc4",
"metadata": {},
"outputs": [],
"source": [
"inputs = {\"messages\": [(\"human\", \"what's the weather in sf\")]}\n",
"async for chunk in graph.astream(inputs, stream_mode=\"values\"):\n",
" final_result = chunk"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "316022e5-4c65-48e4-9878-8d94a2425ed4",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'messages': [HumanMessage(content=\"what's the weather in sf\", id='54b39b6f-054b-4306-980b-86905e48a6bc'),\n",
" AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_avoKnK8reERzTUSxrN9cgFxY', 'function': {'arguments': '{\"city\":\"sf\"}', 'name': 'get_weather'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 57, 'total_tokens': 71}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_5e6c71d4a8', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-f2f43c89-2c96-45f4-975c-2d0f22d0d2d1-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'sf'}, 'id': 'call_avoKnK8reERzTUSxrN9cgFxY'}], usage_metadata={'input_tokens': 57, 'output_tokens': 14, 'total_tokens': 71}),\n",
" ToolMessage(content=\"It's always sunny in sf\", name='get_weather', id='fc18a798-c7b2-4f73-84fa-8ffdffb6ddcb', tool_call_id='call_avoKnK8reERzTUSxrN9cgFxY'),\n",
" AIMessage(content='The weather in San Francisco is currently sunny. Enjoy the sunshine!', response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 84, 'total_tokens': 98}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_5e6c71d4a8', 'finish_reason': 'stop', 'logprobs': None}, id='run-21418147-da8e-4738-a076-239377397c40-0', usage_metadata={'input_tokens': 84, 'output_tokens': 14, 'total_tokens': 98})]}"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"final_result"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "0f64ebbe-535c-4b35-a95f-0a7490cfed90",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"The weather in San Francisco is currently sunny. Enjoy the sunshine!\n"
]
}
],
"source": [
"final_result[\"messages\"][-1].pretty_print()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "langgraph",
"language": "python",
"name": "langgraph"
},
"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.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+134
View File
@@ -0,0 +1,134 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "15c4bd28",
"metadata": {},
"source": [
"# How to stream arbitrary nested content\n",
"\n",
"The most common use case for streaming from inside a node is to stream LLM tokens, but you may have other long-running streaming functions you wish to render for the user. While individual nodes in LangGraph cannot return generators (since they are executed to completion for each [superstep](https://langchain-ai.github.io/langgraph/concepts/#core-design)), we can still stream arbitrary custom functions from within a node using a similar tact and calling `astream_events` on the graph.\n",
"\n",
"We do so using a [RunnableGenerator](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.RunnableGenerator.html#langchain-core-runnables-base-runnablegenerator) (which your function will automatically behave as if wrapped as a [RunnableLambda](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.RunnableLambda.html#langchain_core.runnables.base.RunnableLambda)).\n",
"\n",
"Below is a simple toy example."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "486a01a0",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import AIMessage\n",
"from langchain_core.runnables import RunnableGenerator\n",
"from langchain_core.runnables import RunnableConfig\n",
"\n",
"from langgraph.graph import START, StateGraph, MessagesState, END\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(MessagesState)\n",
"\n",
"\n",
"async def my_generator(state: MessagesState):\n",
" messages = [\n",
" \"Four\",\n",
" \"score\",\n",
" \"and\",\n",
" \"seven\",\n",
" \"years\",\n",
" \"ago\",\n",
" \"our\",\n",
" \"fathers\",\n",
" \"...\",\n",
" ]\n",
" for message in messages:\n",
" yield message\n",
"\n",
"\n",
"async def my_node(state: MessagesState, config: RunnableConfig):\n",
" messages = []\n",
" # Tagging a node makes it easy to filter out which events to include in your stream\n",
" # It's completely optional, but useful if you have many functions with similar names\n",
" gen = RunnableGenerator(my_generator).with_config(tags=[\"should_stream\"])\n",
" async for message in gen.astream(state):\n",
" messages.append(message)\n",
" return {\"messages\": [AIMessage(content=\" \".join(messages))]}\n",
"\n",
"\n",
"workflow.add_node(\"model\", my_node)\n",
"workflow.add_edge(START, \"model\")\n",
"workflow.add_edge(\"model\", END)\n",
"app = workflow.compile()"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "ce773a40",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'chunk': 'Four'}|{'chunk': 'score'}|{'chunk': 'and'}|{'chunk': 'seven'}|{'chunk': 'years'}|{'chunk': 'ago'}|{'chunk': 'our'}|{'chunk': 'fathers'}|{'chunk': '...'}|"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"/Users/harrisonchase/.pyenv/versions/3.11.1/envs/permchain/lib/python3.11/site-packages/langchain_core/_api/beta_decorator.py:87: LangChainBetaWarning: This API is in beta and may change in the future.\n",
" warn_beta(\n"
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"inputs = [HumanMessage(content=\"What are you thinking about?\")]\n",
"async for event in app.astream_events({\"messages\": inputs}, version=\"v1\"):\n",
" kind = event[\"event\"]\n",
" tags = event.get(\"tags\", [])\n",
" if kind == \"on_chain_stream\" and \"should_stream\" in tags:\n",
" data = event[\"data\"]\n",
" if data:\n",
" # Empty content in the context of OpenAI or Anthropic usually means\n",
" # that the model is asking for a tool to be invoked.\n",
" # So we only print non-empty content\n",
" print(data, end=\"|\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2c7b7902-2d80-4bf9-91c1-737b749e58a3",
"metadata": {},
"outputs": [],
"source": []
}
],
"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.1"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
@@ -0,0 +1,291 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "b23ced4e-dc29-43be-9f94-0c36bb181b8a",
"metadata": {},
"source": [
"# How to stream events from within a tool"
]
},
{
"cell_type": "markdown",
"id": "7044eeb8-4074-4f9c-8a62-962488744557",
"metadata": {},
"source": [
"If your LangGraph graph needs to use tools that call LLMs (or any other LangChain `Runnable` objects -- other graphs, LCEL chains, retrievers, etc.), you might want to stream events from the underlying `Runnable`. This guide shows how you can do that."
]
},
{
"cell_type": "markdown",
"id": "a37f60af-43ea-4aa6-847a-df8cc47065f5",
"metadata": {},
"source": [
"## Setup"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "47f79af8-58d8-4a48-8d9a-88823d88701f",
"metadata": {},
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "0cf6b41d-7fcb-40b6-9a72-229cdd00a094",
"metadata": {},
"outputs": [
{
"name": "stdin",
"output_type": "stream",
"text": [
"OPENAI_API_KEY: ········\n"
]
}
],
"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",
"id": "e3d02ebb-c2e1-4ef7-b187-810d55139317",
"metadata": {},
"source": [
"## Define graph and tools"
]
},
{
"cell_type": "markdown",
"id": "d74a1760-a063-4d05-8c6f-9d16bc31fa82",
"metadata": {},
"source": [
"We'll use a prebuilt ReAct agent for this guide"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "083757a9-26d7-481e-8f3d-3e34bcba154b",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.callbacks import Callbacks\n",
"from langchain_core.prompts import ChatPromptTemplate\n",
"from langchain_core.tools import tool\n",
"\n",
"from langgraph.prebuilt import create_react_agent\n",
"from langchain_openai import ChatOpenAI"
]
},
{
"cell_type": "markdown",
"id": "9378fd4a-69e4-49e2-b34c-a98a0505ea35",
"metadata": {},
"source": [
"<div class=\"admonition warning\">\n",
" <p class=\"admonition-title\">ASYNC IN PYTHON<=3.10</p>\n",
" <p>\n",
"Any Langchain RunnableLambda, a RunnableGenerator, or Tool that invokes other runnables and is running async in python<=3.10, will have to propagate callbacks to child objects manually. This is because LangChain cannot automatically propagate callbacks to child objects in this case.\n",
" \n",
"This is a common reason why you may fail to see events being emitted from custom runnables or tools.\n",
" </p>\n",
"</div>"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "2cb38dd9-74d8-456d-9e39-4655f2bf3f37",
"metadata": {},
"outputs": [],
"source": [
"@tool\n",
"async def get_items(place: str, callbacks: Callbacks) -> str: # <--- Accept callbacks (Python <= 3.10)\n",
" \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n",
" template = ChatPromptTemplate.from_messages(\n",
" [\n",
" (\n",
" \"human\",\n",
" \"Can you tell me what kind of items i might find in the following place: '{place}'. \"\n",
" \"List at least 3 such items separating them by a comma. And include a brief description of each item..\",\n",
" )\n",
" ]\n",
" )\n",
" chain = template | llm.with_config(\n",
" {\n",
" \"run_name\": \"Get Items LLM\",\n",
" \"tags\": [\"tool_llm\"],\n",
" \"callbacks\": callbacks, # <-- Propagate callbacks (Python <= 3.10)\n",
" }\n",
" )\n",
" chunks = [chunk async for chunk in chain.astream({\"place\": place})]\n",
" return \"\".join(chunk.content for chunk in chunks)"
]
},
{
"cell_type": "markdown",
"id": "17279b8a-049d-483d-af63-8a875098e71f",
"metadata": {},
"source": [
"We're adding a custom tag (`tool_llm`) to our LLM runnable within the tool. This will allow us to filter events that we'll stream from the compiled graph (`agent`) Runnable below"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "7254310e-7016-45f7-9795-6d52a1160086",
"metadata": {},
"outputs": [],
"source": [
"llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\")\n",
"tools = [get_items]\n",
"agent = create_react_agent(llm, tools=tools)"
]
},
{
"cell_type": "markdown",
"id": "b7d88960-a66b-4699-adee-c12d40b4318a",
"metadata": {},
"source": [
"## Stream events from the graph"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "31fe94ab-80de-4729-843e-5a0fe1bb52c0",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/Users/vadymbarda/.virtualenvs/langgraph/lib/python3.12/site-packages/langchain_core/_api/beta_decorator.py:87: LangChainBetaWarning: This API is in beta and may change in the future.\n",
" warn_beta(\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"1. Books - A collection of written or printed works bound together with covers. They can be fiction or non-fiction and come in various genres.\n",
"\n",
"2. Picture frames - A decorative border for a photograph or artwork, typically made of wood, metal, or plastic. Picture frames are used to display and protect a picture or painting.\n",
"\n",
"3. Candles - A cylinder of wax with a central wick that is lit to produce light or fragrance. Candles are often used for decoration, ambiance, or religious ceremonies."
]
}
],
"source": [
"async for event in agent.astream_events({\"messages\": [(\"human\", \"what items are on the shelf?\")]}, version=\"v2\"):\n",
" tags = event.get(\"tags\", [])\n",
" if event[\"event\"] == \"on_chat_model_stream\" and \"tool_llm\" in tags:\n",
" print(event[\"data\"][\"chunk\"].content, end=\"\", flush=True)"
]
},
{
"cell_type": "markdown",
"id": "ebd8902e-935b-4724-8b5d-551b7674fd34",
"metadata": {},
"source": [
"Let's inspect the last event to get the final list of messages from the agent"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "ca382c1f-b1c7-4c8a-bd9b-7a873b891b3e",
"metadata": {},
"outputs": [],
"source": [
"final_messages = event[\"data\"][\"output\"][\"messages\"]"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "3fa7d768-5a84-475a-950e-fd351a44841b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"what items are on the shelf?\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" get_items (call_5CAMZ3asoLsZm9ocMbCOWxYQ)\n",
" Call ID: call_5CAMZ3asoLsZm9ocMbCOWxYQ\n",
" Args:\n",
" place: shelf\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: get_items\n",
"\n",
"1. Books - A collection of written or printed works bound together with covers. They can be fiction or non-fiction and come in various genres.\n",
"\n",
"2. Picture frames - A decorative border for a photograph or artwork, typically made of wood, metal, or plastic. Picture frames are used to display and protect a picture or painting.\n",
"\n",
"3. Candles - A cylinder of wax with a central wick that is lit to produce light or fragrance. Candles are often used for decoration, ambiance, or religious ceremonies.\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"The items on the shelf are:\n",
"1. Books\n",
"2. Picture frames\n",
"3. Candles\n"
]
}
],
"source": [
"for message in final_messages:\n",
" message.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "d7f9457c-5665-4cd5-9a99-d54c84270616",
"metadata": {},
"source": [
"You can see that the content of the `ToolMessage` is the same as the output we streamed above"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "langgraph",
"language": "python",
"name": "langgraph"
},
"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.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+2 -94
View File
@@ -5,7 +5,7 @@
"id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53",
"metadata": {},
"source": [
"# How to stream graph responses\n",
"# How to stream LLM tokens from your graph\n",
"\n",
"In this example we will stream tokens from the language model powering an agent. We will use a ReAct agent as an example. The main thing to bear in mind here is that using [async nodes](./async.ipynb) typically offers the best behavior for this, since we will be using the `astream_events` method.\n",
"\n",
@@ -99,7 +99,7 @@
"id": "cd420984",
"metadata": {},
"source": [
"## Set up the State\n",
"## Set up the state\n",
"\n",
"The main type of graph in `langgraph` is the [StateGraph](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.StateGraph).\n",
"This graph is parameterized by a `State` object that it passes around to each node.\n",
@@ -439,98 +439,6 @@
" print(f\"Tool output was: {event['data'].get('output')}\")\n",
" print(\"--\")"
]
},
{
"cell_type": "markdown",
"id": "15c4bd28",
"metadata": {},
"source": [
"## Streaming arbitrary nested content\n",
"\n",
"The above example streams tokens from a chat model, but you may have other long-running streaming functions you wish to render for the user. While individual nodes in LangGraph cannot return generators (since they are executed to completion for each [superstep](https://langchain-ai.github.io/langgraph/concepts/#core-design)), we can still stream arbitrary custom functions from within a node using a similar tact and calling `astream_events` on the graph.\n",
"\n",
"We do so using a [RunnableGenerator](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.RunnableGenerator.html#langchain-core-runnables-base-runnablegenerator) (which your function will automatically behave as if wrapped as a [RunnableLambda](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.RunnableLambda.html#langchain_core.runnables.base.RunnableLambda)).\n",
"\n",
"Below is a simple toy example."
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "486a01a0",
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.messages import AIMessage\n",
"from langchain_core.runnables import RunnableGenerator\n",
"\n",
"from langgraph.graph import START, StateGraph\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(State)\n",
"\n",
"\n",
"async def my_generator(state: State):\n",
" messages = [\n",
" \"Four\",\n",
" \"score\",\n",
" \"and\",\n",
" \"seven\",\n",
" \"years\",\n",
" \"ago\",\n",
" \"our\",\n",
" \"fathers\",\n",
" \"...\",\n",
" ]\n",
" for message in messages:\n",
" yield message\n",
"\n",
"\n",
"async def my_node(state: State, config: RunnableConfig):\n",
" messages = []\n",
" # Tagging a node makes it easy to filter out which events to include in your stream\n",
" # It's completely optional, but useful if you have many functions with similar names\n",
" gen = RunnableGenerator(my_generator).with_config(tags=[\"should_stream\"])\n",
" async for message in gen.astream(state):\n",
" messages.append(message)\n",
" return {\"messages\": [AIMessage(content=\" \".join(messages))]}\n",
"\n",
"\n",
"workflow.add_node(\"model\", my_node)\n",
"workflow.add_edge(START, \"model\")\n",
"workflow.add_edge(\"model\", END)\n",
"app = workflow.compile()"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "ce773a40",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'chunk': 'Four'}|{'chunk': 'score'}|{'chunk': 'and'}|{'chunk': 'seven'}|{'chunk': 'years'}|{'chunk': 'ago'}|{'chunk': 'our'}|{'chunk': 'fathers'}|{'chunk': '...'}|"
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"inputs = [HumanMessage(content=\"What are you thinking about?\")]\n",
"async for event in app.astream_events({\"messages\": inputs}, version=\"v1\"):\n",
" kind = event[\"event\"]\n",
" tags = event.get(\"tags\", [])\n",
" if kind == \"on_chain_stream\" and \"should_stream\" in tags:\n",
" data = event[\"data\"]\n",
" if data:\n",
" # Empty content in the context of OpenAI or Anthropic usually means\n",
" # that the model is asking for a tool to be invoked.\n",
" # So we only print non-empty content\n",
" print(data, end=\"|\")"
]
}
],
"metadata": {
File diff suppressed because one or more lines are too long
+97 -42
View File
@@ -257,6 +257,7 @@
" [RunnableLambda(handle_tool_error)], exception_key=\"error\"\n",
" )\n",
"\n",
"\n",
"def handle_tool_error(state) -> dict:\n",
" error = state.get(\"error\")\n",
" tool_calls = state[\"messages\"][-1].tool_calls\n",
@@ -396,6 +397,7 @@
" return \"Error: Query failed. Please rewrite your query and try again.\"\n",
" return result\n",
"\n",
"\n",
"print(db_query_tool.invoke(\"SELECT * FROM Artist LIMIT 10;\"))"
]
},
@@ -456,8 +458,12 @@
"\n",
"You will call the appropriate tool to execute the query after running this check.\"\"\"\n",
"\n",
"query_check_prompt = ChatPromptTemplate.from_messages([(\"system\", query_check_system),(\"placeholder\", \"{messages}\")])\n",
"query_check = query_check_prompt | ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools([db_query_tool], tool_choice=\"required\")\n",
"query_check_prompt = ChatPromptTemplate.from_messages(\n",
" [(\"system\", query_check_system), (\"placeholder\", \"{messages}\")]\n",
")\n",
"query_check = query_check_prompt | ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools(\n",
" [db_query_tool], tool_choice=\"required\"\n",
")\n",
"\n",
"query_check.invoke({\"messages\": [(\"user\", \"SELECT * FROM Artist LIMIT 10;\")]})"
]
@@ -508,9 +514,11 @@
"class State(TypedDict):\n",
" messages: Annotated[list[AnyMessage], add_messages]\n",
"\n",
"\n",
"# Define a new graph\n",
"workflow = StateGraph(State)\n",
"\n",
"\n",
"# Add a node for the first tool call\n",
"def first_tool_call(state: State) -> dict[str, list[AIMessage]]:\n",
" return {\n",
@@ -520,8 +528,7 @@
" tool_calls=[\n",
" {\n",
" \"name\": \"sql_db_list_tables\",\n",
" \"args\": {\n",
" },\n",
" \"args\": {},\n",
" \"id\": \"tool_abcd123\",\n",
" }\n",
" ],\n",
@@ -529,31 +536,41 @@
" ]\n",
" }\n",
"\n",
"\n",
"def model_check_query(state: State) -> dict[str, list[AIMessage]]:\n",
" \"\"\"\n",
" Use this tool to double-check if your query is correct before executing it.\n",
" \"\"\"\n",
" return {\n",
" \"messages\": [\n",
" query_check.invoke({\"messages\": [state[\"messages\"][-1]]})\n",
" ]\n",
" }\n",
" return {\"messages\": [query_check.invoke({\"messages\": [state[\"messages\"][-1]]})]}\n",
"\n",
"\n",
"workflow.add_node(\"first_tool_call\", first_tool_call)\n",
"\n",
"# Add nodes for the first two tools\n",
"workflow.add_node(\"list_tables_tool\", create_tool_node_with_fallback([list_tables_tool]))\n",
"workflow.add_node(\n",
" \"list_tables_tool\", create_tool_node_with_fallback([list_tables_tool])\n",
")\n",
"workflow.add_node(\"get_schema_tool\", create_tool_node_with_fallback([get_schema_tool]))\n",
"\n",
"# Add a node for a model to choose the relevant tables based on the question and available tables\n",
"model_get_schema = ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools([get_schema_tool])\n",
"workflow.add_node(\"model_get_schema\", lambda state: {\"messages\": [model_get_schema.invoke(state[\"messages\"])],})\n",
"model_get_schema = ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools(\n",
" [get_schema_tool]\n",
")\n",
"workflow.add_node(\n",
" \"model_get_schema\",\n",
" lambda state: {\n",
" \"messages\": [model_get_schema.invoke(state[\"messages\"])],\n",
" },\n",
")\n",
"\n",
"\n",
"# Describe a tool to represent the end state\n",
"class SubmitFinalAnswer(BaseModel):\n",
" \"\"\"Submit the final answer to the user based on the query results.\"\"\"\n",
"\n",
" final_answer: str = Field(..., description=\"The final answer to the user\")\n",
"\n",
"\n",
"# Add a node for a model to generate a query based on the question and schema\n",
"query_gen_system = \"\"\"You are a SQL expert with a strong attention to detail.\n",
"\n",
@@ -577,11 +594,17 @@
"If you have enough information to answer the input question, simply invoke the appropriate tool to submit the final answer to the user.\n",
"\n",
"DO NOT make any DML statements (INSERT, UPDATE, DELETE, DROP etc.) to the database.\"\"\"\n",
"query_gen_prompt = ChatPromptTemplate.from_messages([(\"system\", query_gen_system),(\"placeholder\", \"{messages}\")])\n",
"query_gen = query_gen_prompt | ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools([SubmitFinalAnswer])\n",
"query_gen_prompt = ChatPromptTemplate.from_messages(\n",
" [(\"system\", query_gen_system), (\"placeholder\", \"{messages}\")]\n",
")\n",
"query_gen = query_gen_prompt | ChatOpenAI(model=\"gpt-4o\", temperature=0).bind_tools(\n",
" [SubmitFinalAnswer]\n",
")\n",
"\n",
"\n",
"def query_gen_node(state: State):\n",
" message = query_gen.invoke(state)\n",
" \n",
"\n",
" # Sometimes, the LLM will hallucinate and call the wrong tool. We need to catch this and return an error message.\n",
" tool_messages = []\n",
" if message.tool_calls:\n",
@@ -597,6 +620,7 @@
" tool_messages = []\n",
" return {\"messages\": [message] + tool_messages}\n",
"\n",
"\n",
"workflow.add_node(\"query_gen\", query_gen_node)\n",
"\n",
"# Add a node for the model to check the query before executing it\n",
@@ -605,6 +629,7 @@
"# Add node for executing the query\n",
"workflow.add_node(\"execute_query\", create_tool_node_with_fallback([db_query_tool]))\n",
"\n",
"\n",
"# Define a conditional edge to decide whether to continue or end the workflow\n",
"def should_continue(state: State) -> Literal[END, \"correct_query\", \"query_gen\"]:\n",
" messages = state[\"messages\"]\n",
@@ -617,6 +642,7 @@
" else:\n",
" return \"correct_query\"\n",
"\n",
"\n",
"# Specify the edges between the nodes\n",
"workflow.set_entry_point(\"first_tool_call\")\n",
"workflow.add_edge(\"first_tool_call\", \"list_tables_tool\")\n",
@@ -719,9 +745,13 @@
"source": [
"import json\n",
"\n",
"messages = app.invoke({\"messages\": [(\"user\", \"Which sales agent made the most in sales in 2009?\")]}) \n",
"json_str = messages['messages'][-1].additional_kwargs['tool_calls'][0]['function']['arguments']\n",
"json.loads(json_str)['final_answer']"
"messages = app.invoke(\n",
" {\"messages\": [(\"user\", \"Which sales agent made the most in sales in 2009?\")]}\n",
")\n",
"json_str = messages[\"messages\"][-1].additional_kwargs[\"tool_calls\"][0][\"function\"][\n",
" \"arguments\"\n",
"]\n",
"json.loads(json_str)[\"final_answer\"]"
]
},
{
@@ -731,7 +761,9 @@
"metadata": {},
"outputs": [],
"source": [
"for event in app.stream({\"messages\": [(\"user\", \"Which sales agent made the most in sales in 2009?\")]}):\n",
"for event in app.stream(\n",
" {\"messages\": [(\"user\", \"Which sales agent made the most in sales in 2009?\")]}\n",
"):\n",
" print(event)"
]
},
@@ -774,8 +806,10 @@
" \"\"\"Use this for answer evaluation\"\"\"\n",
" msg = {\"messages\": (\"user\", example[\"input\"])}\n",
" messages = app.invoke(msg)\n",
" json_str = messages['messages'][-1].additional_kwargs['tool_calls'][0]['function']['arguments']\n",
" response = json.loads(json_str)['final_answer']\n",
" json_str = messages[\"messages\"][-1].additional_kwargs[\"tool_calls\"][0][\"function\"][\n",
" \"arguments\"\n",
" ]\n",
" response = json.loads(json_str)[\"final_answer\"]\n",
" return {\"response\": response}"
]
},
@@ -792,13 +826,14 @@
"# Grade prompt\n",
"grade_prompt_answer_accuracy = prompt = hub.pull(\"langchain-ai/rag-answer-vs-reference\")\n",
"\n",
"\n",
"def answer_evaluator(run, example) -> dict:\n",
" \"\"\"\n",
" A simple evaluator for RAG answer accuracy\n",
" \"\"\"\n",
"\n",
" # Get question, ground truth answer, chain\n",
" input_question = example.inputs[\"input\"] \n",
" input_question = example.inputs[\"input\"]\n",
" reference = example.outputs[\"output\"]\n",
" prediction = run.outputs[\"response\"]\n",
"\n",
@@ -809,9 +844,13 @@
" answer_grader = grade_prompt_answer_accuracy | llm\n",
"\n",
" # Run evaluator\n",
" score = answer_grader.invoke({\"question\": input_question,\n",
" \"correct_answer\": reference,\n",
" \"student_answer\": prediction})\n",
" score = answer_grader.invoke(\n",
" {\n",
" \"question\": input_question,\n",
" \"correct_answer\": reference,\n",
" \"student_answer\": prediction,\n",
" }\n",
" )\n",
" score = score[\"Score\"]\n",
"\n",
" return {\"key\": \"answer_v_reference_score\", \"score\": score}"
@@ -872,10 +911,12 @@
"outputs": [],
"source": [
"# These are the tools that we expect the agent to use\n",
"expected_trajectory=['sql_db_list_tables', # first: list_tables_tool node\n",
" 'sql_db_schema', # second: get_schema_tool node\n",
" 'db_query_tool', # third: execute_query node\n",
" 'SubmitFinalAnswer'] # fourth: query_gen"
"expected_trajectory = [\n",
" \"sql_db_list_tables\", # first: list_tables_tool node\n",
" \"sql_db_schema\", # second: get_schema_tool node\n",
" \"db_query_tool\", # third: execute_query node\n",
" \"SubmitFinalAnswer\",\n",
"] # fourth: query_gen"
]
},
{
@@ -903,48 +944,59 @@
"\n",
"\n",
"def find_tool_calls(messages):\n",
" \"\"\" \n",
" Find all tool calls in the messages returned \n",
" \"\"\"\n",
" tool_calls = [tc['name'] for m in messages['messages'] for tc in getattr(m, 'tool_calls', [])]\n",
" Find all tool calls in the messages returned\n",
" \"\"\"\n",
" tool_calls = [\n",
" tc[\"name\"] for m in messages[\"messages\"] for tc in getattr(m, \"tool_calls\", [])\n",
" ]\n",
" return tool_calls\n",
"\n",
"def contains_all_tool_calls_in_order_exact_match(root_run: Run, example: Example) -> dict:\n",
"\n",
"def contains_all_tool_calls_in_order_exact_match(\n",
" root_run: Run, example: Example\n",
") -> dict:\n",
" \"\"\"\n",
" Check if all expected tools are called in exact order and without any additional tool calls.\n",
" \"\"\"\n",
" expected_trajectory = ['sql_db_list_tables', 'sql_db_schema', 'db_query_tool', 'SubmitFinalAnswer']\n",
" expected_trajectory = [\n",
" \"sql_db_list_tables\",\n",
" \"sql_db_schema\",\n",
" \"db_query_tool\",\n",
" \"SubmitFinalAnswer\",\n",
" ]\n",
" messages = root_run.outputs[\"response\"]\n",
" tool_calls = find_tool_calls(messages)\n",
" \n",
"\n",
" # Print the tool calls for debugging\n",
" print(\"Here are my tool calls:\")\n",
" print(tool_calls)\n",
" \n",
"\n",
" # Check if the tool calls match the expected trajectory exactly\n",
" if tool_calls == expected_trajectory:\n",
" score = 1\n",
" else:\n",
" score = 0\n",
" \n",
"\n",
" return {\"score\": int(score), \"key\": \"multi_tool_call_in_exact_order\"}\n",
"\n",
"\n",
"def contains_all_tool_calls_in_order(root_run: Run, example: Example) -> dict:\n",
" \"\"\"\n",
" Check if all expected tools are called in order, \n",
" Check if all expected tools are called in order,\n",
" but it allows for other tools to be called in between the expected ones.\n",
" \"\"\"\n",
" messages = root_run.outputs[\"response\"]\n",
" tool_calls = find_tool_calls(messages)\n",
" \n",
"\n",
" # Print the tool calls for debugging\n",
" print(\"Here are my tool calls:\")\n",
" print(tool_calls)\n",
" \n",
"\n",
" it = iter(tool_calls)\n",
" if all(elem in it for elem in expected_trajectory):\n",
" score = 1\n",
" else: \n",
" else:\n",
" score = 0\n",
" return {\"score\": int(score), \"key\": \"multi_tool_call_in_order\"}"
]
@@ -959,7 +1011,10 @@
"experiment_results = evaluate(\n",
" predict_sql_agent_messages,\n",
" data=dataset_name,\n",
" evaluators=[contains_all_tool_calls_in_order,contains_all_tool_calls_in_order_exact_match],\n",
" evaluators=[\n",
" contains_all_tool_calls_in_order,\n",
" contains_all_tool_calls_in_order_exact_match,\n",
" ],\n",
" num_repetitions=3,\n",
" experiment_prefix=\"sql-agent-multi-step-tool-calling-trajecory-in-order\",\n",
" metadata={\"version\": \"Chinook, gpt-4o multi-step-agent\"},\n",
+3 -1
View File
@@ -153,7 +153,9 @@
"\n",
"\n",
"summary_llm_chain = (\n",
" summary_prompt | ChatAnthropic(model=\"claude-3-haiku-20240307\") | StrOutputParser()\n",
" summary_prompt\n",
" | ChatAnthropic(model=\"claude-3-haiku-20240307\")\n",
" | StrOutputParser()\n",
" # Customize the tracing name for easier organization\n",
").with_config(run_name=\"GenerateSummary\")\n",
"summary_chain = summary_llm_chain | parse_summary\n",
+98
View File
@@ -0,0 +1,98 @@
import functools
import json
import os
import pathlib
import platform
import threading
import urllib.error
import urllib.request
from typing import Any, TypedDict
from langgraph_cli.constants import (
DEFAULT_CONFIG,
DEFAULT_PORT,
SUPABASE_PUBLIC_API_KEY,
SUPABASE_URL,
)
from langgraph_cli.version import __version__
class LogData(TypedDict):
os: str
os_version: str
python_version: str
cli_version: str
cli_command: str
params: dict[str, Any]
def get_anonymized_params(kwargs: dict[str, Any]) -> dict[str, bool]:
params = {}
# anonymize params with values
if config := kwargs.get("config"):
if config != pathlib.Path(DEFAULT_CONFIG).resolve():
params["config"] = True
if port := kwargs.get("port"):
if port != DEFAULT_PORT:
params["port"] = True
if kwargs.get("docker_compose"):
params["docker_compose"] = True
if kwargs.get("debugger_port"):
params["debugger_port"] = True
if kwargs.get("postgres_uri"):
params["postgres_uri"] = True
# pick up exact values for boolean flags
for boolean_param in ["recreate", "pull", "watch", "wait", "verbose"]:
if kwargs.get(boolean_param):
params[boolean_param] = kwargs[boolean_param]
return params
def log_data(data: LogData) -> None:
headers = {
"Content-Type": "application/json",
"apikey": SUPABASE_PUBLIC_API_KEY,
"User-Agent": "Mozilla/5.0",
}
supabase_url = SUPABASE_URL
req = urllib.request.Request(
f"{supabase_url}/rest/v1/logs",
data=json.dumps(data).encode("utf-8"),
headers=headers,
method="POST",
)
try:
urllib.request.urlopen(req)
except urllib.error.URLError:
pass
def log_command(func):
@functools.wraps(func)
def decorator(*args, **kwargs):
if os.getenv("LANGGRAPH_CLI_NO_ANALYTICS") == "1":
return func(*args, **kwargs)
data = {
"os": platform.system(),
"os_version": platform.version(),
"python_version": platform.python_version(),
"cli_version": __version__,
"cli_command": func.__name__,
"params": get_anonymized_params(kwargs),
}
background_thread = threading.Thread(target=log_data, args=(data,))
background_thread.start()
return func(*args, **kwargs)
return decorator
+22 -108
View File
@@ -9,16 +9,17 @@ import click.exceptions
import langgraph_cli.config
import langgraph_cli.docker
from langgraph_cli.analytics import log_command
from langgraph_cli.config import Config
from langgraph_cli.constants import DEFAULT_CONFIG, DEFAULT_PORT
from langgraph_cli.docker import DockerCapabilities
from langgraph_cli.exec import Runner, subp_exec
from langgraph_cli.progress import Progress
from langgraph_cli.util import clean_empty_lines
OPT_DOCKER_COMPOSE = click.option(
"--docker-compose",
"-d",
help="Advanced: Path to docker-compose.yml file with additional services to launch",
help="Advanced: Path to docker-compose.yml file with additional services to launch.",
type=click.Path(
exists=True,
file_okay=True,
@@ -79,7 +80,7 @@ OPT_CONFIG = click.option(
}
Defaults to looking for langgraph.json in the current directory.""",
default="langgraph.json",
default=DEFAULT_CONFIG,
type=click.Path(
exists=True,
file_okay=True,
@@ -92,7 +93,7 @@ OPT_PORT = click.option(
"--port",
"-p",
type=int,
default=8123,
default=DEFAULT_PORT,
show_default=True,
help="""
Port to expose.
@@ -166,6 +167,7 @@ def cli():
help="Wait for services to start before returning. Implies --detach",
)
@cli.command(help="Start langgraph API server")
@log_command
def up(
config: pathlib.Path,
docker_compose: Optional[pathlib.Path],
@@ -254,6 +256,7 @@ def up(
@OPT_VERBOSE
@OPT_DEBUGGER_PORT
@cli.command(help="Stop langgraph API server")
@log_command
def down(
config: pathlib.Path,
docker_compose: Optional[pathlib.Path],
@@ -290,6 +293,7 @@ def down(
@OPT_CONFIG
@click.option("--follow", "-f", is_flag=True, help="Follow logs")
@cli.command(help="Show langgraph API server logs")
@log_command
def logs(
config: pathlib.Path,
docker_compose: Optional[pathlib.Path],
@@ -346,13 +350,20 @@ def logs(
\b
""",
)
@click.option(
"--base-image",
hidden=True,
)
@cli.command(help="Build langgraph API server docker image")
@log_command
def build(
config: pathlib.Path,
platform: Optional[str],
base_image: Optional[str],
pull: bool,
tag: str,
):
base_image = base_image or "langchain/langgraph-api"
with open(config) as f:
config_json = langgraph_cli.config.validate_config(json.load(f))
with Runner() as runner:
@@ -364,7 +375,7 @@ def build(
subp_exec(
"docker",
"pull",
f"langchain/langgraph-api:{config_json['python_version']}",
f"{base_image}:{config_json['python_version']}",
)
)
# apply options
@@ -377,7 +388,7 @@ def build(
if platform:
args.extend(["--platform", platform])
# apply config
stdin = langgraph_cli.config.config_to_docker(config, config_json)
stdin = langgraph_cli.config.config_to_docker(config, config_json, base_image)
# run docker build
runner.run(
subp_exec(
@@ -386,107 +397,6 @@ def build(
)
@cli.group(help="Export langgraph compose files")
def export():
pass
@click.option(
"--output",
"-o",
help="Output path to write the docker compose file to",
type=click.Path(
exists=False,
file_okay=True,
dir_okay=False,
resolve_path=True,
path_type=pathlib.Path,
),
required=True,
)
@OPT_CONFIG
@OPT_PORT
@OPT_WATCH
@OPT_LANGGRAPH_API_PATH
@export.command(name="compose", help="Export docker compose file")
def export_compose(
output: pathlib.Path,
config: pathlib.Path,
port: int,
watch: bool,
langgraph_api_path: Optional[pathlib.Path],
):
with Runner() as runner:
capabilities = langgraph_cli.docker.check_capabilities(runner)
_, stdin = prepare(
runner,
capabilities=capabilities,
config_path=config,
docker_compose=None,
pull=False,
watch=watch,
langgraph_api_path=langgraph_api_path,
port=port,
verbose=False,
)
with open(output, "w") as f:
f.write(clean_empty_lines(stdin))
@click.option(
"--output",
"-o",
help="Output path (directory) to write the helm chart to",
type=click.Path(
exists=False,
file_okay=False,
dir_okay=True,
resolve_path=True,
path_type=pathlib.Path,
),
required=True,
)
@OPT_PORT
@OPT_DOCKER_COMPOSE
@OPT_CONFIG
@export.command(
name="helm",
help="Build and export a helm chart to deploy to a Kubernetes cluster",
hidden=True,
)
def export_helm(
output: pathlib.Path,
config: pathlib.Path,
docker_compose: Optional[pathlib.Path],
port: int,
):
with open(config) as f:
config_json = langgraph_cli.config.validate_config(json.load(f))
with Runner() as runner:
# check docker available
capabilities = langgraph_cli.docker.check_capabilities(runner)
# prepare args
stdin = langgraph_cli.docker.compose(capabilities, port=port)
args = [
"convert",
"--chart",
"-o",
str(output),
"-v",
]
# apply options
if docker_compose:
args.extend(["-f", str(docker_compose)])
args.extend(["-f", "-"]) # stdin
# apply config
stdin += langgraph_cli.config.config_to_compose(config, config_json)
# run kompose convert
runner.run(subp_exec("kompose", *args, input=stdin))
def prepare_args_and_stdin(
*,
capabilities: DockerCapabilities,
@@ -516,7 +426,11 @@ def prepare_args_and_stdin(
args.extend(["-f", "-"]) # stdin
# apply config
stdin += langgraph_cli.config.config_to_compose(
config_path, config, watch=watch, langgraph_api_path=langgraph_api_path
config_path,
config,
watch=watch,
langgraph_api_path=langgraph_api_path,
base_image="langchain/langgraph-api",
)
return args, stdin
+13 -10
View File
@@ -191,7 +191,7 @@ def _update_graph_paths(
config["graphs"][graph_id] = f"{module_str}:{attr_str}"
def config_to_docker(config_path: pathlib.Path, config: Config):
def config_to_docker(config_path: pathlib.Path, config: Config, base_image: str):
# configure pip
pip_install = "pip install -c /api/constraints.txt"
if config.get("pip_config_file"):
@@ -223,13 +223,15 @@ def config_to_docker(config_path: pathlib.Path, config: Config):
# https://til.simonwillison.net/python/pyproject
faux_pkgs_str = f"{os.linesep}{os.linesep}".join(
f"""ADD {relpath} {destpath}
COPY <<EOF /deps/__outer_{fullpath.name}/pyproject.toml
[project]
name = "{fullpath.name}"
version = "0.1"
[tool.setuptools.package-data]
"*" = ["**/*"]
EOF"""
RUN set -ex && \\
for line in '[project]' \\
'name = "{fullpath.name}"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_{fullpath.name}/pyproject.toml; \\
done
"""
for fullpath, (relpath, destpath) in local_deps.faux_pkgs.items()
)
local_pkgs_str = os.linesep.join(
@@ -237,7 +239,7 @@ EOF"""
for fullpath, relpath in local_deps.real_pkgs.items()
)
return f"""FROM langchain/langgraph-api:{config['python_version']}
return f"""FROM {base_image}:{config['python_version']}
{os.linesep.join(config["dockerfile_lines"])}
@@ -261,6 +263,7 @@ ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
def config_to_compose(
config_path: pathlib.Path,
config: Config,
base_image: str,
watch: bool = False,
langgraph_api_path: Optional[pathlib.Path] = None,
):
@@ -301,6 +304,6 @@ def config_to_compose(
build:
context: .
dockerfile_inline: |
{textwrap.indent(config_to_docker(config_path, config), " ")}
{textwrap.indent(config_to_docker(config_path, config, base_image), " ")}
{watch_str}
"""
+6
View File
@@ -0,0 +1,6 @@
DEFAULT_CONFIG = "langgraph.json"
DEFAULT_PORT = 8123
# analytics
SUPABASE_PUBLIC_API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imt6cmxwcG9qaW5wY3l5YWlweG5iIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTkyNTc1NzksImV4cCI6MjAzNDgzMzU3OX0.kkVOlLz3BxemA5nP-vat3K4qRtrDuO4SwZSR_htcX9c"
SUPABASE_URL = "https://kzrlppojinpcyyaipxnb.supabase.co"
+1 -1
View File
@@ -37,7 +37,7 @@ DEBUGGER = """
image: langchain/langgraph-debugger
restart: on-failure
ports:
- "{debugger_port}:80"
- "{debugger_port}:3968"
depends_on:
langgraph-postgres:
condition: service_healthy
+10
View File
@@ -0,0 +1,10 @@
"""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__)
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-cli"
version = "0.1.41"
version = "0.1.44"
description = "CLI for interacting with LangGraph API"
authors = []
license = "MIT"
+1 -1
View File
@@ -65,7 +65,7 @@ services:
image: langchain/langgraph-debugger
restart: on-failure
ports:
- "{debugger_port}:80"
- "{debugger_port}:3968"
depends_on:
langgraph-postgres:
condition: service_healthy
+92 -68
View File
@@ -66,18 +66,21 @@ def test_validate_config():
def test_config_to_docker_simple():
graphs = {"agent": "./agent.py:graph"}
actual_docker_stdin = config_to_docker(
PATH_TO_CONFIG, validate_config({"dependencies": ["."], "graphs": graphs})
PATH_TO_CONFIG,
validate_config({"dependencies": ["."], "graphs": graphs}),
"langchain/langgraph-api",
)
expected_docker_stdin = """\
FROM langchain/langgraph-api:3.11
ADD . /deps/__outer_unit_tests/unit_tests
COPY <<EOF /deps/__outer_unit_tests/pyproject.toml
[project]
name = "unit_tests"
version = "0.1"
[tool.setuptools.package-data]
"*" = ["**/*"]
EOF
RUN set -ex && \\
for line in '[project]' \\
'name = "unit_tests"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
RUN pip install -c /api/constraints.txt -e /deps/*
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
WORKDIR /deps/__outer_unit_tests/unit_tests\
@@ -96,18 +99,20 @@ def test_config_to_docker_pipconfig():
"pip_config_file": "pipconfig.txt",
}
),
"langchain/langgraph-api",
)
expected_docker_stdin = """\
FROM langchain/langgraph-api:3.11
ADD pipconfig.txt /pipconfig.txt
ADD . /deps/__outer_unit_tests/unit_tests
COPY <<EOF /deps/__outer_unit_tests/pyproject.toml
[project]
name = "unit_tests"
version = "0.1"
[tool.setuptools.package-data]
"*" = ["**/*"]
EOF
RUN set -ex && \\
for line in '[project]' \\
'name = "unit_tests"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
RUN PIP_CONFIG_FILE=/pipconfig.txt pip install -c /api/constraints.txt -e /deps/*
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
WORKDIR /deps/__outer_unit_tests/unit_tests\
@@ -122,13 +127,16 @@ def test_config_to_docker_invalid_inputs():
config_to_docker(
PATH_TO_CONFIG,
validate_config({"dependencies": ["./missing"], "graphs": graphs}),
"langchain/langgraph-api",
)
# test missing local module
with pytest.raises(FileNotFoundError):
graphs = {"agent": "./missing_agent.py:graph"}
config_to_docker(
PATH_TO_CONFIG, validate_config({"dependencies": ["."], "graphs": graphs})
PATH_TO_CONFIG,
validate_config({"dependencies": ["."], "graphs": graphs}),
"langchain/langgraph-api",
)
@@ -142,17 +150,19 @@ def test_config_to_docker_local_deps():
"graphs": graphs,
}
),
"langchain/langgraph-api-custom",
)
expected_docker_stdin = """\
FROM langchain/langgraph-api:3.11
FROM langchain/langgraph-api-custom:3.11
ADD ./graphs /deps/__outer_graphs/src
COPY <<EOF /deps/__outer_graphs/pyproject.toml
[project]
name = "graphs"
version = "0.1"
[tool.setuptools.package-data]
"*" = ["**/*"]
EOF
RUN set -ex && \\
for line in '[project]' \\
'name = "graphs"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \\
done
RUN pip install -c /api/constraints.txt -e /deps/*
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'\
"""
@@ -177,6 +187,7 @@ dependencies = ["langchain"]"""
"graphs": graphs,
}
),
"langchain/langgraph-api",
)
os.remove(pyproject_path)
expected_docker_stdin = """FROM langchain/langgraph-api:3.11
@@ -200,6 +211,7 @@ def test_config_to_docker_end_to_end():
"dockerfile_lines": ["ARG meow", "ARG foo"],
}
),
"langchain/langgraph-api",
)
expected_docker_stdin = """FROM langchain/langgraph-api:3.12
ARG meow
@@ -207,13 +219,14 @@ ARG foo
ADD pipconfig.txt /pipconfig.txt
RUN PIP_CONFIG_FILE=/pipconfig.txt pip install -c /api/constraints.txt langchain langchain_openai
ADD ./graphs/ /deps/__outer_graphs/src
COPY <<EOF /deps/__outer_graphs/pyproject.toml
[project]
name = "graphs"
version = "0.1"
[tool.setuptools.package-data]
"*" = ["**/*"]
EOF
RUN set -ex && \\
for line in '[project]' \\
'name = "graphs"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \\
done
RUN PIP_CONFIG_FILE=/pipconfig.txt pip install -c /api/constraints.txt -e /deps/*
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'"""
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
@@ -230,19 +243,22 @@ def test_config_to_compose_simple_config():
dockerfile_inline: |
FROM langchain/langgraph-api:3.11
ADD . /deps/__outer_unit_tests/unit_tests
COPY <<EOF /deps/__outer_unit_tests/pyproject.toml
[project]
name = "unit_tests"
version = "0.1"
[tool.setuptools.package-data]
"*" = ["**/*"]
EOF
RUN set -ex && \\
for line in '[project]' \\
'name = "unit_tests"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
RUN pip install -c /api/constraints.txt -e /deps/*
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
WORKDIR /deps/__outer_unit_tests/unit_tests
"""
actual_compose_stdin = config_to_compose(
PATH_TO_CONFIG, validate_config({"dependencies": ["."], "graphs": graphs})
PATH_TO_CONFIG,
validate_config({"dependencies": ["."], "graphs": graphs}),
"langchain/langgraph-api",
)
assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
@@ -255,15 +271,16 @@ def test_config_to_compose_env_vars():
build:
context: .
dockerfile_inline: |
FROM langchain/langgraph-api:3.11
FROM langchain/langgraph-api-custom:3.11
ADD . /deps/__outer_unit_tests/unit_tests
COPY <<EOF /deps/__outer_unit_tests/pyproject.toml
[project]
name = "unit_tests"
version = "0.1"
[tool.setuptools.package-data]
"*" = ["**/*"]
EOF
RUN set -ex && \\
for line in '[project]' \\
'name = "unit_tests"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
RUN pip install -c /api/constraints.txt -e /deps/*
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
WORKDIR /deps/__outer_unit_tests/unit_tests
@@ -278,6 +295,7 @@ def test_config_to_compose_env_vars():
"env": {"OPENAI_API_KEY": openai_api_key},
}
),
"langchain/langgraph-api-custom",
)
assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
@@ -292,13 +310,14 @@ def test_config_to_compose_env_file():
dockerfile_inline: |
FROM langchain/langgraph-api:3.11
ADD . /deps/__outer_unit_tests/unit_tests
COPY <<EOF /deps/__outer_unit_tests/pyproject.toml
[project]
name = "unit_tests"
version = "0.1"
[tool.setuptools.package-data]
"*" = ["**/*"]
EOF
RUN set -ex && \\
for line in '[project]' \\
'name = "unit_tests"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
RUN pip install -c /api/constraints.txt -e /deps/*
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
WORKDIR /deps/__outer_unit_tests/unit_tests
@@ -306,6 +325,7 @@ def test_config_to_compose_env_file():
actual_compose_stdin = config_to_compose(
PATH_TO_CONFIG,
validate_config({"dependencies": ["."], "graphs": graphs, "env": ".env"}),
"langchain/langgraph-api",
)
assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
@@ -320,13 +340,14 @@ def test_config_to_compose_watch():
dockerfile_inline: |
FROM langchain/langgraph-api:3.11
ADD . /deps/__outer_unit_tests/unit_tests
COPY <<EOF /deps/__outer_unit_tests/pyproject.toml
[project]
name = "unit_tests"
version = "0.1"
[tool.setuptools.package-data]
"*" = ["**/*"]
EOF
RUN set -ex && \\
for line in '[project]' \\
'name = "unit_tests"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
RUN pip install -c /api/constraints.txt -e /deps/*
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
WORKDIR /deps/__outer_unit_tests/unit_tests
@@ -345,6 +366,7 @@ def test_config_to_compose_watch():
actual_compose_stdin = config_to_compose(
PATH_TO_CONFIG,
validate_config({"dependencies": ["."], "graphs": graphs}),
"langchain/langgraph-api",
watch=True,
)
assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin
@@ -361,13 +383,14 @@ def test_config_to_compose_end_to_end():
dockerfile_inline: |
FROM langchain/langgraph-api:3.11
ADD . /deps/__outer_unit_tests/unit_tests
COPY <<EOF /deps/__outer_unit_tests/pyproject.toml
[project]
name = "unit_tests"
version = "0.1"
[tool.setuptools.package-data]
"*" = ["**/*"]
EOF
RUN set -ex && \\
for line in '[project]' \\
'name = "unit_tests"' \\
'version = "0.1"' \\
'[tool.setuptools.package-data]' \\
'"*" = ["**/*"]'; do \\
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
done
RUN pip install -c /api/constraints.txt -e /deps/*
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
WORKDIR /deps/__outer_unit_tests/unit_tests
@@ -389,6 +412,7 @@ def test_config_to_compose_end_to_end():
actual_compose_stdin = config_to_compose(
PATH_TO_CONFIG,
validate_config({"dependencies": ["."], "graphs": graphs, "env": ".env"}),
"langchain/langgraph-api",
watch=True,
langgraph_api_path="path/to/langgraph/api",
)
+19 -7
View File
@@ -8,6 +8,9 @@
⚡ Building language agents as graphs ⚡
> [!NOTE]
> Looking for the JS version? Click [here](https://github.com/langchain-ai/langgraphjs) ([JS docs](https://langchain-ai.github.io/langgraphjs/)).
## Overview
[LangGraph](https://langchain-ai.github.io/langgraph/) is a library for building stateful, multi-actor applications with LLMs, used to create agent and multi-agent workflows. Compared to other LLM frameworks, it offers these core benefits: cycles, controllability, and persistence. LangGraph allows you to define flows that involve cycles, essential for most agentic architectures, differentiating it from DAG-based solutions. As a very low-level framework, it provides fine-grained control over both the flow and state of your application, crucial for creating reliable agents. Additionally, LangGraph includes built-in persistence, enabling advanced human-in-the-loop and memory features.
@@ -69,7 +72,7 @@ tool_node = ToolNode(tools)
model = ChatOpenAI(temperature=0).bind_tools(tools)
# Define the function that determines whether to continue or not
def should_continue(state: AgentState) -> Literal["tools", END]:
def should_continue(state: MessagesState) -> Literal["tools", END]:
messages = state['messages']
last_message = messages[-1]
# If the LLM makes a tool call, then we route to the "tools" node
@@ -80,7 +83,7 @@ def should_continue(state: AgentState) -> Literal["tools", END]:
# Define the function that calls the model
def call_model(state: AgentState):
def call_model(state: MessagesState):
messages = state['messages']
response = model.invoke(messages)
# We return a list, because this will get added to the existing list
@@ -146,27 +149,31 @@ final_state["messages"][-1].content
'The current weather in New York is as follows:\n- Temperature: 20.3°C (68.5°F)\n- Condition: Overcast\n- Wind: 2.2 mph from the north\n- Humidity: 65%\n- Cloud Cover: 100%\n- UV Index: 5.0\n\nFor more details, you can visit [Weather API](https://www.weatherapi.com/).'
```
### Step-by-step Breakdown:
### Step-by-step Breakdown
1. <details>
<summary>Initialize the model and tools.</summary>
- we use `ChatOpenAI` as our LLM. **NOTE:** we need make sure the model knows that it has these tools available to call. We can do this by converting the LangChain tools into the format for OpenAI tool calling using the `.bind_tools()` method.
- we define the tools we want to use -- a web search tool in our case. It is really easy to create your own tools - see documentation here on how to do that [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools).
- we define the tools we want to use - a web search tool in our case. It is really easy to create your own tools - see documentation here on how to do that [here](https://python.langchain.com/docs/modules/agents/tools/custom_tools).
</details>
2. <details>
<summary>Initialize graph with state.</summary>
- we initialize graph (`StateGraph`) by passing state schema (in our case `MessagesState`)
- `MessagesState` is a prebuilt state schema that has one attribute -- a list of LangChain `Message` objects, as well as logic for merging the updates from each node into the state
</details>
3. <details>
<summary>Define graph nodes.</summary>
There are two main nodes we need:
- The `agent` node: responsible for deciding what (if any) actions to take.
- The `tools` node that invokes tools: if the agent decides to take an action, this node will then execute that action.
</details>
4. <details>
<summary>Define entry point and graph edges.</summary>
@@ -179,12 +186,14 @@ final_state["messages"][-1].content
- b. Finish (respond to the user) if the agent did not ask to run tools
- Normal edge: after the tools are invoked, the graph should always return to the agent to decide what to do next
</details>
5. <details>
<summary>Compile the graph.</summary>
- When we compile the graph, we turn it into a LangChain [Runnable](https://python.langchain.com/v0.2/docs/concepts/#runnable-interface), which automatically enables calling `.invoke()`, `.stream()` and `.batch()` with your inputs
- We can also optionally pass checkpointer object for persisting state between graph runs, and enabling memory, human-in-the-loop workflows, time travel and more. In our case we use `MemorySaver` - a simple in-memory checkpointer
</details>
6. <details>
<summary>Execute the graph.</summary>
@@ -192,8 +201,10 @@ final_state["messages"][-1].content
2. The `"agent"` node executes, invoking the chat model.
3. The chat model returns an `AIMessage`. LangGraph adds this to the state.
4. Graph cycles the following steps until there are no more `tool_calls` on `AIMessage`:
- If `AIMessage` has `tool_calls`, `"tools"` node executes
- The `"agent"` node executes again and returns `AIMessage`
- If `AIMessage` has `tool_calls`, `"tools"` node executes
- The `"agent"` node executes again and returns `AIMessage`
5. Execution progresses to the special `END` value and outputs the final state.
And as a result, we get a list of all our chat messages as output.
</details>
@@ -204,4 +215,5 @@ final_state["messages"][-1].content
* [Tutorials](https://langchain-ai.github.io/langgraph/tutorials/): Learn to build with LangGraph through guided examples.
* [How-to Guides](https://langchain-ai.github.io/langgraph/how-tos/): Accomplish specific things within LangGraph, from streaming, to adding memory & persistence, to common design patterns (branching, subgraphs, etc.), these are the place to go if you want to copy and run a specific code snippet.
* [Conceptual Guides](https://langchain-ai.github.io/langgraph/concepts/): In-depth explanations of the key concepts and principles behind LangGraph, such as nodes, edges, state and more.
* [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Review important classes and methods, simple examples of how to use the graph and checkpointing APIs, higher-level prebuilt components and more.
* [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Review important classes and methods, simple examples of how to use the graph and checkpointing APIs, higher-level prebuilt components and more.
* [Cloud (alpha)](https://langchain-ai.github.io/langgraph/cloud/): With one click, deploy LangGraph applications to LangGraph Cloud.
@@ -1,6 +1,7 @@
from contextlib import contextmanager
from typing import Generator, Generic, Optional, Sequence, Type
from langchain_core.runnables import RunnableConfig
from typing_extensions import Self
from langgraph.channels.base import BaseChannel, Value
@@ -32,7 +33,7 @@ class AnyValue(Generic[Value], BaseChannel[Value, Value, Value]):
@contextmanager
def from_checkpoint(
self, checkpoint: Optional[Value] = None
self, checkpoint: Optional[Value], config: RunnableConfig
) -> Generator[Self, None, None]:
empty = self.__class__(self.typ)
if checkpoint is not None:

Some files were not shown because too many files have changed in this diff Show More