mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-18 05:35:43 +02:00
Compare commits
168
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6b8098548 | ||
|
|
0631f19ef9 | ||
|
|
e9b0bc4d3b | ||
|
|
eb19b80d13 | ||
|
|
e5ccdd91c2 | ||
|
|
871f15a630 | ||
|
|
32aa87d4b2 | ||
|
|
55a3352e9b | ||
|
|
6adaa6cf78 | ||
|
|
28172a2767 | ||
|
|
028137a51a | ||
|
|
c3847fae6c | ||
|
|
3daa4ae466 | ||
|
|
6a36f4bf91 | ||
|
|
04a0443742 | ||
|
|
3b224df566 | ||
|
|
0a2e2b4796 | ||
|
|
0e0b3ffac5 | ||
|
|
75509bd221 | ||
|
|
f997e5eafd | ||
|
|
6337c62dc7 | ||
|
|
59992fac7c | ||
|
|
28a7ca03a9 | ||
|
|
9fe61a42d2 | ||
|
|
59ae3b873c | ||
|
|
31ba5c41ac | ||
|
|
6ddbdd0638 | ||
|
|
3f7f52ccdc | ||
|
|
ee9a34f726 | ||
|
|
72ce34e3e6 | ||
|
|
098d5ec403 | ||
|
|
45ef425d85 | ||
|
|
8c192414a2 | ||
|
|
c0db7f4d09 | ||
|
|
072f2d43eb | ||
|
|
c02a74e221 | ||
|
|
15af8a9e6c | ||
|
|
1b257b4d0a | ||
|
|
384814036e | ||
|
|
d5b0ad2cf6 | ||
|
|
8d8e514924 | ||
|
|
a37c4d6f49 | ||
|
|
4b3e07b67a | ||
|
|
6ba61cc768 | ||
|
|
5a79210904 | ||
|
|
4a60eaf32f | ||
|
|
cf7c3e7fd1 | ||
|
|
141b53ee6e | ||
|
|
37e8e00f1f | ||
|
|
a0ec9017f2 | ||
|
|
80cef60405 | ||
|
|
46cba763be | ||
|
|
830e5f4550 | ||
|
|
55e9409b6e | ||
|
|
39e65a1a62 | ||
|
|
82148e9bf2 | ||
|
|
ed09a77d9f | ||
|
|
7e267897c9 | ||
|
|
d279902156 | ||
|
|
440158b969 | ||
|
|
0cf3a64d66 | ||
|
|
953e2907d4 | ||
|
|
0a861815b7 | ||
|
|
928126f2d7 | ||
|
|
0d8fea847a | ||
|
|
b4dc3a851f | ||
|
|
7568862013 | ||
|
|
69dc5f326e | ||
|
|
90f1e66748 | ||
|
|
a910a1a341 | ||
|
|
554e763994 | ||
|
|
adff1df813 | ||
|
|
64b828def6 | ||
|
|
9196cc2da8 | ||
|
|
e13a8bc320 | ||
|
|
4b5b309152 | ||
|
|
9d5166a0d3 | ||
|
|
5b11683f4e | ||
|
|
3fae8d47c3 | ||
|
|
be9cb03dfa | ||
|
|
6926c4bcc0 | ||
|
|
c04802a344 | ||
|
|
cf7f6691cf | ||
|
|
e1140f4fad | ||
|
|
ae8afb7677 | ||
|
|
48040d8ea5 | ||
|
|
ad51bfdf71 | ||
|
|
996b120613 | ||
|
|
1093dd55c8 | ||
|
|
d794875b32 | ||
|
|
6948cf5eb8 | ||
|
|
bacc2955ae | ||
|
|
f014d96d1c | ||
|
|
7b552ebf4b | ||
|
|
1e61ddfdbe | ||
|
|
d34846dc08 | ||
|
|
06823e327f | ||
|
|
1059ef55d1 | ||
|
|
39552255c8 | ||
|
|
38bbe67469 | ||
|
|
6335963674 | ||
|
|
8a4c452317 | ||
|
|
5dc5853161 | ||
|
|
9e066554ba | ||
|
|
6ce9354ee4 | ||
|
|
211fd4337d | ||
|
|
44bf97ac0e | ||
|
|
23c73ae719 | ||
|
|
4165d479e9 | ||
|
|
c43a9a4bd0 | ||
|
|
c9613927dc | ||
|
|
c697c2aa04 | ||
|
|
3f2557c9c9 | ||
|
|
cbad17fa7d | ||
|
|
17dacb83a2 | ||
|
|
3a997be088 | ||
|
|
020d10138d | ||
|
|
303587c4ff | ||
|
|
7d4e636313 | ||
|
|
86913caf89 | ||
|
|
041faefe29 | ||
|
|
b704cf30cc | ||
|
|
3915b44180 | ||
|
|
ac1407b23c | ||
|
|
44840aa23f | ||
|
|
51242e2a32 | ||
|
|
b358e2e7cd | ||
|
|
0d91ab1474 | ||
|
|
12ae297194 | ||
|
|
0177565c6b | ||
|
|
c48d495031 | ||
|
|
22e6468af5 | ||
|
|
24bc0c0630 | ||
|
|
802e6df8df | ||
|
|
3ec55b008d | ||
|
|
e10b7c1391 | ||
|
|
31cc6b9f1d | ||
|
|
2cafb4905b | ||
|
|
fe46576d98 | ||
|
|
16c86c9de6 | ||
|
|
fca0d2d5bb | ||
|
|
c7c62f5587 | ||
|
|
956c5f68fc | ||
|
|
4908caf522 | ||
|
|
f926eada24 | ||
|
|
adc1e47028 | ||
|
|
7820b5c765 | ||
|
|
ca6f8e2042 | ||
|
|
ee61d06f8d | ||
|
|
6eeb9de46a | ||
|
|
19a91f4677 | ||
|
|
6087b1969e | ||
|
|
6cbc7e8b67 | ||
|
|
b2213e523e | ||
|
|
b7c3ac4501 | ||
|
|
09e8516689 | ||
|
|
3ca75d69b8 | ||
|
|
d48dec5452 | ||
|
|
d48b25420b | ||
|
|
12be3fac33 | ||
|
|
aed1f0ba18 | ||
|
|
07695f5c5a | ||
|
|
204c9c83f8 | ||
|
|
31578cbe0e | ||
|
|
44970640d5 | ||
|
|
0f4e42474f | ||
|
|
832f9ad64e | ||
|
|
318de5bb81 |
@@ -9,7 +9,11 @@
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: docs
|
||||
|
||||
jobs:
|
||||
codespell:
|
||||
name: (Check for spelling errors)
|
||||
@@ -21,12 +25,12 @@
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
pip install toml codespell jupytext
|
||||
pip install toml codespell==2.3.0 jupytext
|
||||
|
||||
- name: Extract Ignore Words List
|
||||
run: |
|
||||
# Use a Python script to extract the ignore words list from pyproject.toml
|
||||
python .github/workflows/extract_ignored_words_list.py
|
||||
python ../.github/workflows/extract_ignored_words_list.py
|
||||
id: extract_ignore_words
|
||||
|
||||
- name: Codespell
|
||||
|
||||
@@ -21,6 +21,10 @@ concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: docs
|
||||
|
||||
jobs:
|
||||
get-changed-files:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -73,11 +77,12 @@ jobs:
|
||||
# This step lints the docs using the existing linting set up.
|
||||
# It should be very fast and should not require any external services.
|
||||
run: make lint-docs
|
||||
- name: Build llms-text
|
||||
run: make llms-text
|
||||
- name: Build site
|
||||
run: make build-docs
|
||||
env:
|
||||
MKDOCS_GIT_COMMITTERS_APIKEY: ${{ secrets.MKDOCS_GIT_COMMITTERS_APIKEY }}
|
||||
|
||||
- name: Check links in notebooks
|
||||
env:
|
||||
LANGCHAIN_API_KEY: test
|
||||
@@ -96,13 +101,13 @@ jobs:
|
||||
--check-links-ignore "https://python\.langchain\.com/.*" \
|
||||
--check-links-ignore "https://openai\.com/.*" \
|
||||
--check-links-ignore "https://pepy\.tech/.*" \
|
||||
--check-links $(find docs/site -name "index.html" | grep -v 'storm/index.html')
|
||||
--check-links $(find site -name "index.html" | grep -v 'storm/index.html')
|
||||
|
||||
else
|
||||
echo "Fetching changes from origin/main..."
|
||||
git fetch origin main
|
||||
echo "Checking for changed notebook files..."
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=d origin/main | grep 'docs/docs/.*\.ipynb$' | grep -v 'storm.ipynb' | sed -E 's|^docs/docs/|docs/site/|; s/\.ipynb$/\/index.html/' || true)
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=d origin/main | grep 'docs/docs/.*\.ipynb$' | grep -v 'storm.ipynb' | sed -E 's|^docs/docs/|site/|; s/\.ipynb$/\/index.html/' || true)
|
||||
echo "Changed files: ${CHANGED_FILES}"
|
||||
if [ -n "${CHANGED_FILES}" ]; then
|
||||
echo "Running link check on HTML files matching changed notebook files..."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import toml
|
||||
|
||||
pyproject_toml = toml.load("libs/langgraph/pyproject.toml")
|
||||
pyproject_toml = toml.load("../libs/langgraph/pyproject.toml")
|
||||
|
||||
# Extract the ignore words list (adjust the key as per your TOML structure)
|
||||
ignore_words_list = (
|
||||
|
||||
@@ -11,6 +11,10 @@ on:
|
||||
schedule:
|
||||
- cron: '0 13 * * *'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: docs
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -39,14 +43,14 @@ jobs:
|
||||
|
||||
- name: Pre-download tiktoken files
|
||||
run: |
|
||||
poetry run python docs/_scripts/download_tiktoken.py
|
||||
poetry run python _scripts/download_tiktoken.py
|
||||
|
||||
- name: Prepare notebooks
|
||||
run: |
|
||||
if [ "${{ matrix.lib-version }}" = "development" ]; then
|
||||
poetry run python docs/_scripts/prepare_notebooks_for_ci.py --comment-install-cells
|
||||
poetry run python _scripts/prepare_notebooks_for_ci.py --comment-install-cells
|
||||
else
|
||||
poetry run python docs/_scripts/prepare_notebooks_for_ci.py
|
||||
poetry run python _scripts/prepare_notebooks_for_ci.py
|
||||
fi
|
||||
|
||||
- name: Run notebooks
|
||||
@@ -63,12 +67,12 @@ jobs:
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "schedule" ]; then
|
||||
echo "Running all notebooks"
|
||||
./docs/_scripts/execute_notebooks.sh
|
||||
./_scripts/execute_notebooks.sh
|
||||
else
|
||||
CHANGED_FILES=$(echo '${{ inputs.changed-files }}' | tr ' ' '\n' | grep '\.ipynb$' || true)
|
||||
CHANGED_FILES=$(echo '${{ inputs.changed-files }}' | tr ' ' '\n' | sed 's|^docs/docs/|docs/|' | grep '\.ipynb$' || true)
|
||||
if [ -n "$CHANGED_FILES" ]; then
|
||||
echo "Running changed notebooks: $CHANGED_FILES"
|
||||
./docs/_scripts/execute_notebooks.sh $CHANGED_FILES
|
||||
./_scripts/execute_notebooks.sh $CHANGED_FILES
|
||||
else
|
||||
echo "No notebook files changed, skipping execution"
|
||||
fi
|
||||
|
||||
+1
-1
@@ -178,4 +178,4 @@ Untitled*.ipynb
|
||||
|
||||
Chinook.db
|
||||
|
||||
libs/langgraph/out
|
||||
.vercel
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
.PHONY: lint-docs format-docs build-docs serve-docs serve-clean-docs clean-docs codespell build-typedoc
|
||||
|
||||
build-typedoc:
|
||||
cd libs/sdk-js && yarn install --include-dev && yarn typedoc
|
||||
cd libs/sdk-js && yarn --silent concat-md --decrease-title-levels --ignore=js_ts_sdk_ref.md --start-title-level-at 2 docs > ../../docs/docs/cloud/reference/sdk/js_ts_sdk_ref.md 2>/dev/null
|
||||
# Add links to the monorepo
|
||||
sed -e '1,10s|@langchain/langgraph-sdk|[@langchain/langgraph-sdk](https://github.com/langchain-ai/langgraph/tree/main/libs/sdk-js)|g' docs/docs/cloud/reference/sdk/js_ts_sdk_ref.md > temp_file && mv temp_file docs/docs/cloud/reference/sdk/js_ts_sdk_ref.md
|
||||
|
||||
build-docs: build-typedoc
|
||||
poetry run python -m mkdocs build --clean -f docs/mkdocs.yml --strict
|
||||
|
||||
serve-clean-docs: clean-docs
|
||||
poetry run python -m mkdocs serve -c -f docs/mkdocs.yml --strict -w ./libs/langgraph
|
||||
|
||||
serve-docs: build-typedoc
|
||||
poetry run python -m mkdocs serve -f docs/mkdocs.yml -w ./libs/langgraph -w ./libs/checkpoint -w ./libs/sdk-py --dirty
|
||||
|
||||
clean-docs:
|
||||
find ./docs/docs -name "*.ipynb" -type f -delete
|
||||
rm -rf docs/site
|
||||
|
||||
## Run format against the project documentation.
|
||||
format-docs:
|
||||
poetry run ruff format docs/docs
|
||||
poetry run ruff check --fix docs/docs
|
||||
|
||||
# Check the docs for linting violations
|
||||
lint-docs:
|
||||
poetry run ruff format --check docs/docs
|
||||
poetry run ruff check docs/docs
|
||||
|
||||
codespell:
|
||||
./docs/codespell_notebooks.sh .
|
||||
|
||||
start-services:
|
||||
docker compose -f docs/test-compose.yml up -V --force-recreate --wait --remove-orphans
|
||||
|
||||
stop-services:
|
||||
docker compose -f docs/test-compose.yml down
|
||||
@@ -12,25 +12,45 @@
|
||||
|
||||
## 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.
|
||||
[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. Check out an introductory tutorial [here](https://langchain-ai.github.io/langgraph/tutorials/introduction/).
|
||||
|
||||
|
||||
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
|
||||
|
||||
[LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform) is infrastructure for deploying LangGraph agents. It is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework. The LangGraph Platform consists of several components that work together to support the development, deployment, debugging, and monitoring of LangGraph applications: [LangGraph Server](https://langchain-ai.github.io/langgraph/concepts/langgraph_server) (APIs), [LangGraph SDKs](https://langchain-ai.github.io/langgraph/concepts/sdk) (clients for the APIs), [LangGraph CLI](https://langchain-ai.github.io/langgraph/concepts/langgraph_cli) (command line tool for building the server), [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio) (UI/debugger),
|
||||
### Why use LangGraph?
|
||||
|
||||
To learn more about LangGraph, check out our first LangChain Academy course, *Introduction to LangGraph*, available for free [here](https://academy.langchain.com/courses/intro-to-langgraph).
|
||||
LangGraph powers [production-grade agents](https://www.langchain.com/built-with-langgraph), trusted by Linkedin, Uber, Klarna, GitLab, and many more. LangGraph provides fine-grained control over both the flow and state of your agent applications. It implements a central [persistence layer](https://langchain-ai.github.io/langgraph/concepts/persistence/), enabling features that are common to most agent architectures:
|
||||
|
||||
### Key Features
|
||||
- **Memory**: LangGraph persists arbitrary aspects of your application's state,
|
||||
supporting memory of conversations and other updates within and across user
|
||||
interactions;
|
||||
- **Human-in-the-loop**: Because state is checkpointed, execution can be interrupted
|
||||
and resumed, allowing for decisions, validation, and corrections at key stages via
|
||||
human input.
|
||||
|
||||
- **Cycles and Branching**: Implement loops and conditionals in your apps.
|
||||
- **Persistence**: Automatically save state after each step in the graph. Pause and resume the graph execution at any point to support error recovery, human-in-the-loop workflows, time travel and more.
|
||||
- **Human-in-the-Loop**: Interrupt graph execution to approve or edit next action planned by the agent.
|
||||
- **Streaming Support**: Stream outputs as they are produced by each node (including token streaming).
|
||||
- **Integration with LangChain**: LangGraph integrates seamlessly with [LangChain](https://github.com/langchain-ai/langchain/) and [LangSmith](https://docs.smith.langchain.com/) (but does not require them).
|
||||
Standardizing these components allows individuals and teams to focus on the behavior
|
||||
of their agent, instead of its supporting infrastructure.
|
||||
|
||||
Through [LangGraph Platform](#langgraph-platform), LangGraph also provides tooling for
|
||||
the development, deployment, debugging, and monitoring of your applications.
|
||||
|
||||
LangGraph integrates seamlessly with
|
||||
[LangChain](https://python.langchain.com/docs/introduction/) and
|
||||
[LangSmith](https://docs.smith.langchain.com/) (but does not require them).
|
||||
|
||||
To learn more about LangGraph, check out our first LangChain Academy
|
||||
course, *Introduction to LangGraph*, available for free
|
||||
[here](https://academy.langchain.com/courses/intro-to-langgraph).
|
||||
|
||||
### LangGraph Platform
|
||||
|
||||
LangGraph Platform is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework.
|
||||
[LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform) is infrastructure for deploying LangGraph agents. It is a commercial solution for deploying agentic applications to production, built on the open-source LangGraph framework. The LangGraph Platform consists of several components that work together to support the development, deployment, debugging, and monitoring of LangGraph applications: [LangGraph Server](https://langchain-ai.github.io/langgraph/concepts/langgraph_server) (APIs), [LangGraph SDKs](https://langchain-ai.github.io/langgraph/concepts/sdk) (clients for the APIs), [LangGraph CLI](https://langchain-ai.github.io/langgraph/concepts/langgraph_cli) (command line tool for building the server), and [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio) (UI/debugger).
|
||||
|
||||
See deployment options [here](https://langchain-ai.github.io/langgraph/concepts/deployment_options/)
|
||||
(includes a free tier).
|
||||
|
||||
Here are some common issues that arise in complex deployments, which LangGraph Platform addresses:
|
||||
|
||||
- **Streaming support**: LangGraph Server provides [multiple streaming modes](https://langchain-ai.github.io/langgraph/concepts/streaming) optimized for various application needs
|
||||
@@ -103,6 +123,20 @@ final_state["messages"][-1].content
|
||||
```
|
||||
"Based on the search results, I can tell you that the current weather in San Francisco is:\n\nTemperature: 60 degrees Fahrenheit\nConditions: Foggy\n\nSan Francisco is known for its microclimates and frequent fog, especially during the summer months. The temperature of 60°F (about 15.5°C) is quite typical for the city, which tends to have mild temperatures year-round. The fog, often referred to as "Karl the Fog" by locals, is a characteristic feature of San Francisco\'s weather, particularly in the mornings and evenings.\n\nIs there anything else you\'d like to know about the weather in San Francisco or any other location?"
|
||||
```
|
||||
|
||||
Now when we pass the same <code>"thread_id"</code>, the conversation context is retained via the saved state (i.e. stored list of messages)
|
||||
|
||||
```python
|
||||
final_state = app.invoke(
|
||||
{"messages": [{"role": "user", "content": "what about ny"}]},
|
||||
config={"configurable": {"thread_id": 42}}
|
||||
)
|
||||
final_state["messages"][-1].content
|
||||
```
|
||||
|
||||
```
|
||||
"Based on the search results, I can tell you that the current weather in New York City is:\n\nTemperature: 90 degrees Fahrenheit (approximately 32.2 degrees Celsius)\nConditions: Sunny\n\nThis weather is quite different from what we just saw in San Francisco. New York is experiencing much warmer temperatures right now. Here are a few points to note:\n\n1. The temperature of 90°F is quite hot, typical of summer weather in New York City.\n2. The sunny conditions suggest clear skies, which is great for outdoor activities but also means it might feel even hotter due to direct sunlight.\n3. This kind of weather in New York often comes with high humidity, which can make it feel even warmer than the actual temperature suggests.\n\nIt's interesting to see the stark contrast between San Francisco's mild, foggy weather and New York's hot, sunny conditions. This difference illustrates how varied weather can be across different parts of the United States, even on the same day.\n\nIs there anything else you'd like to know about the weather in New York or any other location?"
|
||||
```
|
||||
</details>
|
||||
|
||||
> [!TIP]
|
||||
@@ -198,82 +232,95 @@ final_state = app.invoke(
|
||||
)
|
||||
final_state["messages"][-1].content
|
||||
```
|
||||
|
||||
<b>Step-by-step Breakdown</b>:
|
||||
|
||||
<details>
|
||||
<summary>Initialize the model and tools.</summary>
|
||||
<ul>
|
||||
<li>
|
||||
We use <code>ChatAnthropic</code> as our LLM. <strong>NOTE:</strong> we need to 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 <code>.bind_tools()</code> method.
|
||||
</li>
|
||||
<li>
|
||||
We define the tools we want to use - a search tool in our case. It is really easy to create your own tools - see documentation here on how to do that <a href="https://python.langchain.com/docs/how_to/custom_tools/">here</a>.
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
Now when we pass the same `"thread_id"`, the conversation context is retained via the saved state (i.e. stored list of messages)
|
||||
<details>
|
||||
<summary>Initialize graph with state.</summary>
|
||||
|
||||
```python
|
||||
final_state = app.invoke(
|
||||
{"messages": [{"role": "user", "content": "what about ny"}]},
|
||||
config={"configurable": {"thread_id": 42}}
|
||||
)
|
||||
final_state["messages"][-1].content
|
||||
```
|
||||
<ul>
|
||||
<li>We initialize graph (<code>StateGraph</code>) by passing state schema (in our case <code>MessagesState</code>)</li>
|
||||
<li><code>MessagesState</code> is a prebuilt state schema that has one attribute -- a list of LangChain <code>Message</code> objects, as well as logic for merging the updates from each node into the state.</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
```
|
||||
"Based on the search results, I can tell you that the current weather in New York City is:\n\nTemperature: 90 degrees Fahrenheit (approximately 32.2 degrees Celsius)\nConditions: Sunny\n\nThis weather is quite different from what we just saw in San Francisco. New York is experiencing much warmer temperatures right now. Here are a few points to note:\n\n1. The temperature of 90°F is quite hot, typical of summer weather in New York City.\n2. The sunny conditions suggest clear skies, which is great for outdoor activities but also means it might feel even hotter due to direct sunlight.\n3. This kind of weather in New York often comes with high humidity, which can make it feel even warmer than the actual temperature suggests.\n\nIt's interesting to see the stark contrast between San Francisco's mild, foggy weather and New York's hot, sunny conditions. This difference illustrates how varied weather can be across different parts of the United States, even on the same day.\n\nIs there anything else you'd like to know about the weather in New York or any other location?"
|
||||
```
|
||||
<details>
|
||||
<summary>Define graph nodes.</summary>
|
||||
|
||||
### Step-by-step Breakdown
|
||||
There are two main nodes we need:
|
||||
|
||||
1. <details>
|
||||
<summary>Initialize the model and tools.</summary>
|
||||
<ul>
|
||||
<li>The <code>agent</code> node: responsible for deciding what (if any) actions to take.</li>
|
||||
<li>The <code>tools</code> node that invokes tools: if the agent decides to take an action, this node will then execute that action.</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
- we use `ChatAnthropic` 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 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>
|
||||
<details>
|
||||
<summary>Define entry point and graph edges.</summary>
|
||||
|
||||
2. <details>
|
||||
<summary>Initialize graph with state.</summary>
|
||||
First, we need to set the entry point for graph execution - <code>agent</code> node.
|
||||
|
||||
- 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>
|
||||
Then we define one normal and one conditional edge. Conditional edge means that the destination depends on the contents of the graph's state (<code>MessagesState</code>). In our case, the destination is not known until the agent (LLM) decides.
|
||||
|
||||
3. <details>
|
||||
<summary>Define graph nodes.</summary>
|
||||
<ul>
|
||||
<li>Conditional edge: after the agent is called, we should either:
|
||||
<ul>
|
||||
<li>a. Run tools if the agent said to take an action, OR</li>
|
||||
<li>b. Finish (respond to the user) if the agent did not ask to run tools</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Normal edge: after the tools are invoked, the graph should always return to the agent to decide what to do next</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
There are two main nodes we need:
|
||||
<details>
|
||||
<summary>Compile the graph.</summary>
|
||||
|
||||
- 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>
|
||||
<ul>
|
||||
<li>
|
||||
When we compile the graph, we turn it into a LangChain
|
||||
<a href="https://python.langchain.com/docs/concepts/runnables/">Runnable</a>,
|
||||
which automatically enables calling <code>.invoke()</code>, <code>.stream()</code> and <code>.batch()</code>
|
||||
with your inputs
|
||||
</li>
|
||||
<li>
|
||||
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 <code>MemorySaver</code> -
|
||||
a simple in-memory checkpointer
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
4. <details>
|
||||
<summary>Define entry point and graph edges.</summary>
|
||||
<details>
|
||||
<summary>Execute the graph.</summary>
|
||||
|
||||
First, we need to set the entry point for graph execution - `agent` node.
|
||||
|
||||
Then we define one normal and one conditional edge. Conditional edge means that the destination depends on the contents of the graph's state (`MessageState`). In our case, the destination is not known until the agent (LLM) decides.
|
||||
|
||||
- Conditional edge: after the agent is called, we should either:
|
||||
- a. Run tools if the agent said to take an action, OR
|
||||
- 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>
|
||||
|
||||
1. LangGraph adds the input message to the internal state, then passes the state to the entrypoint node, `"agent"`.
|
||||
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`
|
||||
|
||||
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>
|
||||
<ol>
|
||||
<li>LangGraph adds the input message to the internal state, then passes the state to the entrypoint node, <code>"agent"</code>.</li>
|
||||
<li>The <code>"agent"</code> node executes, invoking the chat model.</li>
|
||||
<li>The chat model returns an <code>AIMessage</code>. LangGraph adds this to the state.</li>
|
||||
<li>Graph cycles the following steps until there are no more <code>tool_calls</code> on <code>AIMessage</code>:
|
||||
<ul>
|
||||
<li>If <code>AIMessage</code> has <code>tool_calls</code>, <code>"tools"</code> node executes</li>
|
||||
<li>The <code>"agent"</code> node executes again and returns <code>AIMessage</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Execution progresses to the special <code>END</code> value and outputs the final state. And as a result, we get a list of all our chat messages as output.</li>
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
</details>
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -283,6 +330,10 @@ final_state["messages"][-1].content
|
||||
* [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.
|
||||
* [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform): LangGraph Platform is a commercial solution for deploying agentic applications in production, built on the open-source LangGraph framework.
|
||||
|
||||
## Resources
|
||||
|
||||
* [Built with LangGraph](https://www.langchain.com/built-with-langgraph): Hear how industry leaders use LangGraph to ship powerful, production-ready AI applications.
|
||||
|
||||
## Contributing
|
||||
|
||||
For more information on how to contribute, see [here](https://github.com/langchain-ai/langgraph/blob/main/CONTRIBUTING.md).
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
site/
|
||||
docs/cloud/reference/sdk/js_ts_sdk_ref.md
|
||||
|
||||
.vercel
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
.PHONY: lint-docs format-docs build-docs serve-docs serve-clean-docs clean-docs codespell build-typedoc llms-text
|
||||
|
||||
build-typedoc:
|
||||
cd ../libs/sdk-js && yarn install --include-dev && yarn typedoc
|
||||
cd ../libs/sdk-js && yarn --silent concat-md --decrease-title-levels --ignore=js_ts_sdk_ref.md --start-title-level-at 2 docs > ../../docs/docs/cloud/reference/sdk/js_ts_sdk_ref.md 2>/dev/null
|
||||
# Add links to the monorepo
|
||||
sed -e '1,10s|@langchain/langgraph-sdk|[@langchain/langgraph-sdk](https://github.com/langchain-ai/langgraph/tree/main/libs/sdk-js)|g' docs/cloud/reference/sdk/js_ts_sdk_ref.md > temp_file && mv temp_file docs/cloud/reference/sdk/js_ts_sdk_ref.md
|
||||
|
||||
build-docs: build-typedoc
|
||||
poetry run python -m mkdocs build --clean -f mkdocs.yml --strict
|
||||
|
||||
llms-text:
|
||||
poetry run python _scripts/generate_llms_text.py docs/llms-full.txt
|
||||
|
||||
install-vercel-deps:
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
poetry self update 1.8.5
|
||||
|
||||
vercel-build-docs: install-vercel-deps
|
||||
poetry install
|
||||
make build-docs
|
||||
|
||||
serve-clean-docs: clean-docs
|
||||
poetry run python -m mkdocs serve -c -f mkdocs.yml --strict -w ../libs/langgraph
|
||||
|
||||
serve-docs: build-typedoc
|
||||
poetry run python -m mkdocs serve -f mkdocs.yml -w ../libs/langgraph -w ../libs/checkpoint -w ../libs/sdk-py --dirty
|
||||
|
||||
clean-docs:
|
||||
find ./docs -name "*.ipynb" -type f -delete
|
||||
rm -rf site
|
||||
|
||||
## Run format against the project documentation.
|
||||
format-docs:
|
||||
poetry run ruff format docs
|
||||
poetry run ruff check --fix docs
|
||||
|
||||
# Check the docs for linting violations
|
||||
lint-docs:
|
||||
poetry run ruff format --check docs
|
||||
poetry run ruff check docs
|
||||
|
||||
codespell:
|
||||
./codespell_notebooks.sh .
|
||||
|
||||
start-services:
|
||||
docker compose -f test-compose.yml up -V --force-recreate --wait --remove-orphans
|
||||
|
||||
stop-services:
|
||||
docker compose -f test-compose.yml down
|
||||
+7
-9
@@ -19,23 +19,21 @@ make serve-docs
|
||||
If you would like to automatically execute all of the notebooks, to mimic the "Run notebooks" GHA, you can run:
|
||||
|
||||
```bash
|
||||
python docs/_scripts/prepare_notebooks_for_ci.py
|
||||
./docs/_scripts/execute_notebooks.sh
|
||||
python _scripts/prepare_notebooks_for_ci.py
|
||||
./_scripts/execute_notebooks.sh
|
||||
```
|
||||
|
||||
**Note**: if you want to run the notebooks without `%pip install` cells, you can run:
|
||||
|
||||
```bash
|
||||
python docs/_scripts/prepare_notebooks_for_ci.py --comment-install-cells
|
||||
./docs/_scripts/execute_notebooks.sh
|
||||
python _scripts/prepare_notebooks_for_ci.py --comment-install-cells
|
||||
./_scripts/execute_notebooks.sh
|
||||
```
|
||||
|
||||
`prepare_notebooks_for_ci.py` script will add VCR cassette context manager for each cell in the notebook, so that:
|
||||
* when the notebook is run for the first time, cells with network requests will be recorded to a VCR cassette file
|
||||
* when the notebook is run subsequently, the cells with network requests will be replayed from the cassettes
|
||||
|
||||
**Note**: this is currently limited only to the notebooks in `docs/docs/how-tos`
|
||||
|
||||
## Adding new notebooks
|
||||
|
||||
If you are adding a notebook with API requests, it's **recommended** to record network requests so that they can be subsequently replayed. If this is not done, the notebook runner will make API requests every time the notebook is run, which can be costly and slow.
|
||||
@@ -48,14 +46,14 @@ Then, run
|
||||
jupyter execute <path_to_notebook>
|
||||
```
|
||||
|
||||
Once the notebook is executed, you should see the new VCR cassettes recorded in `docs/cassettes` directory and discard the updated notebook.
|
||||
Once the notebook is executed, you should see the new VCR cassettes recorded in `cassettes` directory and discard the updated notebook.
|
||||
|
||||
## Updating existing notebooks
|
||||
|
||||
If you are updating an existing notebook, please make sure to remove any existing cassettes for the notebook in `docs/cassettes` directory (each cassette is prefixed with the notebook name), and then run the steps from the "Adding new notebooks" section above.
|
||||
If you are updating an existing notebook, please make sure to remove any existing cassettes for the notebook in `cassettes` directory (each cassette is prefixed with the notebook name), and then run the steps from the "Adding new notebooks" section above.
|
||||
|
||||
To delete cassettes for a notebook, you can run:
|
||||
|
||||
```bash
|
||||
rm docs/cassettes/<notebook_name>*
|
||||
rm cassettes/<notebook_name>*
|
||||
```
|
||||
@@ -31,7 +31,7 @@ def request(self, method, url, body=None, headers=None):
|
||||
The result of calling the parent request method.
|
||||
"""
|
||||
# Update the inner socket's timeout value to send the request.
|
||||
# This only triggers if the connection is re-used.
|
||||
# This only triggers if the connection is reused.
|
||||
if getattr(self, "sock", None) is not None:
|
||||
self.sock.settimeout(self.timeout)
|
||||
|
||||
@@ -90,4 +90,4 @@ def patch_urllib3():
|
||||
return request(self, *args, **kwargs)
|
||||
|
||||
connection.HTTPConnection.request = new_request
|
||||
_PATCHED = True
|
||||
_PATCHED = True
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Read the list of notebooks to skip from the JSON file
|
||||
SKIP_NOTEBOOKS=$(python -c "import json; print('\n'.join(json.load(open('docs/notebooks_no_execution.json'))))")
|
||||
SKIP_NOTEBOOKS=$(python -c "import json; print('\n'.join(json.load(open('notebooks_no_execution.json'))))")
|
||||
|
||||
# Function to execute a single notebook
|
||||
execute_notebook() {
|
||||
@@ -27,7 +27,7 @@ if [ $# -gt 0 ]; then
|
||||
notebooks=$(echo "$@" | tr ' ' '\n' | grep -vFf <(echo "$SKIP_NOTEBOOKS"))
|
||||
else
|
||||
# Find all notebooks and filter out those in the skip list
|
||||
notebooks=$(find docs/docs/tutorials docs/docs/how-tos -name "*.ipynb" | grep -v ".ipynb_checkpoints" | grep -vFf <(echo "$SKIP_NOTEBOOKS"))
|
||||
notebooks=$(find docs/tutorials docs/how-tos -name "*.ipynb" | grep -v ".ipynb_checkpoints" | grep -vFf <(echo "$SKIP_NOTEBOOKS"))
|
||||
fi
|
||||
|
||||
# Execute notebooks sequentially
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import importlib
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import List, Literal, Optional
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
|
||||
from functools import lru_cache
|
||||
from typing import List, Literal, Optional
|
||||
|
||||
import nbformat
|
||||
from nbconvert.preprocessors import Preprocessor
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -52,6 +46,8 @@ MANUAL_API_REFERENCES_LANGGRAPH = [
|
||||
(["langgraph.constants"], "langgraph.types", "Interrupt", "types"),
|
||||
(["langgraph.constants"], "langgraph.types", "interrupt", "types"),
|
||||
(["langgraph.constants"], "langgraph.types", "Command", "types"),
|
||||
(["langgraph.func"], "langgraph.func", "entrypoint", "func"),
|
||||
(["langgraph.func"], "langgraph.func", "task", "func"),
|
||||
([], "langgraph.types", "RetryPolicy", "types"),
|
||||
([], "langgraph.checkpoint.base", "Checkpoint", "checkpoints"),
|
||||
([], "langgraph.checkpoint.base", "CheckpointMetadata", "checkpoints"),
|
||||
@@ -88,8 +84,6 @@ _IMPORT_LANGCHAIN_RE = _make_regular_expression("langchain")
|
||||
_IMPORT_LANGGRAPH_RE = _make_regular_expression("langgraph")
|
||||
|
||||
|
||||
|
||||
|
||||
@lru_cache(maxsize=10_000)
|
||||
def _get_full_module_name(module_path: str, class_name: str) -> Optional[str]:
|
||||
"""Get full module name using inspect, with LRU cache to memoize results."""
|
||||
@@ -109,6 +103,7 @@ def _get_full_module_name(module_path: str, class_name: str) -> Optional[str]:
|
||||
logger.warning(f"API Reference: Failed to load for class {class_name}, {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _get_doc_title(data: str, file_name: str) -> str:
|
||||
try:
|
||||
return re.findall(r"^#\s*(.*)", data, re.MULTILINE)[0]
|
||||
@@ -287,4 +282,4 @@ def update_markdown_with_imports(markdown: str) -> str:
|
||||
|
||||
# Apply the replace_code_block function to all matches in the markdown
|
||||
updated_markdown = code_block_pattern.sub(replace_code_block, markdown)
|
||||
return updated_markdown
|
||||
return updated_markdown
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Experimental script to generate consolidated llms text from the docs."""
|
||||
|
||||
import glob
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
from mkdocs.structure.files import File
|
||||
from mkdocs.structure.pages import Page
|
||||
|
||||
from notebook_hooks import _on_page_markdown_with_config
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
# Get source directory (parent of HERE / docs)
|
||||
SOURCE_DIR = os.path.abspath(os.path.join(os.path.dirname(HERE), "docs"))
|
||||
|
||||
|
||||
def _make_llms_text(output_file: str) -> str:
|
||||
"""Generate a consolidated text file from markdown/notebook files for LLM training.
|
||||
|
||||
Args:
|
||||
output_file: Path to output the consolidated text file
|
||||
"""
|
||||
# Collect all markdown and notebook files
|
||||
relative_paths = [
|
||||
# Files relative to docs/docs/
|
||||
"tutorials/introduction.ipynb",
|
||||
]
|
||||
all_files = [os.path.join(SOURCE_DIR, path) for path in relative_paths]
|
||||
|
||||
all_files.extend(
|
||||
glob.glob(os.path.join(SOURCE_DIR, "how-tos/*.md"), recursive=True)
|
||||
)
|
||||
all_files.extend(
|
||||
glob.glob(os.path.join(SOURCE_DIR, "how-tos/*.ipynb"), recursive=True)
|
||||
)
|
||||
# Add all concepts
|
||||
all_files.extend(
|
||||
glob.glob(os.path.join(SOURCE_DIR, "concepts/*.md"), recursive=True)
|
||||
)
|
||||
all_files.extend(
|
||||
glob.glob(os.path.join(SOURCE_DIR, "concepts/*.ipynb"), recursive=True)
|
||||
)
|
||||
|
||||
all_content = []
|
||||
|
||||
# Process each file
|
||||
for file_path in all_files:
|
||||
print(f"Processing {file_path}")
|
||||
rel_path = os.path.relpath(file_path, SOURCE_DIR)
|
||||
|
||||
# Create File and Page objects to match mkdocs structure
|
||||
file_obj = File(
|
||||
path=rel_path, src_dir=SOURCE_DIR, dest_dir="", use_directory_urls=True
|
||||
)
|
||||
page = Page(
|
||||
title="",
|
||||
file=file_obj,
|
||||
config={},
|
||||
)
|
||||
|
||||
# Read raw content
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Convert to markdown without logic to resolve API references
|
||||
processed_content = _on_page_markdown_with_config(
|
||||
content, page, add_api_references=False, remove_base64_images=True
|
||||
)
|
||||
if processed_content:
|
||||
# Add file name
|
||||
all_content.append(f"---\n{rel_path}\n---")
|
||||
# Add content
|
||||
all_content.append(processed_content)
|
||||
|
||||
# Write consolidated output
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
f.write("\n\n".join(all_content))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Generate consolidated text file from markdown/notebook files for LLMs."
|
||||
)
|
||||
)
|
||||
parser.add_argument("output_file", help="Path to output the consolidated text file")
|
||||
|
||||
args = parser.parse_args()
|
||||
_make_llms_text(args.output_file)
|
||||
@@ -22,6 +22,8 @@ class EscapePreprocessor(Preprocessor):
|
||||
)
|
||||
|
||||
elif cell.cell_type == "code":
|
||||
# Remove noqa comments
|
||||
cell.source = re.sub(r'#\s*noqa.*$', '', cell.source, flags=re.MULTILINE)
|
||||
# escape ``` in code
|
||||
cell.source = cell.source.replace("```", r"\`\`\`")
|
||||
# escape ``` in output
|
||||
|
||||
@@ -5,9 +5,10 @@ from typing import Any, Dict
|
||||
|
||||
from mkdocs.structure.files import Files, File
|
||||
from mkdocs.structure.pages import Page
|
||||
import posixpath
|
||||
|
||||
from notebook_convert import convert_notebook
|
||||
from generate_api_reference_links import update_markdown_with_imports
|
||||
from notebook_convert import convert_notebook
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig()
|
||||
@@ -15,6 +16,24 @@ logger.setLevel(logging.INFO)
|
||||
DISABLED = os.getenv("DISABLE_NOTEBOOK_CONVERT") in ("1", "true", "True")
|
||||
|
||||
|
||||
REDIRECT_MAP = {
|
||||
# lib redirects
|
||||
"how-tos/stream-values.ipynb": "how-tos/streaming.ipynb#values",
|
||||
"how-tos/stream-updates.ipynb": "how-tos/streaming.ipynb#updates",
|
||||
"how-tos/streaming-content.ipynb": "how-tos/streaming.ipynb#custom",
|
||||
"how-tos/stream-multiple.ipynb": "how-tos/streaming.ipynb#multiple",
|
||||
"how-tos/streaming-tokens-without-langchain.ipynb": "how-tos/streaming-tokens.ipynb#example-without-langchain",
|
||||
"how-tos/streaming-from-final-node.ipynb": "how-tos/streaming-specific-nodes.ipynb",
|
||||
"how-tos/streaming-events-from-within-tools-without-langchain.ipynb": "how-tos/streaming-events-from-within-tools.ipynb#example-without-langchain",
|
||||
# cloud redirects
|
||||
"cloud/index.md": "concepts/index.md#langgraph-platform",
|
||||
"cloud/how-tos/index.md": "how-tos/index.md#langgraph-platform",
|
||||
"cloud/concepts/api.md": "concepts/langgraph_server.md",
|
||||
"cloud/concepts/cloud.md": "concepts/langgraph_cloud.md",
|
||||
"cloud/faq/studio.md": "concepts/langgraph_studio.md#studio-faqs",
|
||||
}
|
||||
|
||||
|
||||
class NotebookFile(File):
|
||||
def is_documentation_page(self):
|
||||
return True
|
||||
@@ -106,7 +125,14 @@ def _highlight_code_blocks(markdown: str) -> str:
|
||||
return markdown
|
||||
|
||||
|
||||
def on_page_markdown(markdown: str, page: Page, **kwargs: Dict[str, Any]):
|
||||
def _on_page_markdown_with_config(
|
||||
markdown: str,
|
||||
page: Page,
|
||||
*,
|
||||
add_api_references: bool = True,
|
||||
remove_base64_images: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
if DISABLED:
|
||||
return markdown
|
||||
if page.file.src_path.endswith(".ipynb"):
|
||||
@@ -114,7 +140,76 @@ def on_page_markdown(markdown: str, page: Page, **kwargs: Dict[str, Any]):
|
||||
markdown = convert_notebook(page.file.abs_src_path)
|
||||
|
||||
# Append API reference links to code blocks
|
||||
markdown = update_markdown_with_imports(markdown)
|
||||
if add_api_references:
|
||||
markdown = update_markdown_with_imports(markdown)
|
||||
# Apply highlight comments to code blocks
|
||||
markdown = _highlight_code_blocks(markdown)
|
||||
|
||||
if remove_base64_images:
|
||||
# Remove base64 encoded images from markdown
|
||||
markdown = re.sub(r"!\[.*?\]\(data:image/[^;]+;base64,[^\)]+\)", "", markdown)
|
||||
|
||||
return markdown
|
||||
|
||||
|
||||
def on_page_markdown(markdown: str, page: Page, **kwargs: Dict[str, Any]):
|
||||
return _on_page_markdown_with_config(
|
||||
markdown,
|
||||
page,
|
||||
add_api_references=True,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# redirects
|
||||
|
||||
HTML_TEMPLATE = """
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Redirecting...</title>
|
||||
<link rel="canonical" href="{url}">
|
||||
<meta name="robots" content="noindex">
|
||||
<script>var anchor=window.location.hash.substr(1);location.href="{url}"+(anchor?"#"+anchor:"")</script>
|
||||
<meta http-equiv="refresh" content="0; url={url}">
|
||||
</head>
|
||||
<body>
|
||||
Redirecting...
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def write_html(site_dir, old_path, new_path):
|
||||
"""Write an HTML file in the site_dir with a meta redirect to the new page"""
|
||||
# Determine all relevant paths
|
||||
old_path_abs = os.path.join(site_dir, old_path)
|
||||
old_dir_abs = os.path.dirname(old_path_abs)
|
||||
|
||||
# Create parent directories if they don't exist
|
||||
if not os.path.exists(old_dir_abs):
|
||||
os.makedirs(old_dir_abs)
|
||||
|
||||
# Write the HTML redirect file in place of the old file
|
||||
content = HTML_TEMPLATE.format(url=new_path)
|
||||
with open(old_path_abs, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
# Create HTML files for redirects after site dir has been built
|
||||
def on_post_build(config):
|
||||
use_directory_urls = config.get("use_directory_urls")
|
||||
for page_old, page_new in REDIRECT_MAP.items():
|
||||
page_old = page_old.replace(".ipynb", ".md")
|
||||
page_new = page_new.replace(".ipynb", ".md")
|
||||
page_new_before_hash, hash, suffix = page_new.partition("#")
|
||||
old_html_path = File(page_old, "", "", use_directory_urls).dest_path.replace(
|
||||
os.sep, "/"
|
||||
)
|
||||
new_html_path = File(page_new_before_hash, "", "", True).url
|
||||
new_html_path = (
|
||||
posixpath.relpath(new_html_path, start=posixpath.dirname(old_html_path))
|
||||
+ hash
|
||||
+ suffix
|
||||
)
|
||||
write_html(config["site_dir"], old_html_path, new_html_path)
|
||||
|
||||
@@ -7,7 +7,7 @@ import click
|
||||
import nbformat
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
NOTEBOOK_DIRS = ("docs/docs/how-tos","docs/docs/tutorials")
|
||||
NOTEBOOK_DIRS = ("docs/how-tos","docs/tutorials")
|
||||
DOCS_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
CASSETTES_PATH = os.path.join(DOCS_PATH, "cassettes")
|
||||
|
||||
@@ -19,36 +19,37 @@ BLOCKLIST_COMMANDS = (
|
||||
)
|
||||
|
||||
NOTEBOOKS_NO_CASSETTES = (
|
||||
"docs/docs/how-tos/visualization.ipynb",
|
||||
"docs/docs/how-tos/many-tools.ipynb"
|
||||
"docs/how-tos/visualization.ipynb",
|
||||
"docs/how-tos/many-tools.ipynb"
|
||||
)
|
||||
|
||||
NOTEBOOKS_NO_EXECUTION = [
|
||||
# this uses a user provided project name for langsmith
|
||||
"docs/docs/tutorials/tnt-llm/tnt-llm.ipynb",
|
||||
"docs/tutorials/tnt-llm/tnt-llm.ipynb",
|
||||
# this uses langsmith datasets
|
||||
"docs/docs/tutorials/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb",
|
||||
"docs/tutorials/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb",
|
||||
# this uses browser APIs
|
||||
"docs/docs/tutorials/web-navigation/web_voyager.ipynb",
|
||||
"docs/tutorials/web-navigation/web_voyager.ipynb",
|
||||
# these RAG guides use an ollama model
|
||||
"docs/docs/tutorials/rag/langgraph_adaptive_rag_local.ipynb",
|
||||
"docs/docs/tutorials/rag/langgraph_crag_local.ipynb",
|
||||
"docs/docs/tutorials/rag/langgraph_self_rag_local.ipynb",
|
||||
"docs/tutorials/rag/langgraph_adaptive_rag_local.ipynb",
|
||||
"docs/tutorials/rag/langgraph_crag_local.ipynb",
|
||||
"docs/tutorials/rag/langgraph_self_rag_local.ipynb",
|
||||
# this loads a massive dataset from gcp
|
||||
"docs/docs/tutorials/usaco/usaco.ipynb",
|
||||
"docs/tutorials/usaco/usaco.ipynb",
|
||||
# TODO: figure out why autogen notebook is not runnable (they are just hanging. possible due to code execution?)
|
||||
"docs/docs/how-tos/autogen-integration.ipynb",
|
||||
"docs/how-tos/autogen-integration.ipynb",
|
||||
"docs/how-tos/autogen-integration-functional.ipynb",
|
||||
# TODO: need to update these notebooks to make sure they are runnable in CI
|
||||
"docs/docs/tutorials/storm/storm.ipynb", # issues only when running with VCR
|
||||
"docs/docs/tutorials/lats/lats.ipynb", # issues only when running with VCR
|
||||
"docs/docs/tutorials/rag/langgraph_crag.ipynb", # flakiness from tavily
|
||||
"docs/docs/tutorials/rag/langgraph_adaptive_rag.ipynb", # flakiness only when running in GHA
|
||||
"docs/docs/tutorials/rag/langgraph_self_rag.ipynb", # flakiness only when running in GHA
|
||||
"docs/docs/tutorials/rag/langgraph_agentic_rag.ipynb", # flakiness only when running in GHA
|
||||
"docs/docs/how-tos/map-reduce.ipynb", # flakiness from structured output, only when running with VCR
|
||||
"docs/docs/tutorials/tot/tot.ipynb",
|
||||
"docs/docs/how-tos/visualization.ipynb",
|
||||
"docs/docs/tutorials/llm-compiler/LLMCompiler.ipynb"
|
||||
"docs/tutorials/storm/storm.ipynb", # issues only when running with VCR
|
||||
"docs/tutorials/lats/lats.ipynb", # issues only when running with VCR
|
||||
"docs/tutorials/rag/langgraph_crag.ipynb", # flakiness from tavily
|
||||
"docs/tutorials/rag/langgraph_adaptive_rag.ipynb", # flakiness only when running in GHA
|
||||
"docs/tutorials/rag/langgraph_self_rag.ipynb", # flakiness only when running in GHA
|
||||
"docs/tutorials/rag/langgraph_agentic_rag.ipynb", # flakiness only when running in GHA
|
||||
"docs/how-tos/map-reduce.ipynb", # flakiness from structured output, only when running with VCR
|
||||
"docs/tutorials/tot/tot.ipynb",
|
||||
"docs/how-tos/visualization.ipynb",
|
||||
"docs/tutorials/llm-compiler/LLMCompiler.ipynb"
|
||||
]
|
||||
|
||||
|
||||
@@ -216,7 +217,7 @@ def process_notebooks(should_comment_install_cells: bool) -> None:
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing {notebook_path}: {e}")
|
||||
|
||||
with open(os.path.join(DOCS_PATH, "notebooks_no_execution.json"), "w") as f:
|
||||
with open("notebooks_no_execution.json", "w") as f:
|
||||
json.dump(NOTEBOOKS_NO_EXECUTION, f)
|
||||
|
||||
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python
|
||||
"""Create the third party page for the documentation."""
|
||||
|
||||
import argparse
|
||||
from typing import List
|
||||
from typing import TypedDict
|
||||
|
||||
import yaml
|
||||
|
||||
MARKDOWN = """\
|
||||
[//]: # (This file is automatically generated using a script in docs/_scripts. Do not edit this file directly!)
|
||||
# 🚀 Prebuilt Libraries
|
||||
|
||||
LangGraph includes a prebuilt React agent. For more information on how to use it,
|
||||
check out our [how-to guides](https://langchain-ai.github.io/langgraph/how-tos/#prebuilt-react-agent).
|
||||
|
||||
If you’re looking for other prebuilt libraries, explore the community-built options
|
||||
below. These libraries can extend LangGraph's functionality in various ways.
|
||||
|
||||
## 📚 Available Libraries
|
||||
|
||||
{library_list}
|
||||
|
||||
## ✨ Contributing Your Library
|
||||
|
||||
Have you built an awesome open-source library using LangGraph? We'd love to feature
|
||||
your project on the official LangGraph documentation pages! 🏆
|
||||
|
||||
To share your project, simply open a Pull Request adding an entry for your package in our [packages.yml]({langgraph_url}) file.
|
||||
|
||||
**Guidelines**
|
||||
|
||||
- Your repo must be distributed as an installable package (e.g., PyPI for Python, npm
|
||||
for JavaScript/TypeScript, etc.) 📦
|
||||
- The repo should either use the Graph API (exposing a `StateGraph` instance) or
|
||||
the Functional API (exposing an `entrypoint`).
|
||||
- The package must include documentation (e.g., a `README.md` or docs site)
|
||||
explaining how to use it.
|
||||
|
||||
We'll review your contribution and merge it in!
|
||||
|
||||
Thanks for contributing! 🚀
|
||||
"""
|
||||
|
||||
|
||||
class ResolvedPackage(TypedDict):
|
||||
name: str
|
||||
"""The name of the package."""
|
||||
repo: str
|
||||
"""Repository ID within github. Format is: [orgname]/[repo_name]."""
|
||||
weekly_downloads: int | None
|
||||
"""The weekly download count of the package."""
|
||||
description: str
|
||||
"""A brief description of what the package does."""
|
||||
|
||||
|
||||
def generate_markdown(resolved_packages: List[ResolvedPackage], language: str) -> str:
|
||||
"""Generate the markdown content for the third party page.
|
||||
|
||||
Args:
|
||||
resolved_packages: A list of resolved package information.
|
||||
language: str
|
||||
|
||||
Returns:
|
||||
The markdown content as a string.
|
||||
"""
|
||||
# Update the URL to the actual file once the initial version is merged
|
||||
if language == "python":
|
||||
langgraph_url = (
|
||||
"https://github.com/langchain-ai/langgraph/blob/main/docs"
|
||||
"/_scripts/third_party_page/packages.yml"
|
||||
)
|
||||
elif language == "js":
|
||||
langgraph_url = (
|
||||
"https://github.com/langchain-ai/langgraphjs/blob/main/docs"
|
||||
"/_scripts/third_party/packages.yml"
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Invalid language '{language}'. Expected 'python' or 'js'.")
|
||||
|
||||
sorted_packages = sorted(
|
||||
resolved_packages, key=lambda p: p["weekly_downloads"] or 0, reverse=True
|
||||
)
|
||||
rows = [
|
||||
"| Name | GitHub URL | Description | Weekly Downloads |",
|
||||
"| --- | --- | --- | --- |",
|
||||
]
|
||||
for package in sorted_packages:
|
||||
name = f"**{package['name']}**"
|
||||
repo_url = f"[{package['repo']}](https://github.com/{package['repo']})"
|
||||
downloads = package["weekly_downloads"] or 0
|
||||
row = f"| {name} | {repo_url} | {package['description']} | {downloads} |"
|
||||
rows.append(row)
|
||||
markdown_content = MARKDOWN.format(
|
||||
library_list="\n".join(rows), langgraph_url=langgraph_url
|
||||
)
|
||||
return markdown_content
|
||||
|
||||
|
||||
def main(input_file: str, output_file: str, language: str) -> None:
|
||||
"""Main function to create the third party page.
|
||||
|
||||
Args:
|
||||
input_file: Path to the input YAML file containing resolved package information.
|
||||
output_file: Path to the output file for the third party page.
|
||||
language: The language for which to generate the third party page.
|
||||
"""
|
||||
# Parse the input YAML file
|
||||
with open(input_file, "r") as f:
|
||||
resolved_packages: List[ResolvedPackage] = yaml.safe_load(f)
|
||||
|
||||
markdown_content = generate_markdown(resolved_packages, language)
|
||||
|
||||
# Write the markdown content to the output file
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Create the third party page.")
|
||||
parser.add_argument(
|
||||
"input_file",
|
||||
help="Path to the input YAML file containing resolved package information.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"output_file", help="Path to the output file for the third party page."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--language",
|
||||
choices=["python", "js"],
|
||||
default="python",
|
||||
help="The language for which to generate the third party page. Defaults to 'python'.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args.input_file, args.output_file, args.language)
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python
|
||||
"""Retrieve download count for a list of Python packages from PyPI."""
|
||||
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from typing import TypedDict
|
||||
import pathlib
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
|
||||
|
||||
class Package(TypedDict):
|
||||
"""A TypedDict representing a package"""
|
||||
|
||||
name: str
|
||||
"""The name of the package."""
|
||||
repo: str
|
||||
"""Repository ID within github. Format is: [orgname]/[repo_name]."""
|
||||
description: str
|
||||
"""A brief description of what the package does."""
|
||||
|
||||
|
||||
class ResolvedPackage(Package):
|
||||
weekly_downloads: int | None
|
||||
|
||||
|
||||
HERE = pathlib.Path(__file__).parent
|
||||
PACKAGES_FILE = HERE / "packages.yml"
|
||||
PACKAGES = yaml.safe_load(PACKAGES_FILE.read_text())['packages']
|
||||
|
||||
|
||||
def _get_weekly_downloads(packages: list[Package]) -> list[ResolvedPackage]:
|
||||
"""Retrieve the monthly download count for a list of packages from PyPIStats."""
|
||||
resolved_packages: list[ResolvedPackage] = []
|
||||
|
||||
for package in packages:
|
||||
url = f"https://pypistats.org/api/packages/{package['name']}/overall"
|
||||
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
sorted_data = sorted(
|
||||
data["data"],
|
||||
key=lambda x: datetime.strptime(x["date"], "%Y-%m-%d"),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
# Sum the last 7 days of downloads
|
||||
num_downloads = sum(entry["downloads"] for entry in sorted_data[:7])
|
||||
|
||||
resolved_packages.append(
|
||||
{
|
||||
"name": package["name"],
|
||||
"repo": package["repo"],
|
||||
"weekly_downloads": num_downloads,
|
||||
"description": package["description"],
|
||||
}
|
||||
)
|
||||
|
||||
return resolved_packages
|
||||
|
||||
|
||||
|
||||
def main(output_file: str) -> None:
|
||||
"""Main function to generate package download information.
|
||||
|
||||
Args:
|
||||
output_file: Path to the output YAML file.
|
||||
"""
|
||||
resolved_packages: list[ResolvedPackage] = _get_weekly_downloads(PACKAGES)
|
||||
|
||||
if not output_file.endswith(".yml"):
|
||||
raise ValueError("Output file must have a .yml extension")
|
||||
|
||||
with open(output_file, "w") as f:
|
||||
f.write("# This file is auto-generated. Do not edit.\n")
|
||||
yaml.dump(resolved_packages, f)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate package download information."
|
||||
)
|
||||
parser.add_argument(
|
||||
"output_file",
|
||||
help=(
|
||||
"Path to the output YAML file. Example: python generate_downloads.py "
|
||||
"downloads.yml"
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args.output_file)
|
||||
@@ -0,0 +1,5 @@
|
||||
#A list of third-party packages to surface on the third-party page.
|
||||
packages:
|
||||
- name: "trustcall"
|
||||
repo: "hinthornw/trustcall"
|
||||
description: "Tenacious tool calling built on LangGraph"
|
||||
+1
@@ -0,0 +1 @@
|
||||
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
|
||||
+1
@@ -0,0 +1 @@
|
||||
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
|
||||
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
||||
eNqNVVtMHFUYXnoleKnGSL01nG4qJHVnd2Z2C+z6oLBQ2yIXYZtKUcnZmcPOwMyc6cxZ2uVmhPqk0M5TE/WpLLtmxQKxalP1SUiqVmyaNkqjyIsxhgRjfCs2eGbZ5VJu3aeZ8//f/33//51/tjfZjgxTxlrOsKwRZECB0BfT6k0a6FQUmeRsQkVEwmK8rrYhNBg15KnnJUJ0M+DxQF12Q41IBtZlwS1g1dPOeVRkmjCCzHgYi7E7ObmdThWeaSa4DWmmMwA4lve5gDObRU+aOp0GVhB9ckZNZDhpVMBUikbsI0neD44WqSCMw85uF1jOhaYpm4TS3wc4IoNyHF4EBRUYFZEb1MgCAgQDFSECYji6HxzBp4EANXAUSEjR7TMaF2HspdUk6wg6LUFSZAI1BjSoIpr/pt0OFpFiR4U0I+NlDjEm1jREGAUSOkVnd1JCUKSjPheXsEmssTXDG4GCgHTCIE3AoqxFrE8jHbLuAiJqsWukBLte2h0r1YaQzkBFbkeJRZQ1CnVdkQVoxz2tlHs4o5khMR2tDafs1hhqgUasL8uyOjx1Meq1Bli31+fmR88wdMKyplCzaB9UUkJPx79aGdCh0EbrMJl7ZCUWwZdW5mDTGqqGQm3DqpLQECRrCBpqse+zledGVCOyiqxksG4tXSa4TOd1c5zbP7aqsBnTBGuoBSomGlsa8hIkxbO8l2GLGZa7lJ2SgrQIkaxBvtj3sYFMne4A6kvQkiRq9sapI+j6tWTm1l6srcq6Oe3YG6+g7ljfnECiC/A8OEZvFa1/CPBcgPUHOA68Uh0aDmZoQuuaMRYyoGa2UEMqs+YnBSmqtSExFVzX9innclsG5VdkVSZMZmWpWfarFfexLDtVuGmmgVQ6NZsx7vX7/VvUpZNBxLps90eHx/B8KNMle3J9HlnTo/QKprc/oyphq6K6Dm6Zv6wtiyl8AMwGCrmTU0XroXGUrJE4VJpme2Hr/GWJGUzRg2A2lgjWg983vkWiA5tkrhzcYjbYNHtDPamM84wsWl/T52aWa2wMHe84Vl7devzVGkmr9tbprWYJGmyXoZXi3ByIYBxR0EjwMBOEgoSYhvQKWcmKxpqy6qPB4deZehzG9C6FIL1zGtZQogEZdDWtlKDgqEg/dgZKUHh9WaN12c8W86XFLB+GXlQi8j6m8kT9aHaZlpYlbn8p038479CVNejRRE5OwXu5jvRvu3iurGr85YffXfj+/L+hOyc95huP7ZmdftLiJ8d/u6TWFz09ytz7++bEzomGmfnpzvM7r+Y+usv/Bfr87dLrt7u6uj6Y++mvy1Xeu9/27Hgo0H9lqG684HHXgbmSmcrAa7V5+bsGfunruzH/3NgVZi4POm9ddM8mDs4+49pRWFmve36Ihv5p+6R5oC8y+d35ghsk3P5fS/9e76niP12tgZvj3O73n63qvTf/yPBIfkVMeaIDbtt9ZOz2R7k9Z2cih/NyB4zfm65d8Py6bV9/Y9Mfew7k/Hwrv3dPkRIsH9nXMF9S91Z3ecHCi5N3Pwz9eLVnu8OxsLDdIZY/lWrOcTj+B1mCCwM=
|
||||
+1
@@ -0,0 +1 @@
|
||||
eNqNVGtMHFUUXoQ0FavRGAt/bCcThCrMMjMsWxarlAIlpVIorKW8bG5nLuzAvJjH8rKmYKkxlbRDUxoTCElZdulKYRGMTaiExpKYtFFrDbAxaeIfpRok0USNreDdZReovDrJJDP3nO+c73znnNvqcUJF5SQxYoATNagARkM/qtHqUWCdDlXtjFuAmkNiXYUFxfZeXeH8rzg0TVbTk5OBzJmBqDkUSeYYMyMJyU4qWYCqCqqh6jopsY3++mZcAA0nNKkWiiqejlEkbUnC8LATOilvxhWJh+gL11Wo4MjKSIiJqAWO6h1AS1AxoRETgQAz8FOVAbTEQj5gZXigs5BIIVIJVRJFqBE80BBn/JTHAQGLCjvvckiqZgyvoToEGAbKGgFFRmI5sdq4Vt3EyUkYC6sCMbxMIF5QC8NbC6FMAJ5zQvcSyvABWeY5BgTsyTUo90CIM6E1ynCt2RsojUAVi5rxeWaYR3JhI1JWxEhzisVM+xoIVQOcyCNtUB2IklsO2sdWG2TA1KI4RKhrhnsJPLjaR1KNvnzAFBQ/FhIojMPoA4pgtYysPld0UeMEaHiyCtemCxlX0qWYKcpsG34ssNooMkZfFeBVOLws8jLES5N0CkFaCZIaDKvEQ7Facxi9FGXrV6Aqo4mD77tRSE1XW12oI/DOV57QkFwpOBzu5n1TjCsbdcf4ogSySRhNY3lAxFD8VIym0klbOkVjufn2gaxQGvu6zRi2K0BUq1BDcsLN9zAOXayFrDdr3bb78ZWyFJSf5wROI0ILgpoV+DVcFpIk/fGbeipQQKoFMrpSbDbbFnGRMlAzRgP1IfEImraHqqTK1s/DibKORjC4bCFW7gArxOu1Lf1XuIUx8U+A2YAhXeZPWA8t6doain1pwWyJW/uvUAxhEp4EszFFbD34/+RbShS3iedq4Za8sU29N+TjDXWe4FjjBvo+QVJHGw5AzuI8mi2WHM+rLSnJrSm0C1KvkwOGlzJTWLUkVfNwKOsgkQUYBySKgytkeLJLj2TmH8oaOE4USSclNEt2gGZOlEToLoYKWk3Dy/CSzqLLToFuBC/KLDVGbaSVTrOSaawtjdzL0hYip6TIF16m5WVxBW7K4PXeglZWQUeTEW/vPrfdFHwi0bu4yF7IPxyZuePsYt6dyduDo+VvXI95tb/w23Otu7I766z6B1/H6x1XO79bNI+V9TdE75vPPeCMLRsYnv6riu7O2Nc01nP3J+fDR1pGzPi/12+Mf/mUMOa+meP9rds3EVdnOpLab7Mm+S534lOzvbeJSyO/X7GD+MRPDulvdttf2jnb1j7je8//cl77+Nm/02oqTv/RWflRuaOt4NljST2+S12nKwsu2Dp+MU3sOTj7IErsmp94RtpzdQTb0XMzffbp7yvoNojPRRe2TEq1P1ujWi53/NNzpvkiz8zd3TZXgW93bvumg3n909hjUfHPz3SM+u81dBVVvPPjwmf7U0v3ftgVzbTfEhgbtRCdt/vjmRenupgk8vziQH1sQ/c1qvSFxKl73v3z7+a2F1h2zTxKfeu5Jnx6YTruB8o29OdDKahdpOnXnbfuP4gwmf4DBVoOEg==
|
||||
+1
@@ -0,0 +1 @@
|
||||
eNqNVF1sFFUUbmlTEYPBYCTGGIahpQY6uzOz28q2prpua2Vr6d+G/gji7czt7tCZucPMncqKgJZiNI3iGAzExBe7P82mlFZRVGqEGgTUpn0huCiYJhp5MvEBmz7VO9vdttilZZ7u3HO+c77znXNuT7wb6oaE1NxBScVQBwImP4bVE9fhfhMauDemQBxCYqShvjnQb+pSsiiEsWaUO51AkxxAxSEdaZLgEJDi7OacCjQMEIRGpAOJ4aRxkFbAgb0YdUHVoMspjuXdJRSdcSI3Lx+kdSRDcqJNA+o0sQqIMFGxfRWSNlE7ihWqA3XQh/bYSCRC2bYIMjBFyLiYUsZAqgoxIwNM+NKH4iEIRFLU8UgIGdgaWULzDBAEqGEGqgISJTVonQ6+LmkllAg77RgJwY6X0sFKdEGoMUCWumFsDmUNA02TJQHYduc+knswzZfBYQ0uNSfsshhSrYqtc94MD2dDmKiqUqzD5XbwwwcYAwNJlYkupA5CKaal7OcXGzQgdJE4TLpjVmwOPLTYBxlWtA4I9c13hQS6ELKiQFfK3J8vvtdNFUsKtOK+hqXp0saFdC4Hxzk8I3cFNsKqYEU7gWzAkXmR5yEJnuVdDFvGsNxQRiUZqkEcsvo5rnRAh4ZGpg0ejZGQ2DR6IqQj8Ocr8fSAfFpfm+nmrZwNkSrSHevbFiiWUDxP+YFKkfilFM+Vs55yjqVq6gKDvnSaQNZmjAR0oBqdpCHVmebHhZCpdkEx4cva9iS9UJZO8suSImEmvRykWfavFXGzLJvcsqynDhWimp0x4vJ4PCvEJcpAbJ216yPiMTwfmKuS9bRnzyOpmklGMLVoaVYxmxXhtXVF/wVuGcyW+8BkZ8ix7cnibGhk4iUUo9tT2bat7L9AMY0pvh/MvSlS2eD/k28uUeEynouFm/OmlvW+J59EuvOMJFqj5LyX5ZqQx2sohrC/1CeJ1Ri5db/ZsLO/WwJWgnNwVBChoAzP+F5gfEAIQaY5tUJWvKptp7duh2+wlWlCHYjMUgCQmVORCmPNUCeraSUEGZkieex0GCPwJm+bddbDlvHbSzuhS+Q6nhZ5N1Pd0jScWab5ZYnYL2XqaX+LrKxOri7lrtvYtzon9eWJx7213z+37tjsj92OV8o+lO98tsHbeKNvYPUaGtx88tfo1Zc+/iT69uSs48HHHq3P+3361tHHNxUMT8bq9uw6F579qqL35PWnJl6rv6nPJHdN5bqvTE6dGDvxW4HXNzGev9vPvbs5dy3zgHyhB+1+uLWk7yeae8QP/EbjRz+8WTBR+OKqf5Vn3ltzuO7OTGtrtTm+8aEvKruujY/iob/7a4oG26sa8/461v6PN1yz7/SXv+D1zWMX17Zob1z/oPf8NzUnT11Y1TRQNJ3/LD1VuTmcX/u15P5jpqji+Yijlhn1+V/d1n+x4rtLN6ZvF/LXjuT9eXX69tiRy22H1wvvX0ak9NnZvJzkE+9Uyrk5Of8BO5PlsA==
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
eNqNVn1sE+cZJ+1gnQYarUq3dd12dQvVUO585ztf7ERhMY4TQhpMEueLhFmv796zL7kv33vnxIFMG63oRhnbrXTR1nXtINgoDSkfWUpp6B9FVSlirdiH1NAsYqrWTkysgoqOtWvZe45dnIGmWvLHe+/veZ7f8zy/93m9I5+BJpJ1rWJC1ixoAsHCC+TsyJswbUNkPZpToZXSxbHN0fbYftuUZ9emLMtA1V4vMGRKN6AGZErQVW+G8QopYHnxb0OBBTdjCV3Mnr9t7zaPChECSYg81UTvNo+g41iahReeLmzyECKsFCQGIcBfJiFrBAIaIZlAE2Qk6N/1VBIeU1egi7cRND0jlUS5E81WlDIIQEhGFsBb+KGl60pcAIpSDG1ljQJIsrVCqi5GFt0nLiie4DJcj2yaYmcCqv1I7dxob+ivT0dc2Gcm1Ti6BtSCnyS04kXiLgaYSVvFrNxonm19HkUXgGvTh9d9nvaGSiIc6vOMeEZGtv5PEp4mtw7I1rTs/eUJuwksSiT+eeiObMUmqi5CpUDSsEhOJ1VZk11XyDIhUPGGBBQEi77/T30+T+IiRIIpG0WYJ4zZEZZOYNSi3kqmrhKAQAYUZEkWiFJ9KNeHAUzsHKsQFSIZJlaXaclwYVmCFhYlmjgVWUvicroVw4qVTehWp/cG2i1ECa0n+qFgYTSufj4FgYhDzS9ZOZbSkeVMLtbz80AQIK4b1ARdxDGcQ8lh2agkRCgpwILjuHMaLJTFGR+A0CCBImdgbsHKOQwMQ5EXKHj7ka5NFDtNulxu3h53dU3iE6JZzlQUkwg1eTdn8cHTCIbyB6mqw0Mk1rSsKfggkQrAfHJGYf+l8g0DCAPYCVk81E5uwXiyHKMj50ALEKLti1wCU0g5B4Cp8tyx8uemrVmyCp18ePPN4YqbN8KxFMNQVUcWOUZZTXAOFKT2wiJjaJlZUtCxD+e3dE7Q9QEZOrNX4nFBiifUWqalM9Dlp9tbKcnXkk1KA0NSKlTFg46NYrOyCbayUjLano6g5KZWkqliq/yBqiDHkAxFUwzFkEJSA1mmS6U6LbsnA7Np/0BsEEicP5aM+ECUahPC0cB6KhtuDG70N7Lrm9GWbqh0bungRb4hzgxR6xMNkVa5v+nhtM8WqnSYbqXEUA2B2dkZWayl/JbKN26Qw9mu+pAaaE01DGq8SetqcNOWTDYda25JD9MbHo7HxXJ6PMeRdJEhT3MB2n1NlrShQC1ppZz9PE8fNCEy8AiFj+RwySwb7RjDOoRnT+eLo3RftPmGhO8Zq8eadE52QbGS8PmIjXh++mifn/Cx1UywmuaIxpbYRLgYJnZLCR6J4YGLJCzDSEnyeSFlawNQHA/fUuwnXbHjTrr08bgi4ZChI0gWWTkT3WTbwiVCNtUfWzhZpG4mgSYPF8I6JwuqHxweGhQFWxRTmUGVDg5zrJyAtiBNFU3wHHDDYEKkipz9nM83Wdwp6W4c50qTDE3SzIkh0sSlUGRVxvUsfBZvMuSM+XGxj98MsPQBiO+8PFfoBv1yOcKEKhasG/uGGy4YDM7cGlRyxWJIkA+cWIxCsJwN41PR8ZsBRRf7aDQxVEKTsujMPogXcRYk/Dyb8AW5QJAWaU4IBHgfLonPxwMJV+FFdyIK2IvbTEM3LRJBAV/bVtaZrVTBkDtjalnGz/I40xp81wqKLcJ2O1GvuzmgGsIwoaID8flwAxkGQgqS7QX9Ofn6nk2hlqbwdDdZLiQyaiz8ZchrOtJkScq1QxM3xhkXFN0W8bA0YQ77agv1OFNBmmc5juMlifUHJClIrsdjqOTtM9mNuZM2DxTMPSM4x1Jsraea41hPDaGC2gCP21T4Y/HD3ML0f7Vi8tuP37Gk8Lodv69f390W0t6mV878Y1Xvbw690nnf6cvk6PQoIQ/cXbGr7tnXn5xaEbmrb23LvdfeX7r67aPnlp2OPPbLp8+dNePsktCWidDBzoY/Z5+Is9/P/mGnfuqTq7/f+vE7b45cvvpBw8j+9/bN/ain7fglb+zysx8d7HtvRYOZGeuo+zrzInXm8Stmx/dI77J96N6Wn/vXvds8Oih+x/vuq75nRv+d+F3XC3vuPtp96s3lSy7Y13K/7nznWPep7Zm6avEXK3ZdTd9R94XkXZ4H2fReftds/drkA+fv/ygXufYT3/TXflC5ffSedUvlVUfMuuVzvR9f+WvsvsP/7MndNl0l/fT9PfLuge1G7/wHjTNrmuc6K1bXPPXp2XNZ5vBLyy+A1F+eWXXnuuNvvLb2ROPU60bH7kzi/NV66sNv3HkuIq/5z0WiGj3HNTTtjE6v2RsjLzYFD3V/+Ku5LzlPW59Wnb0+89q8+NS/3oo+2vb3By6e+eKfxt74clSo+OPql/2Zucsru+mdD7V1R1752XxIILex849d+Oqetzr8s8se+Qr3raZLzx2t23rm0t/afqyMfrLUbcrtS2bg6W/6cIf+C2XGBts=
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
-1
@@ -1 +0,0 @@
|
||||
eNrtGUtv48Y52566pwZo7wRRoEAhyqSot+GDLTu2d2PLtrT2rhNDGA2HIm1yhuYMZckLHbLpvWDQP9CsI6eGs0mQoE3Spuceit69h/yI/oJ8Q0lrOfbGm2SDJpAFQRQ13/vNT4+O2yTkLqO3Tl0qSIiwgBv+zqPjkOxHhIs/9n0iHGYdrVVr9cdR6J79wREi4OWpKRS4aRYQitw0Zv5U25jCDhJT8D3wSELmqMms7tNbrYeqTzhHLcLVsvLGQzVkHoFvasRJqKYUFTPgTYX86QBI/J4rLlWEQ5QmsULGfLW3A1A+s4gnYVqB0Mx0ThNR2GQSn4uQIB+ORBgRuBeMeUNWohskrOyIJqpJ8Gffy8pDlSI/AWgR0XAF8bmEsAjHoRsMgdR7nIA4LlckYfhQPMb2lChQDhwXO0qCpqCQjMRuuW1ClcBDmKQluQCFwAXMyxOWI5lYc5dgkQCEYMhQuGQAkGBeAAUNXdpSez0Alp5xQ2JJBYegO71eb6d37BBkAZM/HTmMi/jJRQd9iDAmYDlCMbOAWPxB69ANUopFbA8JcgJOoCSxS3yyR0igIQ/U6A+w4o9QEHguRvJ8apczejp0miZFvHx8In2rgcupiD+f5V2KqyDJ7PLUWhfCiSpGOltM6x91NC6QSz0ID81DIFQ/SM7/MX4QILwHlLRhqMb9AfKTcRjG4/dWEK7WLpBEIXbi91Do57OfjP8eRlS4PomPK2uX2Q0Pz9mZacNIlz6+QFhqFH+QXMrJp8v+foEIEWFXwwxoxX/Rn4yM5RHaEk782MyV3g8JDyBJyNt9QBMRf3QEjiH/+ffxMFnerd4defSrV357NA9Oir+sO1FKyeSVGgmUjJ7JKoZZzsq3srhSP60M2dSlT84UQTpiirTlL4MUmVYgRUNOxEwkbK34cT1ElNvgqIVRUBxjJ6J7xDqpXBkOX8pwAPWkPpDFGukEjBNtKGZ8el/bGNQNbXn+k0HsaSxsIeoeJrER/1V6GYRw6afDY4h9SRKYaz6PH2eymSfDk5EDTkBRXTN0TTc+l4mAIeik4AELQTGCoSqJbnyW8lFHRtyMaeTMvK7r05CP2IssUoua88wHnnxaCULiMWR90dFCMKjn+i54JfkcVjwIJAOQ9c8uQwi2RyiP38/pg9e/xkFCIjlINZ4ROirB659XA41oZSVMqVD44iIY+OiczuO8zz+7fD4k8a7OTzsjYM214rPfwU2jVLJLmWyuqBvZnNksZQyUMzMoj4hRKOaJaX9YeU2rIOwQrZaEX3w8/2B1dmW5clID2hWocC555+mtXzYa2G40/Zkt74Fr8s1Wox7ou5m2u7gxZ/Olzfn6HO8Yr7f9ppPLpdOr9vZ9rBmFTMHMFTMZUzPSetpIG1recMPD0kp1gRv39l+fx0v59U26udVebdJNI4PTa8JaQAW2d2fznrUdLBp364vtdQd7peDQQ12y7S1FS3ZtLXKKfrViNxcXzUYJr4M/kXBmpqYViEQoinxmmCAaJIgm0yNbNkbpMa1YSRTMpC9WxmllCVpalXrdacgrCCcCVyjYNSjsM6uMkrM/gw2itmvN4NqcjvT9/XzVt+nC3kaHNnfX1zfq1BGdg+2D2vycMGsru41wdn3MCLpZ1PSRHfRsMQmec9G/p1R/u6+N57tWTfoV+JEyTl3b7tdICCkUn2CPRRYU+ZD0wecbsw/iT4u4ZCIb3k0L42zW1Ba2NpJu/VZ/0Gqe/uqRhQSSPciFTqPK1o6hsWuzc7vZ9fVwkdYP767o+VVat7fszYM7lfbiAVJTo6Y2wEifDwPppLAAAIZCJGT3emafTGrU2y+2dsj3TA4weJdDh23YIBYJA5AOyNPI84CWw1wsmyZ0epdapKOW9RR0b08gtTyaM1QE1QVKLKClzkeNAQHZ0BsYed43aQyUhoMGn6XNzY3tpVX3TnHX3pqbe5AtvLa87wKxQXcemy3GRovRZDE+WKgobEU+sAduKjTznRQ0czviyBvI00upHmtBQWzykYCgtMudBtiMS6oJ1E7v9u2fv3Oea/pxG44b7OGbic1ujPRtRhrMgzdmusZMb6rlm2i63kzw/HVjpOuMNHhCvbHSdRnXmzQjXa/ruBnHdH5jvrq6sHP79svcwvyi/8O3MCnlHGtsqLqAqg73L6PgkJykB5XrpqkX2cuMBZQi54Gk2yW1fChjEmVSgm+ufWTkKWNh23hBmUbqSkR5f6VYY+pzhvd4SuEOI3CxIi6UZkSp3OzcLLG+yxLrq1d+fbPG+omtsfo42QrEZ//7iS8FfoTH9UsrvHyh+N1WeL+ZlBWeYRoTuMLLl176Ci/fzJQyum6TbMFCBdPCmWJWz+tG0SgWs6ade+4K7yWshggq4uzVq6FX//v80WwDVxe3816xUqwuNfWVlVknbK9XrCj//UYz8/+xGoIZ5kfYy/xcLHNuhmUYmyZTczkATazu5+P+ROqfmljHd1k0sbo7qD25GZ88tN7k+8S5XS4pbtw+aW5H1JpY3eVCbnIHu0nVPKIT+yTjTm6FT0+U4i/wZxMXLLjqb6avAWQIP1w=
|
||||
+1
@@ -0,0 +1 @@
|
||||
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
|
||||
+1
@@ -0,0 +1 @@
|
||||
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
|
||||
-1
File diff suppressed because one or more lines are too long
-1
@@ -1 +0,0 @@
|
||||
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
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
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
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
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
|
||||
@@ -1,5 +1,5 @@
|
||||
ERROR_FOUND=0
|
||||
for file in $(find $1 -name "*.ipynb"); do
|
||||
for file in $(find $1 -name "*.ipynb" | grep -v ".ipynb_checkpoints"); do
|
||||
OUTPUT=$(cat "$file" | jupytext --from ipynb --to py:percent | codespell -)
|
||||
if [ -n "$OUTPUT" ]; then
|
||||
echo "Errors found in $file"
|
||||
@@ -10,4 +10,4 @@ done
|
||||
|
||||
if [ "$ERROR_FOUND" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -90,7 +90,7 @@ For this guide, we'll use the pre-built Python [**ReAct Agent**](https://github.
|
||||
</figure>
|
||||
|
||||
|
||||
## Lagraph Studio Web UI
|
||||
## LangGraph Studio Web UI
|
||||
|
||||
Once your application is deployed, you can test it in **LangGraph Studio**.
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ For more information, please see:
|
||||
|
||||
The LangGraph Platform Deployments view (within LangSmith SaaS and self-hosted LangSmith) is not available for Self-Hosted Lite LangGraph deployments. Self-hosted LangGraph deployments are managed externally from LangSmith (e.g. there is no UI to manage these deployments).
|
||||
|
||||
The Self-Hosted Lite deployment option is a free (up to 1 million nodes executed), limited version of LangGraph Platform that you can run locally or in a self-hosted manner.
|
||||
The Self-Hosted Lite deployment option is a free (up to 1 million nodes executed per year), limited version of LangGraph Platform that you can run locally or in a self-hosted manner.
|
||||
|
||||
With a Self-Hosted Lite deployment, you are responsible for managing the infrastructure, including setting up and maintaining required databases and Redis instances.
|
||||
|
||||
|
||||
@@ -0,0 +1,938 @@
|
||||
# Functional API
|
||||
|
||||
!!! warning "Beta"
|
||||
The Functional API is currently in **beta** and is subject to change. Please [report any issues](https://github.com/langchain-ai/langgraph/issues) or feedback to the LangGraph team.
|
||||
|
||||
## Overview
|
||||
|
||||
The **Functional API** allows you to add LangGraph's key features -- [persistence](./persistence.md), [memory](./memory.md), [human-in-the-loop](./human_in_the_loop.md), and [streaming](./streaming.md) — to your applications with minimal changes to your existing code.
|
||||
|
||||
It is designed to integrate these features into existing code that may use standard language primitives for branching and control flow, such as `if` statements, `for` loops, and function calls. Unlike many data orchestration frameworks that require restructuring code into an explicit pipeline or DAG, the Functional API allows you to incorporate these capabilities without enforcing a rigid execution model.
|
||||
|
||||
The Functional API uses two key building blocks:
|
||||
|
||||
- **`@entrypoint`** – Marks a function as the starting point of a workflow, encapsulating logic and managing execution flow, including handling long-running tasks and interrupts.
|
||||
- **`@task`** – Represents a discrete unit of work, such as an API call or data processing step, that can be executed asynchronously within an entrypoint. Tasks return a future-like object that can be awaited or resolved synchronously.
|
||||
|
||||
This provides a minimal abstraction for building workflows with state management and streaming.
|
||||
|
||||
!!! tip
|
||||
|
||||
For users who prefer a more declarative approach, LangGraph's [Graph API](./low_level.md) allows you to define workflows using a Graph paradigm. Both APIs share the same underlying runtime, so you can use them together in the same application.
|
||||
Please see the [Functional API vs. Graph API](#functional-api-vs-graph-api) section for a comparison of the two paradigms.
|
||||
|
||||
## Example
|
||||
|
||||
Below we demonstrate a simple application that writes an essay and [interrupts](human_in_the_loop.md) to request human review.
|
||||
|
||||
```python
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import interrupt
|
||||
|
||||
@task
|
||||
def write_essay(topic: str) -> str:
|
||||
"""Write an essay about the given topic."""
|
||||
time.sleep(1) # A placeholder for a long-running task.
|
||||
return f"An essay about topic: {topic}"
|
||||
|
||||
@entrypoint(checkpointer=MemorySaver())
|
||||
def workflow(topic: str) -> dict:
|
||||
"""A simple workflow that writes an essay and asks for a review."""
|
||||
essay = write_essay("cat").result()
|
||||
is_approved = interrupt({
|
||||
# Any json-serializable payload provided to interrupt as argument.
|
||||
# It will be surfaced on the client side as an Interrupt when streaming data
|
||||
# from the workflow.
|
||||
"essay": essay, # The essay we want reviewed.
|
||||
# We can add any additional information that we need.
|
||||
# For example, introduce a key called "action" with some instructions.
|
||||
"action": "Please approve/reject the essay",
|
||||
})
|
||||
|
||||
return {
|
||||
"essay": essay, # The essay that was generated
|
||||
"is_approved": is_approved, # Response from HIL
|
||||
}
|
||||
```
|
||||
|
||||
??? example "Detailed Explanation"
|
||||
|
||||
This workflow will write an essay about the topic "cat" and then pause to get a review from a human. The workflow can be interrupted for an indefinite amount of time until a review is provided.
|
||||
|
||||
When the workflow is resumed, it executes from the very start, but because the result of the `write_essay` task was already saved, the task result will be loaded from the checkpoint instead of being recomputed.
|
||||
|
||||
```python
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import interrupt
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
@task
|
||||
def write_essay(topic: str) -> str:
|
||||
"""Write an essay about the given topic."""
|
||||
time.sleep(1) # This is a placeholder for a long-running task.
|
||||
return f"An essay about topic: {topic}"
|
||||
|
||||
@entrypoint(checkpointer=MemorySaver())
|
||||
def workflow(topic: str) -> dict:
|
||||
"""A simple workflow that writes an essay and asks for a review."""
|
||||
essay = write_essay("cat").result()
|
||||
is_approved = interrupt({
|
||||
# Any json-serializable payload provided to interrupt as argument.
|
||||
# It will be surfaced on the client side as an Interrupt when streaming data
|
||||
# from the workflow.
|
||||
"essay": essay, # The essay we want reviewed.
|
||||
# We can add any additional information that we need.
|
||||
# For example, introduce a key called "action" with some instructions.
|
||||
"action": "Please approve/reject the essay",
|
||||
})
|
||||
|
||||
return {
|
||||
"essay": essay, # The essay that was generated
|
||||
"is_approved": is_approved, # Response from HIL
|
||||
}
|
||||
|
||||
thread_id = str(uuid.uuid4())
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": thread_id
|
||||
}
|
||||
}
|
||||
|
||||
for item in workflow.stream("cat", config):
|
||||
print(item)
|
||||
```
|
||||
|
||||
```pycon
|
||||
{'write_essay': 'An essay about topic: cat'}
|
||||
{'__interrupt__': (Interrupt(value={'essay': 'An essay about topic: cat', 'action': 'Please approve/reject the essay'}, resumable=True, ns=['workflow:f7b8508b-21c0-8b4c-5958-4e8de74d2684'], when='during'),)}
|
||||
```
|
||||
|
||||
An essay has been written and is ready for review. Once the review is provided, we can resume the workflow:
|
||||
|
||||
```python
|
||||
from langgraph.types import Command
|
||||
|
||||
# Get review from a user (e.g., via a UI)
|
||||
# In this case, we're using a bool, but this can be any json-serializable value.
|
||||
human_review = True
|
||||
|
||||
for item in workflow.stream(Command(resume=human_review), config):
|
||||
print(item)
|
||||
```
|
||||
|
||||
```pycon
|
||||
{'workflow': {'essay': 'An essay about topic: cat', 'is_approved': False}}
|
||||
```
|
||||
|
||||
The workflow has been completed and the review has been added to the essay.
|
||||
|
||||
## Entrypoint
|
||||
|
||||
The [`@entrypoint`][langgraph.func.entrypoint] decorator can be used to create a workflow from a function. It encapsulates workflow logic and manages execution flow, including handling *long-running tasks* and [interrupts](./low_level.md#interrupt).
|
||||
|
||||
### Definition
|
||||
|
||||
An **entrypoint** is defined by decorating a function with the `@entrypoint` decorator.
|
||||
|
||||
The function **must accept a single positional argument**, which serves as the workflow input. If you need to pass multiple pieces of data, use a dictionary as the input type for the first argument.
|
||||
|
||||
Decorating a function with an `entrypoint` produces a [`Pregel`][langgraph.pregel.Pregel.stream] instance which helps to manage the execution of the workflow (e.g., handles streaming, resumption, and checkpointing).
|
||||
|
||||
You will usually want to pass a **checkpointer** to the `@entrypoint` decorator to enable persistence and use features like **human-in-the-loop**.
|
||||
|
||||
=== "Sync"
|
||||
|
||||
```python
|
||||
from langgraph.func import entrypoint
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(some_input: dict) -> int:
|
||||
# some logic that may involve long-running tasks like API calls,
|
||||
# and may be interrupted for human-in-the-loop.
|
||||
...
|
||||
return result
|
||||
```
|
||||
|
||||
=== "Async"
|
||||
|
||||
```python
|
||||
from langgraph.func import entrypoint
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
async def my_workflow(some_input: dict) -> int:
|
||||
# some logic that may involve long-running tasks like API calls,
|
||||
# and may be interrupted for human-in-the-loop
|
||||
...
|
||||
return result
|
||||
```
|
||||
|
||||
!!! important "Serialization"
|
||||
|
||||
The **inputs** and **outputs** of entrypoints must be JSON-serializable to support checkpointing. Please see the [serialization](#serialization) section for more details.
|
||||
|
||||
|
||||
### Injectable Parameters
|
||||
|
||||
When declaring an `entrypoint`, you can request access to additional parameters that will be injected automatically at run time. These parameters include:
|
||||
|
||||
|
||||
| Parameter | Description |
|
||||
|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **previous** | Access the the state associated with the previous `checkpoint` for the given thread. See [state management](#state-management). |
|
||||
| **store** | An instance of [BaseStore][langgraph.store.base.BaseStore]. Useful for [long-term memory](#long-term-memory). |
|
||||
| **writer** | For streaming custom data, to write custom data to the `custom` stream. Useful for [streaming custom data](#streaming-custom-data). |
|
||||
| **config** | For accessing run time configuration. See [RunnableConfig](https://python.langchain.com/docs/concepts/runnables/#runnableconfig) for information. |
|
||||
|
||||
!!! important
|
||||
|
||||
Declare the parameters with the appropriate name and type annotation.
|
||||
|
||||
??? example "Requesting Injectable Parameters"
|
||||
|
||||
```python
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.func import entrypoint
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
|
||||
in_memory_store = InMemoryStore(...) # An instance of InMemoryStore for long-term memory
|
||||
|
||||
@entrypoint(
|
||||
checkpointer=checkpointer, # Specify the checkpointer
|
||||
store=in_memory_store # Specify the store
|
||||
)
|
||||
def my_workflow(
|
||||
some_input: dict, # The input (e.g., passed via `invoke`)
|
||||
*,
|
||||
previous: Any = None, # For short-term memory
|
||||
store: BaseStore, # For long-term memory
|
||||
writer: StreamWriter, # For streaming custom data
|
||||
config: RunnableConfig # For accessing the configuration passed to the entrypoint
|
||||
) -> ...:
|
||||
```
|
||||
|
||||
### Executing
|
||||
|
||||
Using the [`@entrypoint`](#entrypoint) yields a [`Pregel`][langgraph.pregel.Pregel.stream] object that can be executed using the `invoke`, `ainvoke`, `stream`, and `astream` methods.
|
||||
|
||||
=== "Invoke"
|
||||
|
||||
```python
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
my_workflow.invoke(some_input, config) # Wait for the result synchronously
|
||||
```
|
||||
|
||||
=== "Async Invoke"
|
||||
|
||||
```python
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
await my_workflow.ainvoke(some_input, config) # Await result asynchronously
|
||||
```
|
||||
|
||||
=== "Stream"
|
||||
|
||||
```python
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
for chunk in my_workflow.stream(some_input, config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
=== "Async Stream"
|
||||
|
||||
```python
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
async for chunk in my_workflow.astream(some_input, config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
### Resuming
|
||||
|
||||
Resuming an execution after an [interrupt][langgraph.types.interrupt] can be done by passing a **resume** value to the [Command][langgraph.types.Command] primitive.
|
||||
|
||||
=== "Invoke"
|
||||
|
||||
```python
|
||||
from langgraph.types import Command
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
my_workflow.invoke(Command(resume=some_resume_value), config)
|
||||
```
|
||||
|
||||
=== "Async Invoke"
|
||||
|
||||
```python
|
||||
from langgraph.types import Command
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
await my_workflow.ainvoke(Command(resume=some_resume_value), config)
|
||||
```
|
||||
|
||||
=== "Stream"
|
||||
|
||||
```python
|
||||
from langgraph.types import Command
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
for chunk in my_workflow.stream(Command(resume=some_resume_value), config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
=== "Async Stream"
|
||||
|
||||
```python
|
||||
from langgraph.types import Command
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
async for chunk in my_workflow.astream(Command(resume=some_resume_value), config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
**Resuming after an error**
|
||||
|
||||
|
||||
To resume after an error, run the `entrypoint` with a `None` and the same **thread id** (config).
|
||||
|
||||
This assumes that the underlying **error** has been resolved and execution can proceed successfully.
|
||||
|
||||
=== "Invoke"
|
||||
|
||||
```python
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
my_workflow.invoke(None, config)
|
||||
```
|
||||
|
||||
=== "Async Invoke"
|
||||
|
||||
```python
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
await my_workflow.ainvoke(None, config)
|
||||
```
|
||||
|
||||
=== "Stream"
|
||||
|
||||
```python
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
for chunk in my_workflow.stream(None, config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
=== "Async Stream"
|
||||
|
||||
```python
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
async for chunk in my_workflow.astream(None, config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
### State Management
|
||||
|
||||
When an `entrypoint` is defined with a `checkpointer`, it stores information between successive invocations on the same **thread id** in [checkpoints](persistence.md#checkpoints).
|
||||
|
||||
This allows accessing the state from the previous invocation using the `previous` parameter.
|
||||
|
||||
By default, the `previous` parameter is the return value of the previous invocation.
|
||||
|
||||
```python
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(number: int, *, previous: Any = None) -> int:
|
||||
previous = previous or 0
|
||||
return number + previous
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "some_thread_id"
|
||||
}
|
||||
}
|
||||
|
||||
my_workflow.invoke(1, config) # 1 (previous was None)
|
||||
my_workflow.invoke(2, config) # 3 (previous was 1 from the previous invocation)
|
||||
```
|
||||
|
||||
#### `entrypoint.final`
|
||||
|
||||
[entrypoint.final][langgraph.func.entrypoint.final] is a special primitive that can be returned from an entrypoint and allows **decoupling** the value that is **saved in the checkpoint** from the **return value of the entrypoint**.
|
||||
|
||||
The first value is the return value of the entrypoint, and the second value is the value that will be saved in the checkpoint. The type annotation is `entrypoint.final[return_type, save_type]`.
|
||||
|
||||
```python
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(number: int, *, previous: Any = None) -> entrypoint.final[int, int]:
|
||||
previous = previous or 0
|
||||
# This will return the previous value to the caller, saving
|
||||
# 2 * number to the checkpoint, which will be used in the next invocation
|
||||
# for the `previous` parameter.
|
||||
return entrypoint.final(value=previous, save=2 * number)
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "1"
|
||||
}
|
||||
}
|
||||
|
||||
my_workflow.invoke(3, config) # 0 (previous was None)
|
||||
my_workflow.invoke(1, config) # 6 (previous was 3 * 2 from the previous invocation)
|
||||
```
|
||||
|
||||
## Task
|
||||
|
||||
A **task** represents a discrete unit of work, such as an API call or data processing step. It has two key characteristics:
|
||||
|
||||
* **Asynchronous Execution**: Tasks are designed to be executed asynchronously, allowing multiple operations to run concurrently without blocking.
|
||||
* **Checkpointing**: Task results are saved to a checkpoint, enabling resumption of the workflow from the last saved state. (See [persistence](persistence.md) for more details).
|
||||
|
||||
### Definition
|
||||
|
||||
Tasks are defined using the `@task` decorator, which wraps a regular Python function.
|
||||
|
||||
```python
|
||||
from langgraph.func import task
|
||||
|
||||
@task()
|
||||
def slow_computation(input_value):
|
||||
# Simulate a long-running operation
|
||||
...
|
||||
return result
|
||||
```
|
||||
|
||||
!!! important "Serialization"
|
||||
|
||||
The **outputs** of tasks must be JSON-serializable to support checkpointing.
|
||||
|
||||
### Execution
|
||||
|
||||
**Tasks** can only be called from within an **entrypoint**, another **task**, or a [state graph node](./low_level.md#nodes).
|
||||
|
||||
Tasks *cannot* be called directly from the main application code.
|
||||
|
||||
When you call a **task**, it returns *immediately* with a future object. A future is a placeholder for a result that will be available later.
|
||||
|
||||
To obtain the result of a **task**, you can either wait for it synchronously (using `result()`) or await it asynchronously (using `await`).
|
||||
|
||||
|
||||
=== "Synchronous Invocation"
|
||||
|
||||
```python
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(some_input: int) -> int:
|
||||
future = slow_computation(some_input)
|
||||
return future.result() # Wait for the result synchronously
|
||||
```
|
||||
|
||||
=== "Asynchronous Invocation"
|
||||
|
||||
```python
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
async def my_workflow(some_input: int) -> int:
|
||||
return await slow_computation(some_input) # Await result asynchronously
|
||||
```
|
||||
|
||||
## When to use a task
|
||||
|
||||
**Tasks** are useful in the following scenarios:
|
||||
|
||||
- **Checkpointing**: When you need to save the result of a long-running operation to a checkpoint, so you don't need to recompute it when resuming the workflow.
|
||||
- **Human-in-the-loop**: If you're building a workflow that requires human intervention, you MUST use **tasks** to encapsulate any randomness (e.g., API calls) to ensure that the workflow can be resumed correctly. See the [determinism](#determinism) section for more details.
|
||||
- **Parallel Execution**: For I/O-bound tasks, **tasks** enable parallel execution, allowing multiple operations to run concurrently without blocking (e.g., calling multiple APIs).
|
||||
- **Observability**: Wrapping operations in **tasks** provides a way to track the progress of the workflow and monitor the execution of individual operations using [LangSmith](https://docs.smith.langchain.com/).
|
||||
- **Retryable Work**: When work needs to be retried to handle failures or inconsistencies, **tasks** provide a way to encapsulate and manage the retry logic.
|
||||
|
||||
## Serialization
|
||||
|
||||
There are two key aspects to serialization in LangGraph:
|
||||
|
||||
1. `@entrypoint` inputs and outputs must be JSON-serializable.
|
||||
2. `@task` outputs must be JSON-serializable.
|
||||
|
||||
These requirements are necessary for enabling checkpointing and workflow resumption. Use python primitives
|
||||
like dictionaries, lists, strings, numbers, and booleans to ensure that your inputs and outputs are serializable.
|
||||
|
||||
Serialization ensures that workflow state, such as task results and intermediate values, can be reliably saved and restored. This is critical for enabling human-in-the-loop interactions, fault tolerance, and parallel execution.
|
||||
|
||||
Providing non-serializable inputs or outputs will result in a runtime error when a workflow is configured with a checkpointer.
|
||||
|
||||
## Determinism
|
||||
|
||||
To utilize features like **human-in-the-loop**, any randomness should be encapsulated inside of **tasks**. This guarantees that when execution is halted (e.g., for human in the loop) and then resumed, it will follow the same *sequence of steps*, even if **task** results are non-deterministic.
|
||||
|
||||
LangGraph achieves this behavior by persisting **task** and [**subgraph**](./low_level.md#subgraphs) results as they execute. A well-designed workflow ensures that resuming execution follows the *same sequence of steps*, allowing previously computed results to be retrieved correctly without having to re-execute them. This is particularly useful for long-running **tasks** or **tasks** with non-deterministic results, as it avoids repeating previously done work and allows resuming from essentially the same
|
||||
|
||||
While different runs of a workflow can produce different results, resuming a **specific** run should always follow the same sequence of recorded steps. This allows LangGraph to efficiently look up **task** and **subgraph** results that were executed prior to the graph being interrupted and avoid recomputing them.
|
||||
|
||||
## Idempotency
|
||||
|
||||
Idempotency ensures that running the same operation multiple times produces the same result. This helps prevent duplicate API calls and redundant processing if a step is rerun due to a failure. Always place API calls inside **tasks** functions for checkpointing, and design them to be idempotent in case of re-execution. Re-execution can occur if a **task** starts, but does not complete successfully. Then, if the workflow is resumed, the **task** will run again. Use idempotency keys or verify existing results to avoid duplication.
|
||||
|
||||
## Functional API vs. Graph API
|
||||
|
||||
The **Functional API** and the [Graph APIs (StateGraph)](./low_level.md#stategraph) provide two different paradigms to create applications with LangGraph. Here are some key differences:
|
||||
|
||||
- **Control flow**: The Functional API does not require thinking about graph structure. You can use standard Python constructs to define workflows. This will usually trim the amount of code you need to write.
|
||||
- **State management**: The **GraphAPI** requires declaring a [**State**](./low_level.md#state) and may require defining [**reducers**](./low_level.md#reducers) to manage updates to the graph state. `@entrypoint` and `@tasks` do not require explicit state management as their state is scoped to the function and is not shared across functions.
|
||||
- **Checkpointing**: Both APIs generate and use checkpoints. In the **Graph API** a new checkpoint is generated after every [superstep](./low_level.md). In the **Functional API**, when tasks are executed, their results are saved to an existing checkpoint associated with the given entrypoint instead of creating a new checkpoint.
|
||||
- **Visualization**: The Graph API makes it easy to visualize the workflow as a graph which can be useful for debugging, understanding the workflow, and sharing with others. The Functional API does not support visualization as the graph is dynamically generated during runtime.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Handling side effects
|
||||
|
||||
Encapsulate side effects (e.g., writing to a file, sending an email) in tasks to ensure they are not executed multiple times when resuming a workflow.
|
||||
|
||||
=== "Incorrect"
|
||||
|
||||
In this example, a side effect (writing to a file) is directly included in the workflow, so it will be executed a second time when resuming the workflow.
|
||||
|
||||
```python
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(inputs: dict) -> int:
|
||||
# This code will be executed a second time when resuming the workflow.
|
||||
# Which is likely not what you want.
|
||||
# highlight-next-line
|
||||
with open("output.txt", "w") as f:
|
||||
# highlight-next-line
|
||||
f.write("Side effect executed")
|
||||
value = interrupt("question")
|
||||
return value
|
||||
```
|
||||
|
||||
=== "Correct"
|
||||
|
||||
In this example, the side effect is encapsulated in a task, ensuring consistent execution upon resumption.
|
||||
|
||||
```python
|
||||
from langgraph.func import task
|
||||
|
||||
# highlight-next-line
|
||||
@task
|
||||
# highlight-next-line
|
||||
def write_to_file():
|
||||
with open("output.txt", "w") as f:
|
||||
f.write("Side effect executed")
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(inputs: dict) -> int:
|
||||
# The side effect is now encapsulated in a task.
|
||||
write_to_file().result()
|
||||
value = interrupt("question")
|
||||
return value
|
||||
```
|
||||
|
||||
### Non-deterministic control flow
|
||||
|
||||
Operations that might give different results each time (like getting current time or random numbers) should be encapsulated in tasks to ensure that on resume, the same result is returned.
|
||||
|
||||
* In a task: Get random number (5) → interrupt → resume → (returns 5 again) → ...
|
||||
* Not in a task: Get random number (5) → interrupt → resume → get new random number (7) → ...
|
||||
|
||||
This is especially important when using **human-in-the-loop** workflows with multiple interrupts calls. LangGraph keeps a list
|
||||
of resume values for each task/entrypoint. When an interrupt is encountered, it's matched with the corresponding resume value.
|
||||
This matching is strictly **index-based**, so the order of the resume values should match the order of the interrupts.
|
||||
|
||||
If order of execution is not maintained when resuming, one `interrupt` call may be matched with the wrong `resume` value, leading to incorrect results.
|
||||
|
||||
Please read the section on [determinism](#determinism) for more details.
|
||||
|
||||
=== "Incorrect"
|
||||
|
||||
In this example, the workflow uses the current time to determine which task to execute. This is non-deterministic because the result of the workflow depends on the time at which it is executed.
|
||||
|
||||
```python
|
||||
from langgraph.func import entrypoint
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(inputs: dict) -> int:
|
||||
t0 = inputs["t0"]
|
||||
# highlight-next-line
|
||||
t1 = time.time()
|
||||
|
||||
delta_t = t1 - t0
|
||||
|
||||
if delta_t > 1:
|
||||
result = slow_task(1).result()
|
||||
value = interrupt("question")
|
||||
else:
|
||||
result = slow_task(2).result()
|
||||
value = interrupt("question")
|
||||
|
||||
return {
|
||||
"result": result,
|
||||
"value": value
|
||||
}
|
||||
```
|
||||
|
||||
=== "Correct"
|
||||
|
||||
In this example, the workflow uses the input `t0` to determine which task to execute. This is deterministic because the result of the workflow depends only on the input.
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
from langgraph.func import task
|
||||
|
||||
# highlight-next-line
|
||||
@task
|
||||
# highlight-next-line
|
||||
def get_time() -> float:
|
||||
return time.time()
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(inputs: dict) -> int:
|
||||
t0 = inputs["t0"]
|
||||
# highlight-next-line
|
||||
t1 = get_time().result()
|
||||
|
||||
delta_t = t1 - t0
|
||||
|
||||
if delta_t > 1:
|
||||
result = slow_task(1).result()
|
||||
value = interrupt("question")
|
||||
else:
|
||||
result = slow_task(2).result()
|
||||
value = interrupt("question")
|
||||
|
||||
return {
|
||||
"result": result,
|
||||
"value": value
|
||||
}
|
||||
```
|
||||
|
||||
## Patterns
|
||||
|
||||
Below are a few simple patterns that show examples of **how to** use the **Functional API**.
|
||||
|
||||
When defining an `entrypoint`, input is restricted to the first argument of the function. To pass multiple inputs, you can use a dictionary.
|
||||
|
||||
```python
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(inputs: dict) -> int:
|
||||
value = inputs["value"]
|
||||
another_value = inputs["another_value"]
|
||||
...
|
||||
|
||||
my_workflow.invoke({"value": 1, "another_value": 2})
|
||||
```
|
||||
|
||||
### Parallel execution
|
||||
|
||||
Tasks can be executed in parallel by invoking them concurrently and waiting for the results. This is useful for improving performance in IO bound tasks (e.g., calling APIs for LLMs).
|
||||
|
||||
```python
|
||||
@task
|
||||
def add_one(number: int) -> int:
|
||||
return number + 1
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def graph(numbers: list[int]) -> list[str]:
|
||||
futures = [add_one(i) for i in numbers]
|
||||
return [f.result() for f in futures]
|
||||
```
|
||||
|
||||
### Calling subgraphs
|
||||
|
||||
The **Functional API** and the [**Graph API**](./low_level.md) can be used together in the same application as they share the same underlying runtime.
|
||||
|
||||
```python
|
||||
from langgraph.func import entrypoint
|
||||
from langgraph.graph import StateGraph
|
||||
|
||||
builder = StateGraph()
|
||||
...
|
||||
some_graph = builder.compile()
|
||||
|
||||
@entrypoint()
|
||||
def some_workflow(some_input: dict) -> int:
|
||||
# Call a graph defined using the graph API
|
||||
result_1 = some_graph.invoke(...)
|
||||
# Call another graph defined using the graph API
|
||||
result_2 = another_graph.invoke(...)
|
||||
return {
|
||||
"result_1": result_1,
|
||||
"result_2": result_2
|
||||
}
|
||||
```
|
||||
|
||||
### Calling other entrypoints
|
||||
|
||||
You can call other **entrypoints** from within an **entrypoint** or a **task**.
|
||||
|
||||
```python
|
||||
@entrypoint() # Will automatically use the checkpointer from the parent entrypoint
|
||||
def some_other_workflow(inputs: dict) -> int:
|
||||
return inputs["value"]
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def my_workflow(inputs: dict) -> int:
|
||||
value = some_other_workflow.invoke({"value": 1})
|
||||
return value
|
||||
```
|
||||
|
||||
### Streaming custom data
|
||||
|
||||
You can stream custom data from an **entrypoint** by using the `StreamWriter` type. This allows you to write custom data to the `custom` stream.
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import StreamWriter
|
||||
|
||||
@task
|
||||
def add_one(x):
|
||||
return x + 1
|
||||
|
||||
@task
|
||||
def add_two(x):
|
||||
return x + 2
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def main(inputs, writer: StreamWriter) -> int:
|
||||
"""A simple workflow that adds one and two to a number."""
|
||||
writer("hello") # Write some data to the `custom` stream
|
||||
add_one(inputs['number']).result() # Will write data to the `updates` stream
|
||||
writer("world") # Write some more data to the `custom` stream
|
||||
add_two(inputs['number']).result() # Will write data to the `updates` stream
|
||||
return 5
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "1"
|
||||
}
|
||||
}
|
||||
|
||||
for chunk in main.stream({"number": 1}, stream_mode=["custom", "updates"], config=config):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
```pycon
|
||||
('updates', {'add_one': 2})
|
||||
('updates', {'add_two': 3})
|
||||
('custom', 'hello')
|
||||
('custom', 'world')
|
||||
('updates', {'main': 5})
|
||||
```
|
||||
|
||||
!!! important
|
||||
|
||||
The `writer` parameter is automatically injected at run time. It will only be injected if the
|
||||
parameter name appears in the function signature with that *exact* name.
|
||||
|
||||
|
||||
### Retry policy
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import RetryPolicy
|
||||
|
||||
attempts = 0
|
||||
|
||||
# Let's configure the RetryPolicy to retry on ValueError.
|
||||
# The default RetryPolicy is optimized for retrying specific network errors.
|
||||
retry_policy = RetryPolicy(retry_on=ValueError)
|
||||
|
||||
@task(retry=retry_policy)
|
||||
def get_info():
|
||||
global attempts
|
||||
attempts += 1
|
||||
|
||||
if attempts < 2:
|
||||
raise ValueError('Failure')
|
||||
return "OK"
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def main(inputs, writer):
|
||||
return get_info().result()
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "1"
|
||||
}
|
||||
}
|
||||
|
||||
main.invoke({'any_input': 'foobar'}, config=config)
|
||||
```
|
||||
|
||||
```pycon
|
||||
'OK'
|
||||
```
|
||||
|
||||
### Resuming after an error
|
||||
|
||||
```python
|
||||
import time
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import StreamWriter
|
||||
|
||||
# Global variable to track the number of attempts
|
||||
attempts = 0
|
||||
|
||||
@task()
|
||||
def get_info():
|
||||
"""
|
||||
Simulates a task that fails once before succeeding.
|
||||
Raises an exception on the first attempt, then returns "OK" on subsequent tries.
|
||||
"""
|
||||
global attempts
|
||||
attempts += 1
|
||||
|
||||
if attempts < 2:
|
||||
raise ValueError("Failure") # Simulate a failure on the first attempt
|
||||
return "OK"
|
||||
|
||||
# Initialize an in-memory checkpointer for persistence
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
@task
|
||||
def slow_task():
|
||||
"""
|
||||
Simulates a slow-running task by introducing a 1-second delay.
|
||||
"""
|
||||
time.sleep(1)
|
||||
return "Ran slow task."
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def main(inputs, writer: StreamWriter):
|
||||
"""
|
||||
Main workflow function that runs the slow_task and get_info tasks sequentially.
|
||||
|
||||
Parameters:
|
||||
- inputs: Dictionary containing workflow input values.
|
||||
- writer: StreamWriter for streaming custom data.
|
||||
|
||||
The workflow first executes `slow_task` and then attempts to execute `get_info`,
|
||||
which will fail on the first invocation.
|
||||
"""
|
||||
slow_task_result = slow_task().result() # Blocking call to slow_task
|
||||
get_info().result() # Exception will be raised here on the first attempt
|
||||
return slow_task_result
|
||||
|
||||
# Workflow execution configuration with a unique thread identifier
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": "1" # Unique identifier to track workflow execution
|
||||
}
|
||||
}
|
||||
|
||||
# This invocation will take ~1 second due to the slow_task execution
|
||||
try:
|
||||
# First invocation will raise an exception due to the `get_info` task failing
|
||||
main.invoke({'any_input': 'foobar'}, config=config)
|
||||
except ValueError:
|
||||
pass # Handle the failure gracefully
|
||||
```
|
||||
|
||||
When we resume execution, we won't need to re-run the `slow_task` as its result is already saved in the checkpoint.
|
||||
|
||||
```python
|
||||
main.invoke(None, config=config)
|
||||
```
|
||||
|
||||
```pycon
|
||||
'Ran slow task.'
|
||||
```
|
||||
|
||||
### Human-in-the-loop
|
||||
|
||||
The functional API supports [human-in-the-loop](human_in_the_loop.md) workflows using the `interrupt` function and the `Command` primitive.
|
||||
|
||||
Please see the following examples for more details:
|
||||
|
||||
* [How to wait for user input (Functional API)](../how-tos/wait-user-input-functional.ipynb): Shows how to implement a simple human-in-the-loop workflow using the functional API.
|
||||
* [How to review tool calls (Functional API)](../how-tos/review-tool-calls-functional.ipynb): Guide demonstrates how to implement human-in-the-loop workflows in a ReAct agent using the LangGraph Functional API.
|
||||
|
||||
### Short-term memory
|
||||
|
||||
[State management](#state-management) using the **previous** parameter and optionally using the `entrypoint.final` primitive can be used to implement [short term memory](memory.md).
|
||||
|
||||
Please see the following how-to guides for more details:
|
||||
|
||||
* [How to add thread-level persistence (functional API)](../how-tos/persistence-functional.ipynb): Shows how to add thread-level persistence to a functional API workflow and implements a simple chatbot.
|
||||
|
||||
### Long-term memory
|
||||
|
||||
[long-term memory](memory.md#long-term-memory) allows storing information across different **thread ids**. This could be useful for learning information
|
||||
about a given user in one conversation and using it in another.
|
||||
|
||||
Please see the following how-to guides for more details:
|
||||
|
||||
* [How to add cross-thread persistence (functional API)](../how-tos/cross-thread-persistence-functional.ipynb): Shows how to add cross-thread persistence to a functional API workflow and implements a simple chatbot.
|
||||
|
||||
### Workflows
|
||||
|
||||
* [Workflows and agent](../tutorials/workflows/index.md) guide for more examples of how to build workflows using the Functional API.
|
||||
|
||||
### Agents
|
||||
|
||||
* [How to create a React agent from scratch (Functional API)](../how-tos/react-agent-from-scratch-functional.ipynb): Shows how to create a simple React agent from scratch using the functional API.
|
||||
* [How to build a multi-agent network](../how-tos/multi-agent-network-functional.ipynb): Shows how to build a multi-agent network using the functional API.
|
||||
* [How to add multi-turn conversation in a multi-agent application (functional API)](../how-tos/multi-agent-multi-turn-convo-functional.ipynb): allow an end-user to engage in a multi-turn conversation with one or more agents.
|
||||
|
||||
@@ -1,58 +1,26 @@
|
||||
# Why LangGraph?
|
||||
|
||||
LLMs are extremely powerful, particularly when connected to other systems such as a retriever or APIs. This is why many LLM applications use a control flow of steps before and / or after LLM calls. As an example [RAG](https://github.com/langchain-ai/rag-from-scratch) performs retrieval of relevant documents to a question, and passes those documents to an LLM in order to ground the response. Often a control flow of steps before and / or after an LLM is called a "chain." Chains are a popular paradigm for programming with LLMs and offer a high degree of reliability; the same set of steps runs with each chain invocation.
|
||||
## LLM applications
|
||||
|
||||
However, we often want LLM systems that can pick their own control flow! This is one definition of an [agent](https://blog.langchain.dev/what-is-an-agent/): an agent is a system that uses an LLM to decide the control flow of an application. Unlike a chain, an agent gives an LLM some degree of control over the sequence of steps in the application. Examples of using an LLM to decide the control of an application:
|
||||
LLMs make it possible to embed intelligence into a new class of applications. There are many patterns for building applications that use LLMs. [Workflows](https://www.anthropic.com/research/building-effective-agents) have scaffolding of predefined code paths around LLM calls. LLMs can direct the control flow through these predefined code paths, which some consider to be an "[agentic system](https://www.anthropic.com/research/building-effective-agents)". In other cases, it's possible to remove this scaffolding, creating autonomous agents that can [plan](https://huyenchip.com/2025/01/07/agents.html), take actions via [tool calls](https://python.langchain.com/docs/concepts/tool_calling/), and directly respond [to the feedback from their own actions](https://research.google/blog/react-synergizing-reasoning-and-acting-in-language-models/) with further actions.
|
||||
|
||||
- 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
|
||||

|
||||
|
||||
There are many different types of [agent architectures](https://blog.langchain.dev/what-is-a-cognitive-architecture/) to consider, which give an LLM varying levels of control. On one extreme, a router allows an LLM to select a single step from a specified set of options and, on the other extreme, a fully autonomous long-running agent may have complete freedom to select any sequence of steps that it wants for a given problem.
|
||||
## What LangGraph provides
|
||||
|
||||

|
||||
LangGraph provides low-level supporting infrastructure that sits underneath *any* workflow or agent. It does not abstract prompts or architecture, and provides three central benefits:
|
||||
|
||||
Several concepts are utilized in many agent architectures:
|
||||
### Persistence
|
||||
|
||||
- [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.
|
||||
LangGraph has a [persistence layer](https://langchain-ai.github.io/langgraph/concepts/persistence/), which offers a number of benefits:
|
||||
|
||||
## Challenges
|
||||
- [Memory](https://langchain-ai.github.io/langgraph/concepts/memory/): LangGraph persists arbitrary aspects of your application's state, supporting memory of conversations and other updates within and across user interactions;
|
||||
- [Human-in-the-loop](https://langchain-ai.github.io/langgraph/concepts/human_in_the_loop/): Because state is checkpointed, execution can be interrupted and resumed, allowing for decisions, validation, and corrections via human input.
|
||||
|
||||
In practice, there is often a trade-off between control and reliability. As we give LLMs more control, the application often become less reliable. This can be due to factors such as LLM non-determinism and / or errors in selecting tools (or steps) that the agent uses (takes).
|
||||
### Streaming
|
||||
|
||||

|
||||
LangGraph also provides support for [streaming](../how-tos/index.md#streaming) workflow / agent state to the user (or developer) over the course of execution. LangGraph supports streaming of both events ([such as feedback from a tool call](../how-tos/streaming.ipynb#updates)) and [tokens from LLM calls](../how-tos/streaming-tokens.ipynb) embedded in an application.
|
||||
|
||||
## Core Principles
|
||||
### Debugging and Deployment
|
||||
|
||||
The motivation of LangGraph is to help bend the curve, preserving higher reliability as we give the agent more control over the application. We'll outline a few specific pillars of LangGraph that make it well suited for building reliable agents.
|
||||
|
||||

|
||||
|
||||
**Controllability**
|
||||
|
||||
LangGraph gives the developer a high degree of [control](../how-tos/index.md#controllability) by expressing the flow of the application as a set of nodes and edges. All nodes can access and modify a common state (memory). The control flow of the application can set using edges that connect nodes, either deterministically or via conditional logic.
|
||||
|
||||
**Persistence**
|
||||
|
||||
LangGraph gives the developer many options for [persisting](../how-tos/index.md#persistence) graph state using short-term or long-term (e.g., via a database) memory.
|
||||
|
||||
**Human-in-the-Loop**
|
||||
|
||||
The persistence layer enables several different [human-in-the-loop](../how-tos/index.md#human-in-the-loop) interaction patterns with agents; for example, it's possible to pause an agent, review its state, edit it state, and approve a follow-up step.
|
||||
|
||||
**Streaming**
|
||||
|
||||
LangGraph comes with first class support for [streaming](../how-tos/index.md#streaming), which can expose state to the user (or developer) over the course of agent execution. 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).
|
||||
|
||||
## Debugging
|
||||
|
||||
Once you've built a graph, you often want to test and debug it. [LangGraph Studio](https://github.com/langchain-ai/langgraph-studio?tab=readme-ov-file) is a specialized IDE for visualization and debugging of LangGraph applications.
|
||||
|
||||

|
||||
|
||||
## Deployment
|
||||
|
||||
Once you have confidence in your LangGraph application, many developers want an easy path to deployment. [LangGraph Platform](../concepts/index.md#langgraph-platform) offers a range of options for deploying LangGraph graphs.
|
||||
LangGraph provides an easy onramp for testing, debugging, and deploying applications via [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/). This includes [Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/), an IDE that enables visualization, interaction, and debugging of workflows or agents. This also includes numerous [options](https://langchain-ai.github.io/langgraph/tutorials/deployment/) for deployment.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 646 KiB |
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,7 @@ The conceptual guide does not cover step-by-step instructions or specific implem
|
||||
- [Persistence](persistence.md): LangGraph has a built-in persistence layer, implemented through checkpointers. This persistence layer helps to support powerful capabilities like human-in-the-loop, memory, time travel, and fault-tolerance.
|
||||
- [Memory](memory.md): Memory in AI applications refers to the ability to process, store, and effectively recall information from past interactions. With memory, your agents can learn from feedback and adapt to users' preferences.
|
||||
- [Streaming](streaming.md): Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
|
||||
- [Functional API (beta)](functional_api.md): An alternative to [Graph API (StateGraph)](low_level.md#stategraph) for development in LangGraph.
|
||||
- [FAQ](faq.md): Frequently asked questions about LangGraph.
|
||||
|
||||
## LangGraph Platform
|
||||
@@ -71,7 +72,7 @@ The LangGraph Platform comprises several components that work together to suppor
|
||||
### Deployment Options
|
||||
|
||||
|
||||
- [Self-Hosted Lite](./self_hosted.md): A free (up to 1 million nodes executed), limited version of LangGraph Platform that you can run locally or in a self-hosted manner
|
||||
- [Self-Hosted Lite](./self_hosted.md): A free (up to 1 million nodes executed per year), limited version of LangGraph Platform that you can run locally or in a self-hosted manner
|
||||
- [Cloud SaaS](./langgraph_cloud.md): Hosted as part of LangSmith.
|
||||
- [Bring Your Own Cloud](./bring_your_own_cloud.md): We manage the infrastructure, so you don't have to, but the infrastructure all runs within your cloud.
|
||||
- [Self-Hosted Enterprise](./self_hosted.md): Completely managed by you.
|
||||
@@ -21,6 +21,12 @@ Resource Allocation:
|
||||
|
||||
See the [how-to guide](../cloud/deployment/cloud.md#create-new-deployment) for creating a new deployment.
|
||||
|
||||
## Revision
|
||||
|
||||
A revision is an iteration of a [deployment](#deployment). When a new deployment is created, an initial revision is automatically created. To deploy new code changes or update environment variable configurations for a deployment, a new revision must be created. When a revision is created, a new container image is built automatically.
|
||||
|
||||
See the [how-to guide](../cloud/deployment/cloud.md#create-new-revision) for creating a new revision.
|
||||
|
||||
## Persistence
|
||||
|
||||
A dedicated database is automatically created for each deployment. The database serves as the [persistence layer](../concepts/persistence.md) for the deployment.
|
||||
@@ -41,12 +47,6 @@ Scale down actions are delayed for 30 minutes before any action is taken. In oth
|
||||
|
||||
In the future, the autoscaling implementation may evolve to accommodate other metrics such as background run queue size.
|
||||
|
||||
## Revision
|
||||
|
||||
A revision is an iteration of a [deployment](#deployment). When a new deployment is created, an initial revision is automatically created. To deploy new code changes or update environment variable configurations for a deployment, a new revision must be created. When a revision is created, a new container image is built automatically.
|
||||
|
||||
See the [how-to guide](../cloud/deployment/cloud.md#create-new-revision) for creating a new revision.
|
||||
|
||||
## Asynchronous Deployment
|
||||
|
||||
Infrastructure for [deployments](#deployment) and [revisions](#revision) are provisioned and deployed asynchronously. They are not deployed immediately after submission. Currently, deployment can take up to several minutes.
|
||||
@@ -55,12 +55,26 @@ Infrastructure for [deployments](#deployment) and [revisions](#revision) are pro
|
||||
- When a subsequent revision is created for a deployment, there is no database creation step. The deployment time for a subsequent revision is significantly faster compared to the deployment time of the initial revision.
|
||||
- The deployment process for each revision contains a build step, which can take up to a few minutes.
|
||||
|
||||
!!! info "Database creation for `Development` type deployments takes longer than database creation for `Production` type deployments."
|
||||
## LangSmith Integration
|
||||
|
||||
A [LangSmith](https://docs.smith.langchain.com/) tracing project is automatically created for each deployemnt. The tracing project has the same name as the deployment. When creating a deployment, the `LANGCHAIN_TRACING_V2` and `LANGCHAIN_API_KEY` environment variables do not need to be specified; they are set internally, automatically. Traces are created for each run and are emitted to the tracing project automatically.
|
||||
|
||||
When a deployment is deleted, the traces and the tracing project are not deleted.
|
||||
|
||||
## Automatic Deletion
|
||||
|
||||
Deployments are automatically deleted after 28 consecutive days of non-use (it is in an unused state). A deployment is in an unused state if there are no traces emitted to LangSmith from the deployment after 28 consecutive days. On any given day, if a deployment emits a trace to LangSmith, the counter for consecutive days of non-use is reset.
|
||||
|
||||
- An email notification is sent after 7 consecutive days of non-use.
|
||||
- A deployment is deleted after 28 consecutive days of non-use.
|
||||
|
||||
!!! danger "Data Cannot Be Recovered"
|
||||
After a deployment is deleted, the data (i.e. [persistence](#persistence)) from the deployment cannot be recovered.
|
||||
|
||||
## Architecture
|
||||
|
||||
!!! warning "Subject to Change"
|
||||
The Cloud SaaS deployment architecture may change in the future.
|
||||
The Cloud SaaS deployment architecture may change in the future.
|
||||
|
||||
A high-level diagram of a Cloud SaaS deployment.
|
||||
|
||||
|
||||
@@ -22,10 +22,6 @@ A super-step can be considered a single iteration over the graph nodes. Nodes th
|
||||
|
||||
The `StateGraph` class is the main graph class to use. 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?
|
||||
@@ -359,6 +355,29 @@ Use `Command` when you need to **both** update the graph state **and** route to
|
||||
|
||||
Use [conditional edges](#conditional-edges) to route between nodes conditionally without updating the state.
|
||||
|
||||
### Navigating to a node in a parent graph
|
||||
|
||||
If you are using [subgraphs](#subgraphs), you might want to navigate from a node a subgraph to a different subgraph (i.e. a different node in the parent graph). To do so, you can specify `graph=Command.PARENT` in `Command`:
|
||||
|
||||
```python
|
||||
def my_node(state: State) -> Command[Literal["my_other_node"]]:
|
||||
return Command(
|
||||
update={"foo": "bar"},
|
||||
goto="other_subgraph", # where `other_subgraph` is a node in the parent graph
|
||||
graph=Command.PARENT
|
||||
)
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
Setting `graph` to `Command.PARENT` will navigate to the closest parent graph.
|
||||
|
||||
!!! important "State updates with `Command.PARENT`"
|
||||
|
||||
When you send updates from a subgraph node to a parent graph node for a key that's shared by both parent and subgraph [state schemas](#schema), you **must** define a [reducer](#reducers) for the key you're updating in the parent graph state. See this [example](../how-tos/command.ipynb#navigating-to-a-node-in-a-parent-graph).
|
||||
|
||||
This is particularly useful when implementing [multi-agent handoffs](./multi_agent.md#handoffs).
|
||||
|
||||
### Using inside tools
|
||||
|
||||
A common use case is updating graph state from inside a tool. For example, in a customer support application you might want to look up customer information based on their account number or ID in the beginning of the conversation. To update the graph state from the tool, you can return `Command(update={"my_custom_key": "foo", "messages": [...]})` from the tool:
|
||||
|
||||
@@ -241,7 +241,7 @@ To address this, you can design your system _hierarchically_. For example, you c
|
||||
from typing import Literal
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.graph import StateGraph, MessagesState, START, END
|
||||
|
||||
from langgraph.types import Command
|
||||
model = ChatOpenAI()
|
||||
|
||||
# define team 1 (same as the single supervisor example above)
|
||||
@@ -286,7 +286,7 @@ team_2_graph = team_2_builder.compile()
|
||||
# define top-level supervisor
|
||||
|
||||
builder = StateGraph(MessagesState)
|
||||
def top_level_supervisor(state: MessagesState):
|
||||
def top_level_supervisor(state: MessagesState) -> Command[Literal["team_1_graph", "team_2_graph", END]]:
|
||||
# you can pass relevant parts of the state to the LLM (e.g., state["messages"])
|
||||
# to determine which team to call next. a common pattern is to call the model
|
||||
# with a structured output (e.g. force it to return an output with a "next_team" field)
|
||||
@@ -297,10 +297,11 @@ def top_level_supervisor(state: MessagesState):
|
||||
|
||||
builder = StateGraph(MessagesState)
|
||||
builder.add_node(top_level_supervisor)
|
||||
builder.add_node(team_1_graph)
|
||||
builder.add_node(team_2_graph)
|
||||
|
||||
builder.add_node("team_1_graph", team_1_graph)
|
||||
builder.add_node("team_2_graph", team_2_graph)
|
||||
builder.add_edge(START, "top_level_supervisor")
|
||||
builder.add_edge("team_1_graph", "top_level_supervisor")
|
||||
builder.add_edge("team_2_graph", "top_level_supervisor")
|
||||
graph = builder.compile()
|
||||
```
|
||||
|
||||
|
||||
@@ -433,7 +433,7 @@ See the [deployment guide](../cloud/deployment/semantic_search.md) for more deta
|
||||
|
||||
Under the hood, checkpointing is powered by checkpointer objects that conform to [BaseCheckpointSaver][langgraph.checkpoint.base.BaseCheckpointSaver] interface. LangGraph provides several checkpointer implementations, all implemented via standalone, installable libraries:
|
||||
|
||||
* `langgraph-checkpoint`: The base interface for checkpointer savers ([BaseCheckpointSaver][langgraph.checkpoint.base.BaseCheckpointSaver]) and serialization/deserialization interface ([SerializerProtocol][langgraph.checkpoint.serde.base.SerializerProtocol]). Includes in-memory checkpointer implementation ([MemorySaver][langgraph.checkpoint.memory.MemorySaver]) for experimentation. LangGraph comes with `langgraph-checkpoint` included.
|
||||
* `langgraph-checkpoint`: The base interface for checkpointer savers ([BaseCheckpointSaver][langgraph.checkpoint.base.BaseCheckpointSaver]) and serialization/deserialization interface ([SerializerProtocol][langgraph.checkpoint.serde.base.SerializerProtocol]). Includes in-memory checkpointer implementation ([InMemorySaver][langgraph.checkpoint.memory.InMemorySaver]) for experimentation. LangGraph comes with `langgraph-checkpoint` included.
|
||||
* `langgraph-checkpoint-sqlite`: An implementation of LangGraph checkpointer that uses SQLite database ([SqliteSaver][langgraph.checkpoint.sqlite.SqliteSaver] / [AsyncSqliteSaver][langgraph.checkpoint.sqlite.aio.AsyncSqliteSaver]). Ideal for experimentation and local workflows. Needs to be installed separately.
|
||||
* `langgraph-checkpoint-postgres`: An advanced checkpointer that uses Postgres database ([PostgresSaver][langgraph.checkpoint.postgres.PostgresSaver] / [AsyncPostgresSaver][langgraph.checkpoint.postgres.aio.AsyncPostgresSaver]), used in LangGraph Cloud. Ideal for using in production. Needs to be installed separately.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ There are three different plans for using it.
|
||||
| Real-time streaming of outputs and intermediate steps | ✅ | ✅ | ✅ |
|
||||
| Assistants API (configurable templates for LangGraph apps) | ✅ | ✅ | ✅ |
|
||||
| Cron scheduling | -- | ✅ | ✅ |
|
||||
| LangGraph Studio for prototyping | Desktop only | Coming Soon! | Coming Soon! |
|
||||
| LangGraph Studio for prototyping | ✅ | ✅ | ✅ |
|
||||
| Authentication & authorization to call the LangGraph APIs | -- | Coming Soon! | Coming Soon! |
|
||||
| Smart caching to reduce traffic to LLM API | -- | Coming Soon! | Coming Soon! |
|
||||
| Publish/subscribe API for state | -- | Coming Soon! | Coming Soon! |
|
||||
|
||||
@@ -11,7 +11,7 @@ There are two versions of the self-hosted deployment: [Self-Hosted Enterprise](.
|
||||
|
||||
### Self-Hosted Lite
|
||||
|
||||
The Self-Hosted Lite version is a limited version of LangGraph Platform that you can run locally or in a self-hosted manner (up to 1 million nodes executed).
|
||||
The Self-Hosted Lite version is a limited version of LangGraph Platform that you can run locally or in a self-hosted manner (up to 1 million nodes executed per year).
|
||||
|
||||
When using the Self-Hosted Lite version, you authenticate with a [LangSmith](https://smith.langchain.com/) API key.
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ LangGraph is built with first class support for streaming. There are several dif
|
||||
`.stream` and `.astream` are sync and async methods for streaming back outputs from a graph run.
|
||||
There are several different modes you can specify when calling these methods (e.g. `graph.stream(..., mode="...")):
|
||||
|
||||
- [`"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.
|
||||
- [`"custom"`](../how-tos/streaming-content.ipynb): This streams custom data from inside your graph nodes.
|
||||
- [`"values"`](../how-tos/streaming.ipynb#values): This streams the full value of the state after each step of the graph.
|
||||
- [`"updates"`](../how-tos/streaming.ipynb#updates): 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.
|
||||
- [`"custom"`](../how-tos/streaming.ipynb#custom): This streams custom data from inside your graph nodes.
|
||||
- [`"messages"`](../how-tos/streaming-tokens.ipynb): This streams LLM tokens and metadata for the graph node where LLM is invoked.
|
||||
- `"debug"`: This streams as much information as possible throughout the execution of the graph.
|
||||
- [`"debug"`](../how-tos/streaming.ipynb#debug): This streams as much information as possible throughout the execution of the graph.
|
||||
|
||||
You can also specify multiple streaming modes at the same time by passing them as a list. When you do this, the streamed outputs will be tuples `(stream_mode, data)`. For example:
|
||||
|
||||
@@ -33,7 +33,7 @@ The below visualization shows the difference between the `values` and `updates`
|
||||
|
||||
## Streaming LLM tokens and events (`.astream_events`)
|
||||
|
||||
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).
|
||||
In addition, you can use the `astream_events` method to stream back events that happen _inside_ nodes. This is useful for [streaming tokens of LLM calls](../how-tos/streaming-tokens.ipynb).
|
||||
|
||||
This is a standard method on all [LangChain objects](https://python.langchain.com/docs/concepts/#runnable-interface). This means that as the graph is executed, certain events are emitted along the way and can be seen if you run the graph using `.astream_events`.
|
||||
|
||||
@@ -145,7 +145,7 @@ guide for that [here](../how-tos/streaming-tokens.ipynb).
|
||||
|
||||
|
||||
!!! warning "ASYNC IN PYTHON<=3.10"
|
||||
You may fail to see events being emitted from inside a node when using `.astream_events` in Python <= 3.10. If you're using a Langchain RunnableLambda, a RunnableGenerator, or Tool asynchronously inside your node, you will have to propagate callbacks to these objects manually. This is because LangChain cannot automatically propagate callbacks to child objects in this case. Please see examples [here](../how-tos/streaming-content.ipynb) and [here](../how-tos/streaming-events-from-within-tools.ipynb).
|
||||
You may fail to see events being emitted from inside a node when using `.astream_events` in Python <= 3.10. If you're using a Langchain RunnableLambda, a RunnableGenerator, or Tool asynchronously inside your node, you will have to propagate callbacks to these objects manually. This is because LangChain cannot automatically propagate callbacks to child objects in this case.
|
||||
|
||||
|
||||
## LangGraph Platform
|
||||
|
||||
@@ -83,18 +83,10 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": null,
|
||||
"id": "b4864843-00a1-4c88-9a7c-c34e6c31c548",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"ANTHROPIC_API_KEY: ········\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import getpass\n",
|
||||
"import os\n",
|
||||
@@ -828,13 +820,13 @@
|
||||
"addition_expert = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" [add, make_handoff_tool(agent_name=\"multiplication_expert\")],\n",
|
||||
" state_modifier=\"You are an addition expert, you can ask the multiplication expert for help with multiplication.\",\n",
|
||||
" prompt=\"You are an addition expert, you can ask the multiplication expert for help with multiplication.\",\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"multiplication_expert = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" [multiply, make_handoff_tool(agent_name=\"addition_expert\")],\n",
|
||||
" state_modifier=\"You are a multiplication expert, you can ask an addition expert for help with addition.\",\n",
|
||||
" prompt=\"You are a multiplication expert, you can ask an addition expert for help with addition.\",\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"builder = StateGraph(MessagesState)\n",
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "100c0c81-6a9f-4ba1-b1a8-42aae82b7172",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to integrate LangGraph (functional API) with AutoGen, CrewAI, and other frameworks\n",
|
||||
"\n",
|
||||
"LangGraph is a framework for building agentic and multi-agent applications. LangGraph can be easily integrated with other agent frameworks. \n",
|
||||
"\n",
|
||||
"The primary reasons you might want to integrate LangGraph with other agent frameworks:\n",
|
||||
"\n",
|
||||
"- create [multi-agent systems](../../concepts/multi_agent) where individual agents are built with different frameworks\n",
|
||||
"- leverage LangGraph to add features like [persistence](../../concepts/persistence), [streaming](../../concepts/streaming), [short and long-term memory](../../concepts/memory) and more\n",
|
||||
"\n",
|
||||
"The simplest way to integrate agents from other frameworks is by calling those agents inside a LangGraph [node](../../concepts/low_level/#nodes):\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"import autogen\n",
|
||||
"from langgraph.func import entrypoint, task\n",
|
||||
"\n",
|
||||
"autogen_agent = autogen.AssistantAgent(name=\"assistant\", ...)\n",
|
||||
"user_proxy = autogen.UserProxyAgent(name=\"user_proxy\", ...)\n",
|
||||
"\n",
|
||||
"@task\n",
|
||||
"def call_autogen_agent(messages):\n",
|
||||
" response = user_proxy.initiate_chat(\n",
|
||||
" autogen_agent,\n",
|
||||
" message=messages[-1],\n",
|
||||
" ...\n",
|
||||
" )\n",
|
||||
" ...\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@entrypoint()\n",
|
||||
"def workflow(messages):\n",
|
||||
" response = call_autogen_agent(messages).result()\n",
|
||||
" return response\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"workflow.invoke(\n",
|
||||
" [\n",
|
||||
" {\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": \"Find numbers between 10 and 30 in fibonacci sequence\",\n",
|
||||
" }\n",
|
||||
" ]\n",
|
||||
")\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"In this guide we show how to build a LangGraph chatbot that integrates with AutoGen, but you can follow the same approach with other frameworks."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "b189ceb2-132b-4c7b-81b4-c7b8b062f833",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "62417d3a-94f9-4a52-9962-12639d714966",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%pip install autogen langgraph"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "d46da41d-0a71-4654-aec8-9e6ad8765236",
|
||||
"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": "1926bbc3-6b06-41e0-9604-860a2bbf8fa3",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Define AutoGen agent\n",
|
||||
"\n",
|
||||
"Here we define our AutoGen agent. Adapted from official tutorial [here](https://github.com/microsoft/autogen/blob/0.2/notebook/agentchat_web_info.ipynb)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "524de117-ff09-4b26-bfe8-a9f85a46ffd5",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import autogen\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"config_list = [{\"model\": \"gpt-4o\", \"api_key\": os.environ[\"OPENAI_API_KEY\"]}]\n",
|
||||
"\n",
|
||||
"llm_config = {\n",
|
||||
" \"timeout\": 600,\n",
|
||||
" \"cache_seed\": 42,\n",
|
||||
" \"config_list\": config_list,\n",
|
||||
" \"temperature\": 0,\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"autogen_agent = autogen.AssistantAgent(\n",
|
||||
" name=\"assistant\",\n",
|
||||
" llm_config=llm_config,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"user_proxy = autogen.UserProxyAgent(\n",
|
||||
" name=\"user_proxy\",\n",
|
||||
" human_input_mode=\"NEVER\",\n",
|
||||
" max_consecutive_auto_reply=10,\n",
|
||||
" is_termination_msg=lambda x: x.get(\"content\", \"\").rstrip().endswith(\"TERMINATE\"),\n",
|
||||
" code_execution_config={\n",
|
||||
" \"work_dir\": \"web\",\n",
|
||||
" \"use_docker\": False,\n",
|
||||
" }, # Please set use_docker=True if docker is available to run the generated code. Using docker is safer than running the generated code directly.\n",
|
||||
" llm_config=llm_config,\n",
|
||||
" system_message=\"Reply TERMINATE if the task has been solved at full satisfaction. Otherwise, reply CONTINUE, or the reason why the task is not solved yet.\",\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "8aa858e2-4acb-4f75-be20-b9ccbbcb5073",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "dcc478f5-4a35-43f8-bf59-9cb71289cd00",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Create the workflow\n",
|
||||
"\n",
|
||||
"We will now create a LangGraph chatbot graph that calls AutoGen agent."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "d129e4e1-3766-429a-b806-cde3d8bc0469",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import Literal, TypedDict\n",
|
||||
"\n",
|
||||
"from langchain_core.messages import convert_to_openai_messages, BaseMessage\n",
|
||||
"from langgraph.func import entrypoint, task\n",
|
||||
"from langgraph.graph import add_messages\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@task\n",
|
||||
"def call_autogen_agent(messages: list[BaseMessage]):\n",
|
||||
" # convert to openai-style messages\n",
|
||||
" messages = convert_to_openai_messages(messages)\n",
|
||||
" response = user_proxy.initiate_chat(\n",
|
||||
" autogen_agent,\n",
|
||||
" message=messages[-1],\n",
|
||||
" # pass previous message history as context\n",
|
||||
" carryover=messages[:-1],\n",
|
||||
" )\n",
|
||||
" # get the final response from the agent\n",
|
||||
" content = response.chat_history[-1][\"content\"]\n",
|
||||
" return {\"role\": \"assistant\", \"content\": content}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# add short-term memory for storing conversation history\n",
|
||||
"checkpointer = MemorySaver()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@entrypoint(checkpointer=checkpointer)\n",
|
||||
"def workflow(messages: list[BaseMessage], previous: list[BaseMessage]):\n",
|
||||
" messages = add_messages(previous or [], messages)\n",
|
||||
" response = call_autogen_agent(messages).result()\n",
|
||||
" return entrypoint.final(value=response, save=add_messages(messages, response))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "23d629c3-1d6b-40af-adf6-915e15657566",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Run the graph\n",
|
||||
"\n",
|
||||
"We can now run the graph."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"id": "a279b667-0f5d-4008-8d43-c806a3f379c4",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
|
||||
"\n",
|
||||
"Find numbers between 10 and 30 in fibonacci sequence\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"To find numbers between 10 and 30 in the Fibonacci sequence, we can generate the Fibonacci sequence and check which numbers fall within this range. Here's a plan:\n",
|
||||
"\n",
|
||||
"1. Generate Fibonacci numbers starting from 0.\n",
|
||||
"2. Continue generating until the numbers exceed 30.\n",
|
||||
"3. Collect and print the numbers that are between 10 and 30.\n",
|
||||
"\n",
|
||||
"Let's implement this in Python:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"# filename: fibonacci_range.py\n",
|
||||
"\n",
|
||||
"def fibonacci_sequence():\n",
|
||||
" a, b = 0, 1\n",
|
||||
" while a <= 30:\n",
|
||||
" if 10 <= a <= 30:\n",
|
||||
" print(a)\n",
|
||||
" a, b = b, a + b\n",
|
||||
"\n",
|
||||
"fibonacci_sequence()\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"This script will print the Fibonacci numbers between 10 and 30. Please execute the code to see the result.\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001b[31m\n",
|
||||
">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n",
|
||||
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
|
||||
"\n",
|
||||
"exitcode: 0 (execution succeeded)\n",
|
||||
"Code output: \n",
|
||||
"13\n",
|
||||
"21\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"The Fibonacci numbers between 10 and 30 are 13 and 21. \n",
|
||||
"\n",
|
||||
"These numbers are part of the Fibonacci sequence, which is generated by adding the two preceding numbers to get the next number, starting from 0 and 1. \n",
|
||||
"\n",
|
||||
"The sequence goes: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...\n",
|
||||
"\n",
|
||||
"As you can see, 13 and 21 are the only numbers in this sequence that fall between 10 and 30.\n",
|
||||
"\n",
|
||||
"TERMINATE\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"{'call_autogen_agent': {'role': 'assistant', 'content': 'The Fibonacci numbers between 10 and 30 are 13 and 21. \\n\\nThese numbers are part of the Fibonacci sequence, which is generated by adding the two preceding numbers to get the next number, starting from 0 and 1. \\n\\nThe sequence goes: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...\\n\\nAs you can see, 13 and 21 are the only numbers in this sequence that fall between 10 and 30.\\n\\nTERMINATE'}}\n",
|
||||
"{'workflow': {'role': 'assistant', 'content': 'The Fibonacci numbers between 10 and 30 are 13 and 21. \\n\\nThese numbers are part of the Fibonacci sequence, which is generated by adding the two preceding numbers to get the next number, starting from 0 and 1. \\n\\nThe sequence goes: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...\\n\\nAs you can see, 13 and 21 are the only numbers in this sequence that fall between 10 and 30.\\n\\nTERMINATE'}}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# pass the thread ID to persist agent outputs for future interactions\n",
|
||||
"# highlight-next-line\n",
|
||||
"config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
|
||||
"\n",
|
||||
"for chunk in workflow.stream(\n",
|
||||
" [\n",
|
||||
" {\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": \"Find numbers between 10 and 30 in fibonacci sequence\",\n",
|
||||
" }\n",
|
||||
" ],\n",
|
||||
" # highlight-next-line\n",
|
||||
" config,\n",
|
||||
"):\n",
|
||||
" print(chunk)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c6cd57b4-d4ee-49f6-be12-318613849669",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Since we're leveraging LangGraph's [persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/) features we can now continue the conversation using the same thread ID -- LangGraph will automatically pass previous history to the AutoGen agent:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"id": "e68811a7-962e-4fe3-9f45-9b99ebbe04e7",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
|
||||
"\n",
|
||||
"Multiply the last number by 3\n",
|
||||
"Context: \n",
|
||||
"Find numbers between 10 and 30 in fibonacci sequence\n",
|
||||
"The Fibonacci numbers between 10 and 30 are 13 and 21. \n",
|
||||
"\n",
|
||||
"These numbers are part of the Fibonacci sequence, which is generated by adding the two preceding numbers to get the next number, starting from 0 and 1. \n",
|
||||
"\n",
|
||||
"The sequence goes: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...\n",
|
||||
"\n",
|
||||
"As you can see, 13 and 21 are the only numbers in this sequence that fall between 10 and 30.\n",
|
||||
"\n",
|
||||
"TERMINATE\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"The last number in the Fibonacci sequence between 10 and 30 is 21. Multiplying 21 by 3 gives:\n",
|
||||
"\n",
|
||||
"21 * 3 = 63\n",
|
||||
"\n",
|
||||
"TERMINATE\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"{'call_autogen_agent': {'role': 'assistant', 'content': 'The last number in the Fibonacci sequence between 10 and 30 is 21. Multiplying 21 by 3 gives:\\n\\n21 * 3 = 63\\n\\nTERMINATE'}}\n",
|
||||
"{'workflow': {'role': 'assistant', 'content': 'The last number in the Fibonacci sequence between 10 and 30 is 21. Multiplying 21 by 3 gives:\\n\\n21 * 3 = 63\\n\\nTERMINATE'}}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for chunk in workflow.stream(\n",
|
||||
" [\n",
|
||||
" {\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": \"Multiply the last number by 3\",\n",
|
||||
" }\n",
|
||||
" ],\n",
|
||||
" # highlight-next-line\n",
|
||||
" config,\n",
|
||||
"):\n",
|
||||
" print(chunk)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
+134
-285
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -23,12 +23,12 @@
|
||||
" </a> \n",
|
||||
" </li>\n",
|
||||
" <li>\n",
|
||||
" <a href=\"https://python.langchain.com/docs/concepts/#chat-models/\">\n",
|
||||
" <a href=\"https://python.langchain.com/docs/concepts/chat_models/\">\n",
|
||||
" Chat Models\n",
|
||||
" </a>\n",
|
||||
" </li>\n",
|
||||
" <li>\n",
|
||||
" <a href=\"https://python.langchain.com/docs/concepts/#tools\">\n",
|
||||
" <a href=\"https://python.langchain.com/docs/concepts/tools/\">\n",
|
||||
" Tools\n",
|
||||
" </a>\n",
|
||||
" </li> \n",
|
||||
@@ -368,7 +368,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.4"
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"id": "992c4695-ec4f-428d-bd05-fb3b5fbd70f4",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to add memory to the prebuilt ReAct agent\n",
|
||||
"# How to add thread-level memory to a ReAct Agent\n",
|
||||
"\n",
|
||||
"<div class=\"admonition tip\">\n",
|
||||
" <p class=\"admonition-title\">Prerequisites</p>\n",
|
||||
@@ -28,12 +28,12 @@
|
||||
" </a> \n",
|
||||
" </li>\n",
|
||||
" <li>\n",
|
||||
" <a href=\"https://python.langchain.com/docs/concepts/#chat-models/\">\n",
|
||||
" <a href=\"https://python.langchain.com/docs/concepts/chat_models/\">\n",
|
||||
" Chat Models\n",
|
||||
" </a>\n",
|
||||
" </li>\n",
|
||||
" <li>\n",
|
||||
" <a href=\"https://python.langchain.com/docs/concepts/#tools\">\n",
|
||||
" <a href=\"https://python.langchain.com/docs/concepts/tools/\">\n",
|
||||
" Tools\n",
|
||||
" </a>\n",
|
||||
" </li>\n",
|
||||
@@ -285,7 +285,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.4"
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "992c4695-ec4f-428d-bd05-fb3b5fbd70f4",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to return structured output from the prebuilt ReAct agent\n",
|
||||
"\n",
|
||||
"!!! info \"Prerequisites\"\n",
|
||||
" This guide assumes familiarity with the following:\n",
|
||||
" \n",
|
||||
" - [Agent Architectures](../../concepts/agentic_concepts/)\n",
|
||||
" - [Chat Models](https://python.langchain.com/docs/concepts/chat_models/)\n",
|
||||
" - [Tools](https://python.langchain.com/docs/concepts/tools/)\n",
|
||||
" - [Structured Output](https://python.langchain.com/docs/concepts/structured_outputs/)\n",
|
||||
"\n",
|
||||
"To return structured output from the prebuilt ReAct agent you can provide a `response_format` parameter with the desired output schema to [create_react_agent][langgraph.prebuilt.chat_agent_executor.create_react_agent]:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"class ResponseFormat(BaseModel):\n",
|
||||
" \"\"\"Respond to the user in this format.\"\"\"\n",
|
||||
" my_special_output: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"graph = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" tools=tools,\n",
|
||||
" # specify the schema for the structured output using `response_format` parameter\n",
|
||||
" response_format=ResponseFormat\n",
|
||||
")\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"Prebuilt ReAct makes an additional LLM call at the end of the ReAct loop to produce a structured output response. Please see [this guide](../react-agent-structured-output) to learn about other strategies for returning structured outputs from a tool-calling agent."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "7be3889f-3c17-4fa1-bd2b-84114a2c7247",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"First, let's install the required packages and set our API keys"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": null,
|
||||
"id": "23a1885c-04ab-4750-aefa-105891fddf3e",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import getpass\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _set_env(var: str):\n",
|
||||
" if not os.environ.get(var):\n",
|
||||
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"_set_env(\"OPENAI_API_KEY\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "87a00ce9",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div class=\"admonition tip\">\n",
|
||||
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
|
||||
" <p style=\"padding-top: 5px;\">\n",
|
||||
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
|
||||
" </p>\n",
|
||||
"</div>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"# 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",
|
||||
"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",
|
||||
"# Define the structured output schema\n",
|
||||
"\n",
|
||||
"from pydantic import BaseModel, Field\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class WeatherResponse(BaseModel):\n",
|
||||
" \"\"\"Respond to the user in this format.\"\"\"\n",
|
||||
"\n",
|
||||
" conditions: str = Field(description=\"Weather conditions\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Define the graph\n",
|
||||
"\n",
|
||||
"from langgraph.prebuilt import create_react_agent\n",
|
||||
"\n",
|
||||
"graph = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" tools=tools,\n",
|
||||
" # specify the schema for the structured output using `response_format` parameter\n",
|
||||
" response_format=WeatherResponse,\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "00407425-506d-4ffd-9c86-987921d8c844",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Usage\n",
|
||||
"\n",
|
||||
"Let's now test our agent:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "9ffff6c3-a4f5-47c9-b51d-97caaee85cd6",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"inputs = {\"messages\": [(\"user\", \"What's the weather in NYC?\")]}\n",
|
||||
"response = graph.invoke(inputs)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "50e273a0-fbdb-4eee-89ca-580fbfb52daf",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"You can see that the agent output contains a `structured_response` key with the structured output conforming to the specified `WeatherResponse` schema, in addition to the message history under `messages` key."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "300748d4-0ed2-470d-8dbc-7c14231e73b8",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"WeatherResponse(conditions='cloudy')"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"response[\"structured_response\"]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "bd9e3487-2cec-44cf-9472-0a51eebeddff",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Customizing prompt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "a608548d-77fc-4d7a-845c-32ae9ec0489a",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"You might need to further customize the second LLM call for the structured output generation and provide a system prompt. To do so, you can pass a tuple (prompt, schema):"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "d1386f99-ffd1-4b36-86ec-cabb3357d929",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"graph = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" tools=tools,\n",
|
||||
" # specify both the system prompt and the schema for the structured output\n",
|
||||
" response_format=(\"Always return capitalized weather conditions\", WeatherResponse),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"inputs = {\"messages\": [(\"user\", \"What's the weather in NYC?\")]}\n",
|
||||
"response = graph.invoke(inputs)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "91f34991-b406-4fd2-a776-4dd03e3dc3dd",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"You can verify that the structured response now contains a capitalized value:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "ba43a67f-127c-45e7-982c-a8210d97a3ed",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"WeatherResponse(conditions='Cloudy')"
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"response[\"structured_response\"]"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
" This guide assumes familiarity with the following:\n",
|
||||
" <ul>\n",
|
||||
" <li> \n",
|
||||
" <a href=\"https://python.langchain.com/v0.1/docs/modules/model_io/concepts/#systemmessage\">\n",
|
||||
" <a href=\"https://python.langchain.com/docs/concepts/messages/#systemmessage\">\n",
|
||||
" SystemMessage\n",
|
||||
" </a>\n",
|
||||
" </li>\n",
|
||||
@@ -24,12 +24,12 @@
|
||||
" </a> \n",
|
||||
" </li>\n",
|
||||
" <li>\n",
|
||||
" <a href=\"https://python.langchain.com/docs/concepts/#chat-models/\">\n",
|
||||
" <a href=\"https://python.langchain.com/docs/concepts/chat_models/\">\n",
|
||||
" Chat Models\n",
|
||||
" </a>\n",
|
||||
" </li>\n",
|
||||
" <li>\n",
|
||||
" <a href=\"https://python.langchain.com/docs/concepts/#tools\">\n",
|
||||
" <a href=\"https://python.langchain.com/docs/concepts/tools/\">\n",
|
||||
" Tools\n",
|
||||
" </a>\n",
|
||||
" </li>\n",
|
||||
@@ -39,7 +39,7 @@
|
||||
"\n",
|
||||
"This tutorial will show how to add a custom system prompt to the [prebuilt ReAct agent](https://langchain-ai.github.io/langgraph/reference/prebuilt/#langgraph.prebuilt.chat_agent_executor.create_react_agent). Please see [this tutorial](../create-react-agent) 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 `state_modifier` param.\n"
|
||||
"You can add a custom system prompt by passing a string to the `prompt` param.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -144,7 +144,7 @@
|
||||
"\n",
|
||||
"from langgraph.prebuilt import create_react_agent\n",
|
||||
"\n",
|
||||
"graph = create_react_agent(model, tools=tools, state_modifier=prompt)"
|
||||
"graph = create_react_agent(model, tools=tools, prompt=prompt)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -223,7 +223,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.4"
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"id": "992c4695-ec4f-428d-bd05-fb3b5fbd70f4",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to use the prebuilt ReAct agent"
|
||||
"# How to use the pre-built ReAct agent"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -24,12 +24,12 @@
|
||||
" </a> \n",
|
||||
" </li>\n",
|
||||
" <li>\n",
|
||||
" <a href=\"https://python.langchain.com/docs/concepts/#chat-models/\">\n",
|
||||
" <a href=\"https://python.langchain.com/docs/concepts/chat_models/\">\n",
|
||||
" Chat Models\n",
|
||||
" </a>\n",
|
||||
" </li>\n",
|
||||
" <li>\n",
|
||||
" <a href=\"https://python.langchain.com/docs/concepts/#tools\">\n",
|
||||
" <a href=\"https://python.langchain.com/docs/concepts/tools/\">\n",
|
||||
" Tools\n",
|
||||
" </a>\n",
|
||||
" </li>\n",
|
||||
@@ -292,7 +292,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.4"
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"id": "d2eecb96-cf0e-47ed-8116-88a7eaa4236d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to add cross-thread persistence (functional API)\n",
|
||||
"\n",
|
||||
"!!! info \"Prerequisites\"\n",
|
||||
"\n",
|
||||
" This guide assumes familiarity with the following:\n",
|
||||
" \n",
|
||||
" - [Functional API](../../concepts/functional_api/)\n",
|
||||
" - [Persistence](../../concepts/persistence/)\n",
|
||||
" - [Memory](../../concepts/memory/)\n",
|
||||
" - [Chat Models](https://python.langchain.com/docs/concepts/chat_models/)\n",
|
||||
"\n",
|
||||
"LangGraph allows you to persist data across **different [threads](../../concepts/persistence/#threads)**. For instance, you can store information about users (their names or preferences) in a shared (cross-thread) memory and reuse them in the new threads (e.g., new conversations).\n",
|
||||
"\n",
|
||||
"When using the [functional API](../../concepts/functional_api/), you can set it up to store and retrieve memories by using the [Store](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore) interface:\n",
|
||||
"\n",
|
||||
"1. Create an instance of a `Store`\n",
|
||||
"\n",
|
||||
" ```python\n",
|
||||
" from langgraph.store.memory import InMemoryStore, BaseStore\n",
|
||||
" \n",
|
||||
" store = InMemoryStore()\n",
|
||||
" ```\n",
|
||||
"\n",
|
||||
"2. Pass the `store` instance to the `entrypoint()` decorator and expose `store` parameter in the function signature:\n",
|
||||
"\n",
|
||||
" ```python\n",
|
||||
" from langgraph.func import entrypoint\n",
|
||||
"\n",
|
||||
" @entrypoint(store=store)\n",
|
||||
" def workflow(inputs: dict, store: BaseStore):\n",
|
||||
" my_task(inputs).result()\n",
|
||||
" ...\n",
|
||||
" ```\n",
|
||||
" \n",
|
||||
"In this guide, we will show how to construct and use a workflow that has a shared memory implemented using the [Store](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore) interface.\n",
|
||||
"\n",
|
||||
"!!! note Note\n",
|
||||
"\n",
|
||||
" Support for the [`Store`](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore) API that is used in this guide was added in LangGraph `v0.2.32`.\n",
|
||||
"\n",
|
||||
" Support for __index__ and __query__ arguments of the [`Store`](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore) API that is used in this guide was added in LangGraph `v0.2.54`.\n",
|
||||
"\n",
|
||||
"!!! tip \"Note\"\n",
|
||||
"\n",
|
||||
" If you need to add cross-thread persistence to a `StateGraph`, check out this [how-to guide](../cross-thread-persistence).\n",
|
||||
"\n",
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"First, let's install the required packages and set our API keys"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "3457aadf",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langchain_anthropic langchain_openai langgraph"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "aa2c64a7",
|
||||
"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\")\n",
|
||||
"_set_env(\"OPENAI_API_KEY\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "51b6817d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"!!! tip \"Set up [LangSmith](https://smith.langchain.com) for LangGraph development\"\n",
|
||||
"\n",
|
||||
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started [here](https://docs.smith.langchain.com)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "6b5b3d42-3d2c-455e-ac10-e2ae74dc1cf1",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Example: simple chatbot with long-term memory"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c4c550b5-1954-496b-8b9d-800361af17dc",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Define store\n",
|
||||
"\n",
|
||||
"In this example we will create a workflow that will be able to retrieve information about a user's preferences. We will do so by defining an `InMemoryStore` - an object that can store data in memory and query that data.\n",
|
||||
"\n",
|
||||
"When storing objects using the `Store` interface you define two things:\n",
|
||||
"\n",
|
||||
"* the namespace for the object, a tuple (similar to directories)\n",
|
||||
"* the object key (similar to filenames)\n",
|
||||
"\n",
|
||||
"In our example, we'll be using `(\"memories\", <user_id>)` as namespace and random UUID as key for each new memory.\n",
|
||||
"\n",
|
||||
"Importantly, to determine the user, we will be passing `user_id` via the config keyword argument of the node function.\n",
|
||||
"\n",
|
||||
"Let's first define our store!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "a7f303d6-612e-4e34-bf36-29d4ed25d802",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.store.memory import InMemoryStore\n",
|
||||
"from langchain_openai import OpenAIEmbeddings\n",
|
||||
"\n",
|
||||
"in_memory_store = InMemoryStore(\n",
|
||||
" index={\n",
|
||||
" \"embed\": OpenAIEmbeddings(model=\"text-embedding-3-small\"),\n",
|
||||
" \"dims\": 1536,\n",
|
||||
" }\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "3389c9f4-226d-40c7-8bfc-ee8aac24f79d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Create workflow"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "2a30a362-528c-45ee-9df6-630d2d843588",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import uuid\n",
|
||||
"\n",
|
||||
"from langchain_anthropic import ChatAnthropic\n",
|
||||
"from langchain_core.runnables import RunnableConfig\n",
|
||||
"from langchain_core.messages import BaseMessage\n",
|
||||
"from langgraph.func import entrypoint, task\n",
|
||||
"from langgraph.graph import add_messages\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.store.base import BaseStore\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"model = ChatAnthropic(model=\"claude-3-5-sonnet-latest\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@task\n",
|
||||
"def call_model(messages: list[BaseMessage], memory_store: BaseStore, user_id: str):\n",
|
||||
" namespace = (\"memories\", user_id)\n",
|
||||
" last_message = messages[-1]\n",
|
||||
" memories = memory_store.search(namespace, query=str(last_message.content))\n",
|
||||
" info = \"\\n\".join([d.value[\"data\"] for d in memories])\n",
|
||||
" system_msg = f\"You are a helpful assistant talking to the user. User info: {info}\"\n",
|
||||
"\n",
|
||||
" # Store new memories if the user asks the model to remember\n",
|
||||
" if \"remember\" in last_message.content.lower():\n",
|
||||
" memory = \"User name is Bob\"\n",
|
||||
" memory_store.put(namespace, str(uuid.uuid4()), {\"data\": memory})\n",
|
||||
"\n",
|
||||
" response = model.invoke([{\"role\": \"system\", \"content\": system_msg}] + messages)\n",
|
||||
" return response\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# NOTE: we're passing the store object here when creating a workflow via entrypoint()\n",
|
||||
"@entrypoint(checkpointer=MemorySaver(), store=in_memory_store)\n",
|
||||
"def workflow(\n",
|
||||
" inputs: list[BaseMessage],\n",
|
||||
" *,\n",
|
||||
" previous: list[BaseMessage],\n",
|
||||
" config: RunnableConfig,\n",
|
||||
" store: BaseStore,\n",
|
||||
"):\n",
|
||||
" user_id = config[\"configurable\"][\"user_id\"]\n",
|
||||
" previous = previous or []\n",
|
||||
" inputs = add_messages(previous, inputs)\n",
|
||||
" response = call_model(inputs, store, user_id).result()\n",
|
||||
" return entrypoint.final(value=response, save=add_messages(inputs, response))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "f22a4a18-67e4-4f0b-b655-a29bbe202e1c",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"!!! note Note\n",
|
||||
"\n",
|
||||
" If you're using LangGraph Cloud or LangGraph Studio, you __don't need__ to pass store to the entrypoint decorator, since it's done automatically."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "552d4e33-556d-4fa5-8094-2a076bc21529",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Run the workflow!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1842c626-6cd9-4f58-b549-58978e478098",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Now let's specify a user ID in the config and tell the model our name:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "c871a073-a466-46ad-aafe-2b870831057e",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"Hello Bob! Nice to meet you. I'll remember that your name is Bob. How can I help you today?\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"1\", \"user_id\": \"1\"}}\n",
|
||||
"input_message = {\"role\": \"user\", \"content\": \"Hi! Remember: my name is Bob\"}\n",
|
||||
"for chunk in workflow.stream([input_message], config, stream_mode=\"values\"):\n",
|
||||
" chunk.pretty_print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "d862be40-1f8a-4057-81c4-b7bf073dc4c1",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"Your name is Bob.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"2\", \"user_id\": \"1\"}}\n",
|
||||
"input_message = {\"role\": \"user\", \"content\": \"what is my name?\"}\n",
|
||||
"for chunk in workflow.stream([input_message], config, stream_mode=\"values\"):\n",
|
||||
" chunk.pretty_print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "80fd01ec-f135-4811-8743-daff8daea422",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We can now inspect our in-memory store and verify that we have in fact saved the memories for the user:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "76cde493-89cf-4709-a339-207d2b7e9ea7",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'data': 'User name is Bob'}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for memory in in_memory_store.search((\"memories\", \"1\")):\n",
|
||||
" print(memory.value)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "23f5d7eb-af23-4131-b8fd-2a69e74e6e55",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's now run the workflow for another user to verify that the memories about the first user are self contained:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "d362350b-d730-48bd-9652-983812fd7811",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"I don't have any information about your name. I can only see our current conversation without any prior context or personal details about you. If you'd like me to know your name, feel free to tell me!\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"3\", \"user_id\": \"2\"}}\n",
|
||||
"input_message = {\"role\": \"user\", \"content\": \"what is my name?\"}\n",
|
||||
"for chunk in workflow.stream([input_message], config, stream_mode=\"values\"):\n",
|
||||
" chunk.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.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -64,18 +64,10 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": null,
|
||||
"id": "aa2c64a7",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"ANTHROPIC_API_KEY: ········\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import getpass\n",
|
||||
"import os\n",
|
||||
@@ -86,7 +78,8 @@
|
||||
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"_set_env(\"ANTHROPIC_API_KEY\")"
|
||||
"_set_env(\"ANTHROPIC_API_KEY\")\n",
|
||||
"_set_env(\"OPENAI_API_KEY\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -183,7 +176,7 @@
|
||||
" store.put(namespace, str(uuid.uuid4()), {\"data\": memory})\n",
|
||||
"\n",
|
||||
" response = model.invoke(\n",
|
||||
" [{\"type\": \"system\", \"content\": system_msg}] + state[\"messages\"]\n",
|
||||
" [{\"role\": \"system\", \"content\": system_msg}] + state[\"messages\"]\n",
|
||||
" )\n",
|
||||
" return {\"messages\": response}\n",
|
||||
"\n",
|
||||
@@ -247,7 +240,7 @@
|
||||
],
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"1\", \"user_id\": \"1\"}}\n",
|
||||
"input_message = {\"type\": \"user\", \"content\": \"Hi! Remember: my name is Bob\"}\n",
|
||||
"input_message = {\"role\": \"user\", \"content\": \"Hi! Remember: my name is Bob\"}\n",
|
||||
"for chunk in graph.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
|
||||
" chunk[\"messages\"][-1].pretty_print()"
|
||||
]
|
||||
@@ -273,7 +266,7 @@
|
||||
],
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"2\", \"user_id\": \"1\"}}\n",
|
||||
"input_message = {\"type\": \"user\", \"content\": \"what is my name?\"}\n",
|
||||
"input_message = {\"role\": \"user\", \"content\": \"what is my name?\"}\n",
|
||||
"for chunk in graph.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
|
||||
" chunk[\"messages\"][-1].pretty_print()"
|
||||
]
|
||||
@@ -334,7 +327,7 @@
|
||||
],
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"3\", \"user_id\": \"2\"}}\n",
|
||||
"input_message = {\"type\": \"user\", \"content\": \"what is my name?\"}\n",
|
||||
"input_message = {\"role\": \"user\", \"content\": \"what is my name?\"}\n",
|
||||
"for chunk in graph.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
|
||||
" chunk[\"messages\"][-1].pretty_print()"
|
||||
]
|
||||
@@ -356,7 +349,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -25,8 +25,20 @@ If you would like to deploy LangGraph Cloud on Kubernetes, you can use this [Hel
|
||||
|
||||
You will eventually need to pass in the following environment variables to the LangGraph Deploy server:
|
||||
|
||||
- `REDIS_URI`: Connection details to a Redis instance. Redis will be used as a pub-sub broker to enable streaming real time output from background runs.
|
||||
- `DATABASE_URI`: Postgres connection details. Postgres will be used to store assistants, threads, runs, persist thread state and long term memory, and to manage the state of the background task queue with 'exactly once' semantics.
|
||||
- `REDIS_URI`: Connection details to a Redis instance. Redis will be used as a pub-sub broker to enable streaming real time output from background runs. The value of `REDIS_URI` must be a valid [Redis connection URI](https://redis-py.readthedocs.io/en/stable/connections.html#redis.Redis.from_url).
|
||||
|
||||
!!! Note "Shared Redis Instance"
|
||||
Multiple self-hosted deployments can share the same Redis instance. For example, for `Deployment A`, `REDIS_URI` can be set to `redis://<hostname_1>:<port>/1` and for `Deployment B`, `REDIS_URI` can be set to `redis://<hostname_1>:<port>/2`.
|
||||
|
||||
`1` and `2` are different database numbers within the same instance, but `<hostname_1>` is shared. **The same database number cannot be used for separate deployments**.
|
||||
|
||||
- `DATABASE_URI`: Postgres connection details. Postgres will be used to store assistants, threads, runs, persist thread state and long term memory, and to manage the state of the background task queue with 'exactly once' semantics. The value of `DATABASE_URI` must be a valid [Postgres connection URI](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS).
|
||||
|
||||
!!! Note "Shared Postgres Instance"
|
||||
Multiple self-hosted deployments can share the same Postgres instance. For example, for `Deployment A`, `DATABASE_URI` can be set to `postgres://<user>:<password>@/<database_name_1>?host=<hostname_1>` and for `Deployment B`, `DATABASE_URI` can be set to `postgres://<user>:<password>@/<database_name_2>?host=<hostname_1>`.
|
||||
|
||||
`<database_name_1>` and `database_name_2` are different databases within the same instance, but `<hostname_1>` is shared. **The same database cannot be used for separate deployments**.
|
||||
|
||||
- `LANGSMITH_API_KEY`: (If using [Self-Hosted Lite](../concepts/deployment_options.md#self-hosted-lite)) LangSmith API key. This will be used to authenticate ONCE at server start up.
|
||||
- `LANGGRAPH_CLOUD_LICENSE_KEY`: (If using [Self-Hosted Enterprise](../concepts/deployment_options.md#self-hosted-enterprise)) LangGraph Platform license key. This will be used to authenticate ONCE at server start up.
|
||||
- `LANGCHAIN_ENDPOINT`: To send traces to a [self-hosted LangSmith](https://docs.smith.langchain.com/self_hosting) instance, set `LANGCHAIN_ENDPOINT` to the hostname of the self-hosted LangSmith instance.
|
||||
|
||||
@@ -591,7 +591,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.4"
|
||||
"version": "3.10.4"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
+75
-28
@@ -9,28 +9,42 @@ Here you’ll find answers to “How do I...?” types of questions. These guide
|
||||
|
||||
## LangGraph
|
||||
|
||||
### Controllability
|
||||
|
||||
LangGraph offers a high level of control over the execution of your graph.
|
||||
|
||||
These how-to guides show how to achieve that controllability.
|
||||
### Graph API Basics
|
||||
|
||||
- [How to update graph state from nodes](state-reducers.ipynb)
|
||||
- [How to create a sequence of steps](sequence.ipynb)
|
||||
- [How to create branches for parallel execution](branching.ipynb)
|
||||
- [How to create and control loops with recursion limits](recursion-limit.ipynb)
|
||||
- [How to visualize your graph](visualization.ipynb)
|
||||
|
||||
### Fine-grained Control
|
||||
|
||||
These guides demonstrate LangGraph features that grant fine-grained control over the
|
||||
execution of your graph.
|
||||
|
||||
- [How to create map-reduce branches for parallel execution](map-reduce.ipynb)
|
||||
- [How to control graph recursion limit](recursion-limit.ipynb)
|
||||
- [How to combine control flow and state updates with Command](command.ipynb)
|
||||
- [How to update state and jump to nodes in graphs and subgraphs](command.ipynb)
|
||||
- [How to add runtime configuration to your graph](configuration.ipynb)
|
||||
- [How to add node retries](node-retries.ipynb)
|
||||
- [How to return state before hitting recursion limit](return-when-recursion-limit-hits.ipynb)
|
||||
|
||||
### Persistence
|
||||
|
||||
[LangGraph Persistence](../concepts/persistence.md) makes it easy to persist state across graph runs (thread-level persistence) and across threads (cross-thread persistence). These how-to guides show how to add persistence to your graph.
|
||||
[LangGraph Persistence](../concepts/persistence.md) makes it easy to persist state across graph runs (per-thread persistence) and across threads (cross-thread persistence). These how-to guides show how to add persistence to your graph.
|
||||
|
||||
- [How to add thread-level persistence to your graph](persistence.ipynb)
|
||||
- [How to add thread-level persistence to subgraphs](subgraph-persistence.ipynb)
|
||||
- [How to add thread-level persistence to a subgraph](subgraph-persistence.ipynb)
|
||||
- [How to add cross-thread persistence to your graph](cross-thread-persistence.ipynb)
|
||||
- [How to use Postgres checkpointer for persistence](persistence_postgres.ipynb)
|
||||
- [How to use MongoDB checkpointer for persistence](persistence_mongodb.ipynb)
|
||||
- [How to create a custom checkpointer using Redis](persistence_redis.ipynb)
|
||||
|
||||
See the below guides for how-to add persistence to your workflow using the (beta)
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to add thread-level persistence (functional API)](persistence-functional.ipynb)
|
||||
- [How to add cross-thread persistence (functional API)](cross-thread-persistence-functional.ipynb)
|
||||
|
||||
### Memory
|
||||
|
||||
LangGraph makes it easy to manage conversation [memory](../concepts/memory.md) in your graph. These how-to guides show how to implement different strategies for that.
|
||||
@@ -59,6 +73,12 @@ Other methods:
|
||||
- [How to edit graph state](human_in_the_loop/edit-graph-state.ipynb): Edit graph state using `graph.update_state` method. Use this if implementing a **human-in-the-loop** workflow via **static breakpoints**.
|
||||
- [How to add dynamic breakpoints with `NodeInterrupt`](human_in_the_loop/dynamic_breakpoints.ipynb): **Not recommended**: Use the [`interrupt` function](../concepts/human_in_the_loop.md) instead.
|
||||
|
||||
See the below guides for how-to implement human-in-the-loop workflows with the (beta)
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to wait for user input (Functional API)](wait-user-input-functional.ipynb)
|
||||
- [How to review tool calls (Functional API)](review-tool-calls-functional.ipynb)
|
||||
|
||||
### Time Travel
|
||||
|
||||
[Time travel](../concepts/time-travel.md) allows you to replay past actions in your LangGraph application to explore alternative paths and debug issues. These how-to guides show how to use time travel in your graph.
|
||||
@@ -69,21 +89,19 @@ Other methods:
|
||||
|
||||
[Streaming](../concepts/streaming.md) is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
|
||||
|
||||
- [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](streaming.ipynb)
|
||||
- [How to stream LLM tokens](streaming-tokens.ipynb)
|
||||
- [How to stream LLM tokens without LangChain models](streaming-tokens-without-langchain.ipynb)
|
||||
- [How to stream custom data](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)
|
||||
- [How to stream events from within a tool without LangChain models](streaming-events-from-within-tools-without-langchain.ipynb)
|
||||
- [How to stream events from the final node](streaming-from-final-node.ipynb)
|
||||
- [How to stream LLM tokens from specific nodes](streaming-specific-nodes.ipynb)
|
||||
- [How to stream data from within a tool](streaming-events-from-within-tools.ipynb)
|
||||
- [How to stream from subgraphs](streaming-subgraphs.ipynb)
|
||||
- [How to disable streaming for models that don't support it](disable-streaming.ipynb)
|
||||
|
||||
### Tool calling
|
||||
|
||||
[Tool calling](https://python.langchain.com/docs/concepts/tool_calling/) is a type of chat model API that accepts tool schemas, along with messages, as input and returns invocations of those tools as part of the output message.
|
||||
[Tool calling](https://python.langchain.com/docs/concepts/tool_calling/) is a type of
|
||||
[chat model](https://python.langchain.com/docs/concepts/chat_models/) API that accepts
|
||||
tool schemas, along with messages, as input and returns invocations of those tools as
|
||||
part of the output message.
|
||||
|
||||
These how-to guides show common patterns for tool calling with LangGraph:
|
||||
|
||||
@@ -98,7 +116,7 @@ These how-to guides show common patterns for tool calling with LangGraph:
|
||||
|
||||
[Subgraphs](../concepts/low_level.md#subgraphs) allow you to reuse an existing graph from another graph. These how-to guides show how to use subgraphs:
|
||||
|
||||
- [How to add and use subgraphs](subgraph.ipynb)
|
||||
- [How to use subgraphs](subgraph.ipynb)
|
||||
- [How to view and update state in subgraphs](subgraphs-manage-state.ipynb)
|
||||
- [How to transform inputs and outputs of a subgraph](subgraph-transform-state.ipynb)
|
||||
|
||||
@@ -112,23 +130,30 @@ These how-to guides show common patterns for tool calling with LangGraph:
|
||||
|
||||
See the [multi-agent tutorials](../tutorials/index.md#multi-agent-systems) for implementations of other multi-agent architectures.
|
||||
|
||||
See the below guides for how to implement multi-agent workflows with the (beta)
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to build a multi-agent network (functional API)](multi-agent-network-functional.ipynb)
|
||||
- [How to add multi-turn conversation in a multi-agent application (functional API)](multi-agent-multi-turn-convo-functional.ipynb)
|
||||
|
||||
### State Management
|
||||
|
||||
- [How to use Pydantic model as state](state-model.ipynb)
|
||||
- [How to use Pydantic model as graph state](state-model.ipynb)
|
||||
- [How to define input/output schema for your graph](input_output_schema.ipynb)
|
||||
- [How to pass private state between nodes inside the graph](pass_private_state.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 add node retries](node-retries.ipynb)
|
||||
- [How to force function calling agent to structure output](react-agent-structured-output.ipynb)
|
||||
- [How to force tool-calling agent to structure output](react-agent-structured-output.ipynb)
|
||||
- [How to pass custom LangSmith run ID for graph runs](run-id-langsmith.ipynb)
|
||||
- [How to return state before hitting recursion limit](return-when-recursion-limit-hits.ipynb)
|
||||
- [How to integrate LangGraph with AutoGen, CrewAI, and other frameworks](autogen-integration.ipynb)
|
||||
|
||||
See the below guide for how to integrate with other frameworks using the (beta)
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to integrate LangGraph (functional API) with AutoGen, CrewAI, and other frameworks](autogen-integration-functional.ipynb)
|
||||
|
||||
### Prebuilt ReAct Agent
|
||||
|
||||
The LangGraph [prebuilt ReAct agent](../reference/prebuilt.md#langgraph.prebuilt.chat_agent_executor.create_react_agent) is pre-built implementation of a [tool calling agent](../concepts/agentic_concepts.md#tool-calling-agent).
|
||||
@@ -137,13 +162,23 @@ One of the big benefits of LangGraph is that you can easily create your own agen
|
||||
|
||||
These guides show how to use the prebuilt ReAct agent:
|
||||
|
||||
- [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 use the pre-built ReAct agent](create-react-agent.ipynb)
|
||||
- [How to add thread-level 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)
|
||||
- [How to create prebuilt ReAct agent from scratch](react-agent-from-scratch.ipynb)
|
||||
- [How to return structured output from a ReAct agent](create-react-agent-structured-output.ipynb)
|
||||
- [How to add semantic search for long-term memory to a ReAct agent](memory/semantic-search.ipynb#using-in-create-react-agent)
|
||||
|
||||
Interested in further customizing the ReAct agent? This guide provides an
|
||||
overview of its underlying implementation to help you customize for your own needs:
|
||||
|
||||
- [How to create prebuilt ReAct agent from scratch](react-agent-from-scratch.ipynb)
|
||||
|
||||
See the below guide for how-to build ReAct agents with the (beta)
|
||||
[Functional API](../concepts/functional_api.md):
|
||||
|
||||
- [How to create a ReAct agent from scratch (Functional API)](react-agent-from-scratch-functional.ipynb)
|
||||
|
||||
## LangGraph Platform
|
||||
|
||||
This section includes how-to guides for LangGraph Platform.
|
||||
@@ -187,11 +222,17 @@ LangGraph applications can be deployed using LangGraph Cloud, which provides a r
|
||||
|
||||
[Assistants](../concepts/assistants.md) is a configured instance of a template.
|
||||
|
||||
See [SDK Reference](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.client.AssistantsClient)
|
||||
for supported endpoints and other details.
|
||||
|
||||
- [How to configure agents](../cloud/how-tos/configuration_cloud.md)
|
||||
- [How to version assistants](../cloud/how-tos/assistant_versioning.md)
|
||||
|
||||
### Threads
|
||||
|
||||
See [SDK Reference](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.client.ThreadsClient)
|
||||
for supported endpoints and other details.
|
||||
|
||||
- [How to copy threads](../cloud/how-tos/copy_threads.md)
|
||||
- [How to check status of your threads](../cloud/how-tos/check_thread_status.md)
|
||||
|
||||
@@ -262,3 +303,9 @@ These are the guides for resolving common errors you may find while building wit
|
||||
- [INVALID_GRAPH_NODE_RETURN_VALUE](../troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md)
|
||||
- [MULTIPLE_SUBGRAPHS](../troubleshooting/errors/MULTIPLE_SUBGRAPHS.md)
|
||||
- [INVALID_CHAT_HISTORY](../troubleshooting/errors/INVALID_CHAT_HISTORY.md)
|
||||
|
||||
### LangGraph Platform Troubleshooting
|
||||
|
||||
These guides provide troubleshooting information for errors that are specific to the LangGraph Platform.
|
||||
|
||||
- [INVALID_LICENSE](../troubleshooting/errors/INVALID_LICENSE.md)
|
||||
@@ -195,7 +195,7 @@
|
||||
"source": [
|
||||
"## Using in `create_react_agent`\n",
|
||||
"\n",
|
||||
"Add semantic search to your tool calling agent by injecting the store in the `state_modifier`. You can also use the store in a tool to let your agent manually store or search for memories."
|
||||
"Add semantic search to your tool calling agent by injecting the store in the `prompt` function. You can also use the store in a tool to let your agent manually store or search for memories."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -248,9 +248,9 @@
|
||||
"agent = create_react_agent(\n",
|
||||
" init_chat_model(\"openai:gpt-4o-mini\"),\n",
|
||||
" tools=[upsert_memory],\n",
|
||||
" # The state_modifier is run to prepare the messages for the LLM. It is called\n",
|
||||
" # The 'prompt' function is run to prepare the messages for the LLM. It is called\n",
|
||||
" # right before each LLM call\n",
|
||||
" state_modifier=prepare_messages,\n",
|
||||
" prompt=prepare_messages,\n",
|
||||
" store=store,\n",
|
||||
")"
|
||||
]
|
||||
@@ -524,7 +524,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.2"
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -0,0 +1,462 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"id": "a2b182eb-1e31-43c8-85b1-706508dfa370",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to add multi-turn conversation in a multi-agent application (functional API)\n",
|
||||
"\n",
|
||||
"!!! info \"Prerequisites\"\n",
|
||||
" This guide assumes familiarity with the following:\n",
|
||||
"\n",
|
||||
" - [Multi-agent systems](../../concepts/multi_agent)\n",
|
||||
" - [Human-in-the-loop](../../concepts/human_in_the_loop)\n",
|
||||
" - [Functional API](../../concepts/functional_api)\n",
|
||||
" - [Command](../../concepts/low_level/#command)\n",
|
||||
" - [LangGraph Glossary](../../concepts/low_level/)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"In this how-to guide, we’ll build an application that allows an end-user to engage in a *multi-turn conversation* with one or more agents. We'll create a node that uses an [`interrupt`](../../reference/types/#langgraph.types.interrupt) to collect user input and routes back to the **active** agent.\n",
|
||||
"\n",
|
||||
"The agents will be implemented as tasks in a workflow that executes agent steps and determines the next action:\n",
|
||||
"\n",
|
||||
"1. **Wait for user input** to continue the conversation, or\n",
|
||||
"2. **Route to another agent** (or back to itself, such as in a loop) via a [**handoff**](../../concepts/multi_agent/#handoffs).\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"from langgraph.func import entrypoint, task\n",
|
||||
"from langgraph.prebuilt import create_react_agent\n",
|
||||
"from langchain_core.tools import tool\n",
|
||||
"from langgraph.types import interrupt\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Define a tool to signal intent to hand off to a different agent\n",
|
||||
"# Note: this is not using Command(goto) syntax for navigating to different agents:\n",
|
||||
"# `workflow()` below handles the handoffs explicitly\n",
|
||||
"@tool(return_direct=True)\n",
|
||||
"def transfer_to_hotel_advisor():\n",
|
||||
" \"\"\"Ask hotel advisor agent for help.\"\"\"\n",
|
||||
" return \"Successfully transferred to hotel advisor\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# define an agent\n",
|
||||
"travel_advisor_tools = [transfer_to_hotel_advisor, ...]\n",
|
||||
"travel_advisor = create_react_agent(model, travel_advisor_tools)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# define a task that calls an agent\n",
|
||||
"@task\n",
|
||||
"def call_travel_advisor(messages):\n",
|
||||
" response = travel_advisor.invoke({\"messages\": messages})\n",
|
||||
" return response[\"messages\"]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# define the multi-agent network workflow\n",
|
||||
"@entrypoint(checkpointer)\n",
|
||||
"def workflow(messages):\n",
|
||||
" call_active_agent = call_travel_advisor\n",
|
||||
" while True:\n",
|
||||
" agent_messages = call_active_agent(messages).result()\n",
|
||||
" ai_msg = get_last_ai_msg(agent_messages)\n",
|
||||
" if not ai_msg.tool_calls:\n",
|
||||
" user_input = interrupt(value=\"Ready for user input.\")\n",
|
||||
" messages = messages + [{\"role\": \"user\", \"content\": user_input}]\n",
|
||||
" continue\n",
|
||||
"\n",
|
||||
" messages = messages + agent_messages\n",
|
||||
" call_active_agent = get_next_agent(messages)\n",
|
||||
" return entrypoint.final(value=agent_messages[-1], save=messages)\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "faaa4444-cd06-4813-b9ca-c9700fe12cb7",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"First, let's install the required packages"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "05038da0-31df-4066-a1a4-c4ccb5db4d3a",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# %%capture --no-stderr\n",
|
||||
"# %pip install -U langgraph langchain-anthropic"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "0bcff5d4-130e-426d-9285-40d0f72c7cd3",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdin",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"ANTHROPIC_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(\"ANTHROPIC_API_KEY\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c3ec6e48-85dc-4905-ba50-985e5d4788e6",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div class=\"admonition tip\">\n",
|
||||
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
|
||||
" <p style=\"padding-top: 5px;\">\n",
|
||||
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
|
||||
" </p>\n",
|
||||
"</div>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"id": "c217c3fe-ca50-45a1-be91-912bc83ed8b3",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"In this example we will build a team of travel assistant agents that can communicate with each other.\n",
|
||||
"\n",
|
||||
"We will create 2 agents:\n",
|
||||
"\n",
|
||||
"* `travel_advisor`: can help with travel destination recommendations. Can ask `hotel_advisor` for help.\n",
|
||||
"* `hotel_advisor`: can help with hotel recommendations. Can ask `travel_advisor` for help.\n",
|
||||
"\n",
|
||||
"This is a fully-connected network - every agent can talk to any other agent. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "eb51463a-4425-44ad-91d5-f21fd5b4e3b3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import random\n",
|
||||
"from typing_extensions import Literal\n",
|
||||
"from langchain_core.tools import tool\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def get_travel_recommendations():\n",
|
||||
" \"\"\"Get recommendation for travel destinations\"\"\"\n",
|
||||
" return random.choice([\"aruba\", \"turks and caicos\"])\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def get_hotel_recommendations(location: Literal[\"aruba\", \"turks and caicos\"]):\n",
|
||||
" \"\"\"Get hotel recommendations for a given destination.\"\"\"\n",
|
||||
" return {\n",
|
||||
" \"aruba\": [\n",
|
||||
" \"The Ritz-Carlton, Aruba (Palm Beach)\"\n",
|
||||
" \"Bucuti & Tara Beach Resort (Eagle Beach)\"\n",
|
||||
" ],\n",
|
||||
" \"turks and caicos\": [\"Grace Bay Club\", \"COMO Parrot Cay\"],\n",
|
||||
" }[location]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool(return_direct=True)\n",
|
||||
"def transfer_to_hotel_advisor():\n",
|
||||
" \"\"\"Ask hotel advisor agent for help.\"\"\"\n",
|
||||
" return \"Successfully transferred to hotel advisor\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool(return_direct=True)\n",
|
||||
"def transfer_to_travel_advisor():\n",
|
||||
" \"\"\"Ask travel advisor agent for help.\"\"\"\n",
|
||||
" return \"Successfully transferred to travel advisor\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "7f5b2a7f",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"!!! note \"Transfer tools\"\n",
|
||||
"\n",
|
||||
" You might have noticed that we're using `@tool(return_direct=True)` in the transfer tools. This is done so that individual agents (e.g., `travel_advisor`) can exit the ReAct loop early once these tools are called. This is the desired behavior, as we want to detect when the agent calls this tool and hand control off _immediately_ to a different agent. \n",
|
||||
" \n",
|
||||
" **NOTE**: This is meant to work with the prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] -- if you are building a custom agent, make sure to manually add logic for handling early exit for tools that are marked with `return_direct`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "213d661e-6ba4-42b9-bc7f-6c8c423e3419",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's now create our agents using the the prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] and our multi-agent workflow. Note that will be calling [`interrupt`][langgraph.types.interrupt] every time after we get the final response from each of the agents."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "aa4bdbff-9461-46cc-aee9-8a22d3c3d9ec",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import uuid\n",
|
||||
"\n",
|
||||
"from langchain_core.messages import AIMessage\n",
|
||||
"from langchain_anthropic import ChatAnthropic\n",
|
||||
"from langgraph.prebuilt import create_react_agent\n",
|
||||
"from langgraph.graph import add_messages\n",
|
||||
"from langgraph.func import entrypoint, task\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.types import interrupt, Command\n",
|
||||
"\n",
|
||||
"model = ChatAnthropic(model=\"claude-3-5-sonnet-latest\")\n",
|
||||
"\n",
|
||||
"# Define travel advisor ReAct agent\n",
|
||||
"travel_advisor_tools = [\n",
|
||||
" get_travel_recommendations,\n",
|
||||
" transfer_to_hotel_advisor,\n",
|
||||
"]\n",
|
||||
"travel_advisor = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" travel_advisor_tools,\n",
|
||||
" state_modifier=(\n",
|
||||
" \"You are a general travel expert that can recommend travel destinations (e.g. countries, cities, etc). \"\n",
|
||||
" \"If you need hotel recommendations, ask 'hotel_advisor' for help. \"\n",
|
||||
" \"You MUST include human-readable response before transferring to another agent.\"\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@task\n",
|
||||
"def call_travel_advisor(messages):\n",
|
||||
" # You can also add additional logic like changing the input to the agent / output from the agent, etc.\n",
|
||||
" # NOTE: we're invoking the ReAct agent with the full history of messages in the state\n",
|
||||
" response = travel_advisor.invoke({\"messages\": messages})\n",
|
||||
" return response[\"messages\"]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Define hotel advisor ReAct agent\n",
|
||||
"hotel_advisor_tools = [get_hotel_recommendations, transfer_to_travel_advisor]\n",
|
||||
"hotel_advisor = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" hotel_advisor_tools,\n",
|
||||
" state_modifier=(\n",
|
||||
" \"You are a hotel expert that can provide hotel recommendations for a given destination. \"\n",
|
||||
" \"If you need help picking travel destinations, ask 'travel_advisor' for help.\"\n",
|
||||
" \"You MUST include human-readable response before transferring to another agent.\"\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@task\n",
|
||||
"def call_hotel_advisor(messages):\n",
|
||||
" response = hotel_advisor.invoke({\"messages\": messages})\n",
|
||||
" return response[\"messages\"]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"checkpointer = MemorySaver()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def string_to_uuid(input_string):\n",
|
||||
" return str(uuid.uuid5(uuid.NAMESPACE_URL, input_string))\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@entrypoint(checkpointer=checkpointer)\n",
|
||||
"def multi_turn_graph(messages, previous):\n",
|
||||
" previous = previous or []\n",
|
||||
" messages = add_messages(previous, messages)\n",
|
||||
" call_active_agent = call_travel_advisor\n",
|
||||
" while True:\n",
|
||||
" agent_messages = call_active_agent(messages).result()\n",
|
||||
" messages = add_messages(messages, agent_messages)\n",
|
||||
" # Find the last AI message\n",
|
||||
" # If one of the handoff tools is called, the last message returned\n",
|
||||
" # by the agent will be a ToolMessage because we set them to have\n",
|
||||
" # \"return_direct=True\". This means that the last AIMessage will\n",
|
||||
" # have tool calls.\n",
|
||||
" # Otherwise, the last returned message will be an AIMessage with\n",
|
||||
" # no tool calls, which means we are ready for new input.\n",
|
||||
" ai_msg = next(m for m in reversed(agent_messages) if isinstance(m, AIMessage))\n",
|
||||
" if not ai_msg.tool_calls:\n",
|
||||
" user_input = interrupt(value=\"Ready for user input.\")\n",
|
||||
" # Add user input as a human message\n",
|
||||
" # NOTE: we generate unique ID for the human message based on its content\n",
|
||||
" # it's important, since on subsequent invocations previous user input (interrupt) values\n",
|
||||
" # will be looked up again and we will attempt to add them again here\n",
|
||||
" # `add_messages` deduplicates messages based on the ID, ensuring correct message history\n",
|
||||
" human_message = {\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": user_input,\n",
|
||||
" \"id\": string_to_uuid(user_input),\n",
|
||||
" }\n",
|
||||
" messages = add_messages(messages, [human_message])\n",
|
||||
" continue\n",
|
||||
"\n",
|
||||
" tool_call = ai_msg.tool_calls[-1]\n",
|
||||
" if tool_call[\"name\"] == \"transfer_to_hotel_advisor\":\n",
|
||||
" call_active_agent = call_hotel_advisor\n",
|
||||
" elif tool_call[\"name\"] == \"transfer_to_travel_advisor\":\n",
|
||||
" call_active_agent = call_travel_advisor\n",
|
||||
" else:\n",
|
||||
" raise ValueError(f\"Expected transfer tool, got '{tool_call['name']}'\")\n",
|
||||
"\n",
|
||||
" return entrypoint.final(value=agent_messages[-1], save=messages)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "af856e1b-41fc-4041-8cbf-3818a60088e0",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Test multi-turn conversation\n",
|
||||
"\n",
|
||||
"Let's test a multi turn conversation with this application."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "2b6fde57-86e3-440e-a7bf-f1e9b5ed9ff2",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\n",
|
||||
"--- Conversation Turn 1 ---\n",
|
||||
"\n",
|
||||
"User: {'role': 'user', 'content': 'i wanna go somewhere warm in the caribbean', 'id': 'f48d82a7-7efa-43f5-ad4c-541758c95f61'}\n",
|
||||
"\n",
|
||||
"call_travel_advisor: Based on the recommendations, Aruba would be an excellent choice for your Caribbean getaway! Known as \"One Happy Island,\" Aruba offers:\n",
|
||||
"- Year-round warm weather with consistent temperatures around 82°F (28°C)\n",
|
||||
"- Beautiful white sand beaches like Eagle Beach and Palm Beach\n",
|
||||
"- Crystal clear waters perfect for swimming and snorkeling\n",
|
||||
"- Minimal rainfall and location outside the hurricane belt\n",
|
||||
"- Rich culture blending Dutch and Caribbean influences\n",
|
||||
"- Various activities from water sports to desert-like landscape exploration\n",
|
||||
"- Excellent dining and shopping options\n",
|
||||
"\n",
|
||||
"Would you like me to help you find suitable accommodations in Aruba? I can transfer you to our hotel advisor who can recommend specific hotels based on your preferences.\n",
|
||||
"\n",
|
||||
"--- Conversation Turn 2 ---\n",
|
||||
"\n",
|
||||
"User: Command(resume='could you recommend a nice hotel in one of the areas and tell me which area it is.')\n",
|
||||
"\n",
|
||||
"call_hotel_advisor: I can recommend two excellent options in different areas:\n",
|
||||
"\n",
|
||||
"1. The Ritz-Carlton, Aruba - Located in Palm Beach\n",
|
||||
"- Luxury beachfront resort\n",
|
||||
"- Located in the vibrant Palm Beach area, known for its lively atmosphere\n",
|
||||
"- Close to restaurants, shopping, and nightlife\n",
|
||||
"- Perfect for those who want a more active vacation with plenty of amenities nearby\n",
|
||||
"\n",
|
||||
"2. Bucuti & Tara Beach Resort - Located in Eagle Beach\n",
|
||||
"- Adults-only boutique resort\n",
|
||||
"- Situated on the quieter Eagle Beach\n",
|
||||
"- Known for its romantic atmosphere and excellent service\n",
|
||||
"- Ideal for couples seeking a more peaceful, intimate setting\n",
|
||||
"\n",
|
||||
"Would you like more specific information about either of these properties or their locations?\n",
|
||||
"\n",
|
||||
"--- Conversation Turn 3 ---\n",
|
||||
"\n",
|
||||
"User: Command(resume='i like the first one. could you recommend something to do near the hotel?')\n",
|
||||
"\n",
|
||||
"call_travel_advisor: Near The Ritz-Carlton in Palm Beach, here are some popular activities you can enjoy:\n",
|
||||
"\n",
|
||||
"1. Palm Beach Strip - Take a walk along this bustling strip filled with restaurants, shops, and bars\n",
|
||||
"2. Visit the Bubali Bird Sanctuary - Just a short distance away\n",
|
||||
"3. Try your luck at the Stellaris Casino - Located right in The Ritz-Carlton\n",
|
||||
"4. Water Sports at Palm Beach - Right in front of the hotel you can:\n",
|
||||
" - Go parasailing\n",
|
||||
" - Try jet skiing\n",
|
||||
" - Take a sunset sailing cruise\n",
|
||||
"5. Visit the Palm Beach Plaza Mall - High-end shopping just a short walk away\n",
|
||||
"6. Enjoy dinner at Madame Janette's - One of Aruba's most famous restaurants nearby\n",
|
||||
"\n",
|
||||
"Would you like more specific information about any of these activities or other suggestions in the area?\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"thread_config = {\"configurable\": {\"thread_id\": uuid.uuid4()}}\n",
|
||||
"\n",
|
||||
"inputs = [\n",
|
||||
" # 1st round of conversation,\n",
|
||||
" {\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": \"i wanna go somewhere warm in the caribbean\",\n",
|
||||
" \"id\": str(uuid.uuid4()),\n",
|
||||
" },\n",
|
||||
" # Since we're using `interrupt`, we'll need to resume using the Command primitive.\n",
|
||||
" # 2nd round of conversation,\n",
|
||||
" Command(\n",
|
||||
" resume=\"could you recommend a nice hotel in one of the areas and tell me which area it is.\"\n",
|
||||
" ),\n",
|
||||
" # 3rd round of conversation,\n",
|
||||
" Command(\n",
|
||||
" resume=\"i like the first one. could you recommend something to do near the hotel?\"\n",
|
||||
" ),\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"for idx, user_input in enumerate(inputs):\n",
|
||||
" print()\n",
|
||||
" print(f\"--- Conversation Turn {idx + 1} ---\")\n",
|
||||
" print()\n",
|
||||
" print(f\"User: {user_input}\")\n",
|
||||
" print()\n",
|
||||
" for update in multi_turn_graph.stream(\n",
|
||||
" user_input,\n",
|
||||
" config=thread_config,\n",
|
||||
" stream_mode=\"updates\",\n",
|
||||
" ):\n",
|
||||
" for node_id, value in update.items():\n",
|
||||
" if isinstance(value, list) and value:\n",
|
||||
" last_message = value[-1]\n",
|
||||
" if isinstance(last_message, dict) or last_message.type != \"ai\":\n",
|
||||
" continue\n",
|
||||
" print(f\"{node_id}: {last_message.content}\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -229,7 +229,7 @@
|
||||
"travel_advisor = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" travel_advisor_tools,\n",
|
||||
" state_modifier=(\n",
|
||||
" prompt=(\n",
|
||||
" \"You are a general travel expert that can recommend travel destinations (e.g. countries, cities, etc). \"\n",
|
||||
" \"If you need hotel recommendations, ask 'hotel_advisor' for help. \"\n",
|
||||
" \"You MUST include human-readable response before transferring to another agent.\"\n",
|
||||
@@ -254,7 +254,7 @@
|
||||
"hotel_advisor = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" hotel_advisor_tools,\n",
|
||||
" state_modifier=(\n",
|
||||
" prompt=(\n",
|
||||
" \"You are a hotel expert that can provide hotel recommendations for a given destination. \"\n",
|
||||
" \"If you need help picking travel destinations, ask 'travel_advisor' for help.\"\n",
|
||||
" \"You MUST include human-readable response before transferring to another agent.\"\n",
|
||||
|
||||
@@ -0,0 +1,500 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "87684b48-150e-4e15-b0a5-a9dd7851f8fb",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to build a multi-agent network (functional API)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"id": "2c65639c-9705-49f1-840a-370718852e98",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"!!! info \"Prerequisites\" \n",
|
||||
" This guide assumes familiarity with the following:\n",
|
||||
"\n",
|
||||
" - [Multi-agent systems](../../concepts/multi_agent)\n",
|
||||
" - [Functional API](../../concepts/functional_api)\n",
|
||||
" - [Command](../../concepts/low_level/#command)\n",
|
||||
" - [LangGraph Glossary](../../concepts/low_level/)\n",
|
||||
"\n",
|
||||
"In this how-to guide we will demonstrate how to implement a [multi-agent network](../../concepts/multi_agent#network) architecture where each agent can communicate with every other agent (many-to-many connections) and can decide which agent to call next. We will be using [functional API](../../concepts/functional_api) — individual agents will be defined as tasks and the agent handoffs will be defined in the main [entrypoint()][langgraph.func.entrypoint]:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"from langgraph.func import entrypoint\n",
|
||||
"from langgraph.prebuilt import create_react_agent\n",
|
||||
"from langchain_core.tools import tool\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Define a tool to signal intent to hand off to a different agent\n",
|
||||
"@tool(return_direct=True)\n",
|
||||
"def transfer_to_hotel_advisor():\n",
|
||||
" \"\"\"Ask hotel advisor agent for help.\"\"\"\n",
|
||||
" return \"Successfully transferred to hotel advisor\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# define an agent\n",
|
||||
"travel_advisor_tools = [transfer_to_hotel_advisor, ...]\n",
|
||||
"travel_advisor = create_react_agent(model, travel_advisor_tools)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# define a task that calls an agent\n",
|
||||
"@task\n",
|
||||
"def call_travel_advisor(messages):\n",
|
||||
" response = travel_advisor.invoke({\"messages\": messages})\n",
|
||||
" return response[\"messages\"]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# define the multi-agent network workflow\n",
|
||||
"@entrypoint()\n",
|
||||
"def workflow(messages):\n",
|
||||
" call_active_agent = call_travel_advisor\n",
|
||||
" while True:\n",
|
||||
" agent_messages = call_active_agent(messages).result()\n",
|
||||
" messages = messages + agent_messages\n",
|
||||
" call_active_agent = get_next_agent(messages)\n",
|
||||
" return messages\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "faaa4444-cd06-4813-b9ca-c9700fe12cb7",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"First, let's install the required packages"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "05038da0-31df-4066-a1a4-c4ccb5db4d3a",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain-anthropic"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "0bcff5d4-130e-426d-9285-40d0f72c7cd3",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdin",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"ANTHROPIC_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(\"ANTHROPIC_API_KEY\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c3ec6e48-85dc-4905-ba50-985e5d4788e6",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div class=\"admonition tip\">\n",
|
||||
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
|
||||
" <p style=\"padding-top: 5px;\">\n",
|
||||
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
|
||||
" </p>\n",
|
||||
"</div>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "4a53f304-3709-4df7-8714-1ca61e615743",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Travel agent example"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "34cd131b-f0c2-4b69-887f-2cbd5afb14a7",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"In this example we will build a team of travel assistant agents that can communicate with each other.\n",
|
||||
"\n",
|
||||
"We will create 2 agents:\n",
|
||||
"\n",
|
||||
"* `travel_advisor`: can help with travel destination recommendations. Can ask `hotel_advisor` for help.\n",
|
||||
"* `hotel_advisor`: can help with hotel recommendations. Can ask `travel_advisor` for help.\n",
|
||||
"\n",
|
||||
"This is a fully-connected network - every agent can talk to any other agent. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "fedc9ed0-e90c-4ee1-a7c6-f5af3c634a7b",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"First, let's create some of the tools that the agents will be using:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "7e31f258-ec28-4020-b86d-c91dfa9a3bfc",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import random\n",
|
||||
"from typing_extensions import Literal\n",
|
||||
"from langchain_core.tools import tool\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def get_travel_recommendations():\n",
|
||||
" \"\"\"Get recommendation for travel destinations\"\"\"\n",
|
||||
" return random.choice([\"aruba\", \"turks and caicos\"])\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def get_hotel_recommendations(location: Literal[\"aruba\", \"turks and caicos\"]):\n",
|
||||
" \"\"\"Get hotel recommendations for a given destination.\"\"\"\n",
|
||||
" return {\n",
|
||||
" \"aruba\": [\n",
|
||||
" \"The Ritz-Carlton, Aruba (Palm Beach)\"\n",
|
||||
" \"Bucuti & Tara Beach Resort (Eagle Beach)\"\n",
|
||||
" ],\n",
|
||||
" \"turks and caicos\": [\"Grace Bay Club\", \"COMO Parrot Cay\"],\n",
|
||||
" }[location]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool(return_direct=True)\n",
|
||||
"def transfer_to_hotel_advisor():\n",
|
||||
" \"\"\"Ask hotel advisor agent for help.\"\"\"\n",
|
||||
" return \"Successfully transferred to hotel advisor\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool(return_direct=True)\n",
|
||||
"def transfer_to_travel_advisor():\n",
|
||||
" \"\"\"Ask travel advisor agent for help.\"\"\"\n",
|
||||
" return \"Successfully transferred to travel advisor\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "d8519a32-d23b-48b0-bd18-74f8c0dacf58",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"!!! note \"Transfer tools\"\n",
|
||||
"\n",
|
||||
" You might have noticed that we're using `@tool(return_direct=True)` in the transfer tools. This is done so that individual agents (e.g., `travel_advisor`) can exit the ReAct loop early once these tools are called. This is the desired behavior, as we want to detect when the agent calls this tool and hand control off _immediately_ to a different agent. \n",
|
||||
" \n",
|
||||
" **NOTE**: This is meant to work with the prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] -- if you are building a custom agent, make sure to manually add logic for handling early exit for tools that are marked with `return_direct`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "93dbc3bd-27b9-4d79-b5dd-be592bc50f74",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Now let's define our agent tasks and combine them into a single multi-agent network workflow:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "b638d6c4-3de6-4921-980c-2df1bd1cc9c7",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_core.messages import AIMessage\n",
|
||||
"from langchain_anthropic import ChatAnthropic\n",
|
||||
"from langgraph.prebuilt import create_react_agent\n",
|
||||
"from langgraph.graph import add_messages\n",
|
||||
"from langgraph.func import entrypoint, task\n",
|
||||
"\n",
|
||||
"model = ChatAnthropic(model=\"claude-3-5-sonnet-latest\")\n",
|
||||
"\n",
|
||||
"# Define travel advisor ReAct agent\n",
|
||||
"travel_advisor_tools = [\n",
|
||||
" get_travel_recommendations,\n",
|
||||
" transfer_to_hotel_advisor,\n",
|
||||
"]\n",
|
||||
"travel_advisor = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" travel_advisor_tools,\n",
|
||||
" state_modifier=(\n",
|
||||
" \"You are a general travel expert that can recommend travel destinations (e.g. countries, cities, etc). \"\n",
|
||||
" \"If you need hotel recommendations, ask 'hotel_advisor' for help. \"\n",
|
||||
" \"You MUST include human-readable response before transferring to another agent.\"\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@task\n",
|
||||
"def call_travel_advisor(messages):\n",
|
||||
" # You can also add additional logic like changing the input to the agent / output from the agent, etc.\n",
|
||||
" # NOTE: we're invoking the ReAct agent with the full history of messages in the state\n",
|
||||
" response = travel_advisor.invoke({\"messages\": messages})\n",
|
||||
" return response[\"messages\"]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Define hotel advisor ReAct agent\n",
|
||||
"hotel_advisor_tools = [get_hotel_recommendations, transfer_to_travel_advisor]\n",
|
||||
"hotel_advisor = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" hotel_advisor_tools,\n",
|
||||
" state_modifier=(\n",
|
||||
" \"You are a hotel expert that can provide hotel recommendations for a given destination. \"\n",
|
||||
" \"If you need help picking travel destinations, ask 'travel_advisor' for help.\"\n",
|
||||
" \"You MUST include human-readable response before transferring to another agent.\"\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@task\n",
|
||||
"def call_hotel_advisor(messages):\n",
|
||||
" response = hotel_advisor.invoke({\"messages\": messages})\n",
|
||||
" return response[\"messages\"]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@entrypoint()\n",
|
||||
"def workflow(messages):\n",
|
||||
" messages = add_messages([], messages)\n",
|
||||
"\n",
|
||||
" call_active_agent = call_travel_advisor\n",
|
||||
" while True:\n",
|
||||
" agent_messages = call_active_agent(messages).result()\n",
|
||||
" messages = add_messages(messages, agent_messages)\n",
|
||||
" ai_msg = next(m for m in reversed(agent_messages) if isinstance(m, AIMessage))\n",
|
||||
" if not ai_msg.tool_calls:\n",
|
||||
" break\n",
|
||||
"\n",
|
||||
" tool_call = ai_msg.tool_calls[-1]\n",
|
||||
" if tool_call[\"name\"] == \"transfer_to_travel_advisor\":\n",
|
||||
" call_active_agent = call_travel_advisor\n",
|
||||
" elif tool_call[\"name\"] == \"transfer_to_hotel_advisor\":\n",
|
||||
" call_active_agent = call_hotel_advisor\n",
|
||||
" else:\n",
|
||||
" raise ValueError(f\"Expected transfer tool, got '{tool_call['name']}'\")\n",
|
||||
"\n",
|
||||
" return messages"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "9223db83-1938-434a-9d24-8666842a8eea",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Lastly, let's define a helper to render the agent outputs:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"id": "058f3d96-534f-4b97-afb3-799ba81224ea",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_core.messages import convert_to_messages\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def pretty_print_messages(update):\n",
|
||||
" if isinstance(update, tuple):\n",
|
||||
" ns, update = update\n",
|
||||
" # skip parent graph updates in the printouts\n",
|
||||
" if len(ns) == 0:\n",
|
||||
" return\n",
|
||||
"\n",
|
||||
" graph_id = ns[-1].split(\":\")[0]\n",
|
||||
" print(f\"Update from subgraph {graph_id}:\")\n",
|
||||
" print(\"\\n\")\n",
|
||||
"\n",
|
||||
" for node_name, node_update in update.items():\n",
|
||||
" print(f\"Update from node {node_name}:\")\n",
|
||||
" print(\"\\n\")\n",
|
||||
"\n",
|
||||
" for m in convert_to_messages(node_update[\"messages\"]):\n",
|
||||
" m.pretty_print()\n",
|
||||
" print(\"\\n\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "7132e2c0-d937-4325-a30e-e715c5304fe0",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's test it out using the same input as our original multi-agent system:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"id": "29b47c57-ad05-4f10-83bf-c3ff6ff8eb93",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Update from subgraph call_travel_advisor:\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Update from node agent:\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"[{'text': \"I'll help you find a warm Caribbean destination and then get some hotel recommendations for you.\\n\\nLet me first get some destination recommendations for the Caribbean region.\", 'type': 'text'}, {'id': 'toolu_015vT8PkPq1VXvjrDvSpWUwJ', 'input': {}, 'name': 'get_travel_recommendations', 'type': 'tool_use'}]\n",
|
||||
"Tool Calls:\n",
|
||||
" get_travel_recommendations (toolu_015vT8PkPq1VXvjrDvSpWUwJ)\n",
|
||||
" Call ID: toolu_015vT8PkPq1VXvjrDvSpWUwJ\n",
|
||||
" Args:\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Update from subgraph call_travel_advisor:\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Update from node tools:\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
|
||||
"Name: get_travel_recommendations\n",
|
||||
"\n",
|
||||
"turks and caicos\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Update from subgraph call_travel_advisor:\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Update from node agent:\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"[{'text': \"Based on the recommendation, I suggest Turks and Caicos! This beautiful British Overseas Territory is known for its stunning white-sand beaches, crystal-clear turquoise waters, and year-round warm weather. Grace Bay Beach in Providenciales is consistently ranked among the world's best beaches. The islands offer excellent snorkeling, diving, and water sports opportunities, plus a relaxed Caribbean atmosphere.\\n\\nNow, let me connect you with our hotel advisor to get some specific hotel recommendations for Turks and Caicos.\", 'type': 'text'}, {'id': 'toolu_01JY7pNNWFuaWoe9ymxFYiPV', 'input': {}, 'name': 'transfer_to_hotel_advisor', 'type': 'tool_use'}]\n",
|
||||
"Tool Calls:\n",
|
||||
" transfer_to_hotel_advisor (toolu_01JY7pNNWFuaWoe9ymxFYiPV)\n",
|
||||
" Call ID: toolu_01JY7pNNWFuaWoe9ymxFYiPV\n",
|
||||
" Args:\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Update from subgraph call_travel_advisor:\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Update from node tools:\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
|
||||
"Name: transfer_to_hotel_advisor\n",
|
||||
"\n",
|
||||
"Successfully transferred to hotel advisor\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Update from subgraph call_hotel_advisor:\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Update from node agent:\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"[{'text': 'Let me get some hotel recommendations for Turks and Caicos:', 'type': 'text'}, {'id': 'toolu_0129ELa7jFocn16bowaGNapg', 'input': {'location': 'turks and caicos'}, 'name': 'get_hotel_recommendations', 'type': 'tool_use'}]\n",
|
||||
"Tool Calls:\n",
|
||||
" get_hotel_recommendations (toolu_0129ELa7jFocn16bowaGNapg)\n",
|
||||
" Call ID: toolu_0129ELa7jFocn16bowaGNapg\n",
|
||||
" Args:\n",
|
||||
" location: turks and caicos\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Update from subgraph call_hotel_advisor:\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Update from node tools:\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
|
||||
"Name: get_hotel_recommendations\n",
|
||||
"\n",
|
||||
"[\"Grace Bay Club\", \"COMO Parrot Cay\"]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Update from subgraph call_hotel_advisor:\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"Update from node agent:\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"Here are two excellent hotel options in Turks and Caicos:\n",
|
||||
"\n",
|
||||
"1. Grace Bay Club: This luxury resort is located on the world-famous Grace Bay Beach. It offers all-oceanfront suites, exceptional dining options, and personalized service. The resort features adult-only and family-friendly sections, making it perfect for any type of traveler.\n",
|
||||
"\n",
|
||||
"2. COMO Parrot Cay: This exclusive private island resort offers the ultimate luxury escape. It's known for its pristine beach, world-class spa, and holistic wellness programs. The resort provides an intimate, secluded experience with top-notch amenities and service.\n",
|
||||
"\n",
|
||||
"Would you like more specific information about either of these properties or would you like to explore hotels in another destination?\n",
|
||||
"\n",
|
||||
"\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for chunk in workflow.stream(\n",
|
||||
" [\n",
|
||||
" {\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": \"i wanna go somewhere warm in the caribbean. pick one destination and give me hotel recommendations\",\n",
|
||||
" }\n",
|
||||
" ],\n",
|
||||
" subgraphs=True,\n",
|
||||
"):\n",
|
||||
" pretty_print_messages(chunk)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "d7d89ee0-0229-4718-9b98-bdd3f59c1014",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Voila - `travel_advisor` picks a destination and then makes a decision to call `hotel_advisor` for more info!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -555,7 +555,7 @@
|
||||
"travel_advisor = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" travel_advisor_tools,\n",
|
||||
" state_modifier=(\n",
|
||||
" prompt=(\n",
|
||||
" \"You are a general travel expert that can recommend travel destinations (e.g. countries, cities, etc). \"\n",
|
||||
" \"If you need hotel recommendations, ask 'hotel_advisor' for help. \"\n",
|
||||
" \"You MUST include human-readable response before transferring to another agent.\"\n",
|
||||
@@ -579,7 +579,7 @@
|
||||
"hotel_advisor = create_react_agent(\n",
|
||||
" model,\n",
|
||||
" hotel_advisor_tools,\n",
|
||||
" state_modifier=(\n",
|
||||
" prompt=(\n",
|
||||
" \"You are a hotel expert that can provide hotel recommendations for a given destination. \"\n",
|
||||
" \"If you need help picking travel destinations, ask 'travel_advisor' for help.\"\n",
|
||||
" \"You MUST include human-readable response before transferring to another agent.\"\n",
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to add thread-level persistence (functional API)\n",
|
||||
"\n",
|
||||
"!!! info \"Prerequisites\"\n",
|
||||
"\n",
|
||||
" This guide assumes familiarity with the following:\n",
|
||||
" \n",
|
||||
" - [Functional API](../../concepts/functional_api/)\n",
|
||||
" - [Persistence](../../concepts/persistence/)\n",
|
||||
" - [Memory](../../concepts/memory/)\n",
|
||||
" - [Chat Models](https://python.langchain.com/docs/concepts/chat_models/)\n",
|
||||
"\n",
|
||||
"Many AI applications need memory to share context across multiple interactions on the same [thread](../../concepts/persistence#threads) (e.g., multiple turns of a conversation). In LangGraph functional API, this kind of memory can be added to any [entrypoint()][langgraph.func.entrypoint] workflow using [thread-level persistence](https://langchain-ai.github.io/langgraph/concepts/persistence).\n",
|
||||
"\n",
|
||||
"When creating a LangGraph workflow, you can set it up to persist its results by using a [checkpointer](https://langchain-ai.github.io/langgraph/reference/checkpoints/#basecheckpointsaver):\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"1. Create an instance of a checkpointer:\n",
|
||||
"\n",
|
||||
" ```python\n",
|
||||
" from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
" \n",
|
||||
" checkpointer = MemorySaver() \n",
|
||||
" ```\n",
|
||||
"\n",
|
||||
"2. Pass `checkpointer` instance to the `entrypoint()` decorator:\n",
|
||||
"\n",
|
||||
" ```python\n",
|
||||
" from langgraph.func import entrypoint\n",
|
||||
" \n",
|
||||
" @entrypoint(checkpointer=checkpointer)\n",
|
||||
" def workflow(inputs)\n",
|
||||
" ...\n",
|
||||
" ```\n",
|
||||
"\n",
|
||||
"3. Optionally expose `previous` parameter in the workflow function signature:\n",
|
||||
"\n",
|
||||
" ```python\n",
|
||||
" @entrypoint(checkpointer=checkpointer)\n",
|
||||
" def workflow(\n",
|
||||
" inputs,\n",
|
||||
" *,\n",
|
||||
" # you can optionally specify `previous` in the workflow function signature\n",
|
||||
" # to access the return value from the workflow as of the last execution\n",
|
||||
" previous\n",
|
||||
" ):\n",
|
||||
" previous = previous or []\n",
|
||||
" combined_inputs = previous + inputs\n",
|
||||
" result = do_something(combined_inputs)\n",
|
||||
" ...\n",
|
||||
" ```\n",
|
||||
"\n",
|
||||
"4. Optionally choose which values will be returned from the workflow and which will be saved by the checkpointer as `previous`:\n",
|
||||
"\n",
|
||||
" ```python\n",
|
||||
" @entrypoint(checkpointer=checkpointer)\n",
|
||||
" def workflow(inputs, *, previous):\n",
|
||||
" ...\n",
|
||||
" result = do_something(...)\n",
|
||||
" return entrypoint.final(value=result, save=combine(inputs, result))\n",
|
||||
" ```\n",
|
||||
"\n",
|
||||
"This guide shows how you can add thread-level persistence to your workflow.\n",
|
||||
"\n",
|
||||
"!!! tip \"Note\"\n",
|
||||
"\n",
|
||||
" If you need memory that is __shared__ across multiple conversations or users (cross-thread persistence), check out this [how-to guide](../cross-thread-persistence-functional).\n",
|
||||
"\n",
|
||||
"!!! tip \"Note\"\n",
|
||||
"\n",
|
||||
" If you need to add thread-level persistence to a `StateGraph`, check out this [how-to guide](../persistence)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"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 key for Anthropic (the LLM we will use)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"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": [
|
||||
"<div class=\"admonition tip\">\n",
|
||||
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
|
||||
" <p style=\"padding-top: 5px;\">\n",
|
||||
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
|
||||
" </p>\n",
|
||||
"</div>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "4cf509bc",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Example: simple chatbot with short-term memory\n",
|
||||
"\n",
|
||||
"We will be using a workflow with a single task that calls a [chat model](https://python.langchain.com/docs/concepts/chat_models/).\n",
|
||||
"\n",
|
||||
"Let's first define the model we'll be using:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "892b54b9-75f0-4804-9ed0-88b5e5532989",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_anthropic import ChatAnthropic\n",
|
||||
"\n",
|
||||
"model = ChatAnthropic(model=\"claude-3-5-sonnet-latest\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "7b7a2792-982b-4e47-83eb-0c594725d1c1",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Now we can define our task and workflow. To add in persistence, we need to pass in a [Checkpointer](https://langchain-ai.github.io/langgraph/reference/checkpoints/#langgraph.checkpoint.base.BaseCheckpointSaver) to the [entrypoint()][langgraph.func.entrypoint] decorator."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "87326ea6-34c5-46da-a41f-dda26ef9bd74",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_core.messages import BaseMessage\n",
|
||||
"from langgraph.graph import add_messages\n",
|
||||
"from langgraph.func import entrypoint, task\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@task\n",
|
||||
"def call_model(messages: list[BaseMessage]):\n",
|
||||
" response = model.invoke(messages)\n",
|
||||
" return response\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"checkpointer = MemorySaver()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@entrypoint(checkpointer=checkpointer)\n",
|
||||
"def workflow(inputs: list[BaseMessage], *, previous: list[BaseMessage]):\n",
|
||||
" if previous:\n",
|
||||
" inputs = add_messages(previous, inputs)\n",
|
||||
"\n",
|
||||
" response = call_model(inputs).result()\n",
|
||||
" return entrypoint.final(value=response, save=add_messages(inputs, response))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "250d8fd9-2e7a-4892-9adc-19762a1e3cce",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"If we try to use this workflow, the context of the conversation will be persisted across interactions:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "7654ebcc-2179-41b4-92d1-6666f6f8634f",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"!!! note Note\n",
|
||||
"\n",
|
||||
" If you're using LangGraph Cloud or LangGraph Studio, you __don't need__ to pass checkpointer to the entrypoint decorator, since it's done automatically."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We can now interact with the agent and see that it remembers previous messages!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "cfd140f0-a5a6-4697-8115-322242f197b5",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"Hi Bob! I'm Claude. Nice to meet you! How are you today?\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
|
||||
"input_message = {\"role\": \"user\", \"content\": \"hi! I'm bob\"}\n",
|
||||
"for chunk in workflow.stream([input_message], config, stream_mode=\"values\"):\n",
|
||||
" chunk.pretty_print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1bb07bf8-68b7-4049-a0f1-eb67a4879a3a",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"You can always resume previous threads:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "08ae8246-11d5-40e1-8567-361e5bef8917",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"Your name is Bob.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"input_message = {\"role\": \"user\", \"content\": \"what's my name?\"}\n",
|
||||
"for chunk in workflow.stream([input_message], config, stream_mode=\"values\"):\n",
|
||||
" chunk.pretty_print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "3f47bbfc-d9ef-4288-ba4a-ebbc0136fa9d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"If we want to start a new conversation, we can pass in a different `thread_id`. Poof! All the memories are gone!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "273d56a8-f40f-4a51-a27f-7c6bb2bda0ba",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"I don't know your name unless you tell me. Each conversation I have starts fresh, so I don't have access to any previous interactions or personal information unless you share it with me.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"input_message = {\"role\": \"user\", \"content\": \"what's my name?\"}\n",
|
||||
"for chunk in workflow.stream(\n",
|
||||
" [input_message],\n",
|
||||
" {\"configurable\": {\"thread_id\": \"2\"}},\n",
|
||||
" stream_mode=\"values\",\n",
|
||||
"):\n",
|
||||
" chunk.pretty_print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "ac7926a8-4c88-4b16-973c-53d6da3f4a08",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"!!! tip \"Streaming tokens\"\n",
|
||||
"\n",
|
||||
" If you would like to stream LLM tokens from your chatbot, you can use `stream_mode=\"messages\"`. Check out this [how-to guide](../streaming-tokens) to learn more."
|
||||
]
|
||||
}
|
||||
],
|
||||
"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.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -78,7 +78,7 @@
|
||||
"id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Next, we need to set API keys for OpenAI (the LLM we will use) and Tavily (the search tool we will use)"
|
||||
"Next, we need to set API key for Anthropic (the LLM we will use)."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -211,11 +211,11 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"input_message = {\"type\": \"user\", \"content\": \"hi! I'm bob\"}\n",
|
||||
"input_message = {\"role\": \"user\", \"content\": \"hi! I'm bob\"}\n",
|
||||
"for chunk in graph.stream({\"messages\": [input_message]}, stream_mode=\"values\"):\n",
|
||||
" chunk[\"messages\"][-1].pretty_print()\n",
|
||||
"\n",
|
||||
"input_message = {\"type\": \"user\", \"content\": \"what's my name?\"}\n",
|
||||
"input_message = {\"role\": \"user\", \"content\": \"what's my name?\"}\n",
|
||||
"for chunk in graph.stream({\"messages\": [input_message]}, stream_mode=\"values\"):\n",
|
||||
" chunk[\"messages\"][-1].pretty_print()"
|
||||
]
|
||||
@@ -286,7 +286,7 @@
|
||||
],
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
|
||||
"input_message = {\"type\": \"user\", \"content\": \"hi! I'm bob\"}\n",
|
||||
"input_message = {\"role\": \"user\", \"content\": \"hi! I'm bob\"}\n",
|
||||
"for chunk in graph.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
|
||||
" chunk[\"messages\"][-1].pretty_print()"
|
||||
]
|
||||
@@ -319,7 +319,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"input_message = {\"type\": \"user\", \"content\": \"what's my name?\"}\n",
|
||||
"input_message = {\"role\": \"user\", \"content\": \"what's my name?\"}\n",
|
||||
"for chunk in graph.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
|
||||
" chunk[\"messages\"][-1].pretty_print()"
|
||||
]
|
||||
@@ -352,7 +352,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"input_message = {\"type\": \"user\", \"content\": \"what's my name?\"}\n",
|
||||
"input_message = {\"role\": \"user\", \"content\": \"what's my name?\"}\n",
|
||||
"for chunk in graph.stream(\n",
|
||||
" {\"messages\": [input_message]},\n",
|
||||
" {\"configurable\": {\"thread_id\": \"2\"}},\n",
|
||||
@@ -378,7 +378,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
"...\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"!!! info \"Setup\n",
|
||||
"!!! info \"Setup\"",
|
||||
"\n",
|
||||
" You need to run `.setup()` once on your checkpointer to initialize the database before you can use it."
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,463 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to create a ReAct agent from scratch (Functional API)\n",
|
||||
"\n",
|
||||
"!!! info \"Prerequisites\"\n",
|
||||
" This guide assumes familiarity with the following:\n",
|
||||
" \n",
|
||||
" - [Chat Models](https://python.langchain.com/docs/concepts/chat_models)\n",
|
||||
" - [Messages](https://python.langchain.com/docs/concepts/messages)\n",
|
||||
" - [Tool Calling](https://python.langchain.com/docs/concepts/tool_calling/)\n",
|
||||
" - [Entrypoints](../../concepts/functional_api/#entrypoint) and [Tasks](../../concepts/functional_api/#task)\n",
|
||||
"\n",
|
||||
"This guide demonstrates how to implement a ReAct agent using the LangGraph [Functional API](../../concepts/functional_api).\n",
|
||||
"\n",
|
||||
"The ReAct agent is a [tool-calling agent](../../concepts/agentic_concepts/#tool-calling-agent) that operates as follows:\n",
|
||||
"\n",
|
||||
"1. Queries are issued to a chat model;\n",
|
||||
"2. If the model generates no [tool calls](../../concepts/agentic_concepts/#tool-calling), we return the model response.\n",
|
||||
"3. If the model generates tool calls, we execute the tool calls with available tools, append them as [tool messages](https://python.langchain.com/docs/concepts/messages/) to our message list, and repeat the process.\n",
|
||||
"\n",
|
||||
"This is a simple and versatile set-up that can be extended with memory, human-in-the-loop capabilities, and other features. See the dedicated [how-to guides](../../how-tos/#prebuilt-react-agent) for examples.\n",
|
||||
"\n",
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"First, let's install the required packages and set our API keys:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain-openai"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import getpass\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _set_env(var: str):\n",
|
||||
" if not os.environ.get(var):\n",
|
||||
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"_set_env(\"OPENAI_API_KEY\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div class=\"admonition tip\">\n",
|
||||
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for better debugging</p>\n",
|
||||
" <p style=\"padding-top: 5px;\">\n",
|
||||
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM aps built with LangGraph — read more about how to get started in the <a href=\"https://docs.smith.langchain.com\">docs</a>. \n",
|
||||
" </p>\n",
|
||||
" </div>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Create ReAct agent\n",
|
||||
"\n",
|
||||
"Now that you have installed the required packages and set your environment variables, we can create our agent.\n",
|
||||
"\n",
|
||||
"### Define model and tools\n",
|
||||
"\n",
|
||||
"Let's first define the tools and model we will use for our example. Here we will use a single place-holder tool that gets a description of the weather for a location.\n",
|
||||
"\n",
|
||||
"We will use an [OpenAI](https://python.langchain.com/docs/integrations/providers/openai/) chat model for this example, but any model [supporting tool-calling](https://python.langchain.com/docs/integrations/chat/) will suffice."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langchain_core.tools import tool\n",
|
||||
"\n",
|
||||
"model = ChatOpenAI(model=\"gpt-4o-mini\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def get_weather(location: str):\n",
|
||||
" \"\"\"Call to get the weather from a specific location.\"\"\"\n",
|
||||
" # This is a placeholder for the actual implementation\n",
|
||||
" if any([city in location.lower() for city in [\"sf\", \"san francisco\"]]):\n",
|
||||
" return \"It's sunny!\"\n",
|
||||
" elif \"boston\" in location.lower():\n",
|
||||
" return \"It's rainy!\"\n",
|
||||
" else:\n",
|
||||
" return f\"I am not sure what the weather is in {location}\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"tools = [get_weather]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Define tasks\n",
|
||||
"\n",
|
||||
"We next define the [tasks](../../concepts/functional_api/#task) we will execute. Here there are two different tasks:\n",
|
||||
"\n",
|
||||
"1. **Call model**: We want to query our chat model with a list of messages.\n",
|
||||
"2. **Call tool**: If our model generates tool calls, we want to execute them."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_core.messages import ToolMessage\n",
|
||||
"from langgraph.func import entrypoint, task\n",
|
||||
"\n",
|
||||
"tools_by_name = {tool.name: tool for tool in tools}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@task\n",
|
||||
"def call_model(messages):\n",
|
||||
" \"\"\"Call model with a sequence of messages.\"\"\"\n",
|
||||
" response = model.bind_tools(tools).invoke(messages)\n",
|
||||
" return response\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@task\n",
|
||||
"def call_tool(tool_call):\n",
|
||||
" tool = tools_by_name[tool_call[\"name\"]]\n",
|
||||
" observation = tool.invoke(tool_call[\"args\"])\n",
|
||||
" return ToolMessage(content=observation, tool_call_id=tool_call[\"id\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Define entrypoint\n",
|
||||
"\n",
|
||||
"Our [entrypoint](../../concepts/functional_api/#entrypoint) will handle the orchestration of these two tasks. As described above, when our `call_model` task generates tool calls, the `call_tool` task will generate responses for each. We append all messages to a single messages list.\n",
|
||||
"\n",
|
||||
"!!! tip\n",
|
||||
" Note that because tasks return future-like objects, the below implementation executes tools in parallel."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.graph.message import add_messages\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@entrypoint()\n",
|
||||
"def agent(messages):\n",
|
||||
" llm_response = call_model(messages).result()\n",
|
||||
" while True:\n",
|
||||
" if not llm_response.tool_calls:\n",
|
||||
" break\n",
|
||||
"\n",
|
||||
" # Execute tools\n",
|
||||
" tool_result_futures = [\n",
|
||||
" call_tool(tool_call) for tool_call in llm_response.tool_calls\n",
|
||||
" ]\n",
|
||||
" tool_results = [fut.result() for fut in tool_result_futures]\n",
|
||||
"\n",
|
||||
" # Append to message list\n",
|
||||
" messages = add_messages(messages, [llm_response, *tool_results])\n",
|
||||
"\n",
|
||||
" # Call model again\n",
|
||||
" llm_response = call_model(messages).result()\n",
|
||||
"\n",
|
||||
" return llm_response"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Usage\n",
|
||||
"\n",
|
||||
"To use our agent, we invoke it with a messages list. Based on our implementation, these can be LangChain [message](https://python.langchain.com/docs/concepts/messages/) objects or OpenAI-style dicts:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'role': 'user', 'content': \"What's the weather in san francisco?\"}\n",
|
||||
"\n",
|
||||
"call_model:\n",
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"Tool Calls:\n",
|
||||
" get_weather (call_tNnkrjnoz6MNfCHJpwfuEQ0v)\n",
|
||||
" Call ID: call_tNnkrjnoz6MNfCHJpwfuEQ0v\n",
|
||||
" Args:\n",
|
||||
" location: san francisco\n",
|
||||
"\n",
|
||||
"call_tool:\n",
|
||||
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
|
||||
"\n",
|
||||
"It's sunny!\n",
|
||||
"\n",
|
||||
"call_model:\n",
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"The weather in San Francisco is sunny!\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"user_message = {\"role\": \"user\", \"content\": \"What's the weather in san francisco?\"}\n",
|
||||
"print(user_message)\n",
|
||||
"\n",
|
||||
"for step in agent.stream([user_message]):\n",
|
||||
" for task_name, message in step.items():\n",
|
||||
" if task_name == \"agent\":\n",
|
||||
" continue # Just print task updates\n",
|
||||
" print(f\"\\n{task_name}:\")\n",
|
||||
" message.pretty_print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Perfect! The graph correctly calls the `get_weather` tool and responds to the user after receiving the information from the tool. Check out the LangSmith trace [here](https://smith.langchain.com/public/d5a0d5ea-bdaa-4032-911e-7db177c8141b/r)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Add thread-level persistence\n",
|
||||
"\n",
|
||||
"Adding [thread-level persistence](../../concepts/persistence#threads) lets us support conversational experiences with our agent: subsequent invocations will append to the prior messages list, retaining the full conversational context.\n",
|
||||
"\n",
|
||||
"To add thread-level persistence to our agent:\n",
|
||||
"\n",
|
||||
"1. Select a [checkpointer](../../concepts/persistence#checkpointer-libraries): here we will use [MemorySaver](../../reference/checkpoints/#langgraph.checkpoint.memory.MemorySaver), a simple in-memory checkpointer.\n",
|
||||
"2. Update our entrypoint to accept the previous messages state as a second argument. Here, we simply append the message updates to the previous sequence of messages.\n",
|
||||
"3. Choose which values will be returned from the workflow and which will be saved by the checkpointer as `previous` using `entrypoint.final` (optional)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"\n",
|
||||
"# highlight-next-line\n",
|
||||
"checkpointer = MemorySaver()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# highlight-next-line\n",
|
||||
"@entrypoint(checkpointer=checkpointer)\n",
|
||||
"# highlight-next-line\n",
|
||||
"def agent(messages, previous):\n",
|
||||
" # highlight-next-line\n",
|
||||
" if previous is not None:\n",
|
||||
" # highlight-next-line\n",
|
||||
" messages = add_messages(previous, messages)\n",
|
||||
"\n",
|
||||
" llm_response = call_model(messages).result()\n",
|
||||
" while True:\n",
|
||||
" if not llm_response.tool_calls:\n",
|
||||
" break\n",
|
||||
"\n",
|
||||
" # Execute tools\n",
|
||||
" tool_result_futures = [\n",
|
||||
" call_tool(tool_call) for tool_call in llm_response.tool_calls\n",
|
||||
" ]\n",
|
||||
" tool_results = [fut.result() for fut in tool_result_futures]\n",
|
||||
"\n",
|
||||
" # Append to message list\n",
|
||||
" messages = add_messages(messages, [llm_response, *tool_results])\n",
|
||||
"\n",
|
||||
" # Call model again\n",
|
||||
" llm_response = call_model(messages).result()\n",
|
||||
"\n",
|
||||
" # Generate final response\n",
|
||||
" messages = add_messages(messages, llm_response)\n",
|
||||
" # highlight-next-line\n",
|
||||
" return entrypoint.final(value=llm_response, save=messages)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We will now need to pass in a config when running our application. The config will specify an identifier for the conversational thread.\n",
|
||||
"\n",
|
||||
"!!! tip\n",
|
||||
"\n",
|
||||
" Read more about thread-level persistence in our [concepts page](../../concepts/persistence/) and [how-to guides](../../how-tos/#persistence)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"1\"}}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We start a thread the same way as before, this time passing in the config:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'role': 'user', 'content': \"What's the weather in san francisco?\"}\n",
|
||||
"\n",
|
||||
"call_model:\n",
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"Tool Calls:\n",
|
||||
" get_weather (call_lubbUSdDofmOhFunPEZLBz3g)\n",
|
||||
" Call ID: call_lubbUSdDofmOhFunPEZLBz3g\n",
|
||||
" Args:\n",
|
||||
" location: San Francisco\n",
|
||||
"\n",
|
||||
"call_tool:\n",
|
||||
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
|
||||
"\n",
|
||||
"It's sunny!\n",
|
||||
"\n",
|
||||
"call_model:\n",
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"The weather in San Francisco is sunny!\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"user_message = {\"role\": \"user\", \"content\": \"What's the weather in san francisco?\"}\n",
|
||||
"print(user_message)\n",
|
||||
"\n",
|
||||
"# highlight-next-line\n",
|
||||
"for step in agent.stream([user_message], config):\n",
|
||||
" for task_name, message in step.items():\n",
|
||||
" if task_name == \"agent\":\n",
|
||||
" continue # Just print task updates\n",
|
||||
" print(f\"\\n{task_name}:\")\n",
|
||||
" message.pretty_print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"When we ask a follow-up conversation, the model uses the prior context to infer that we are asking about the weather:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'role': 'user', 'content': 'How does it compare to Boston, MA?'}\n",
|
||||
"\n",
|
||||
"call_model:\n",
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"Tool Calls:\n",
|
||||
" get_weather (call_8sTKYAhSIHOdjLD5d6gaswuV)\n",
|
||||
" Call ID: call_8sTKYAhSIHOdjLD5d6gaswuV\n",
|
||||
" Args:\n",
|
||||
" location: Boston, MA\n",
|
||||
"\n",
|
||||
"call_tool:\n",
|
||||
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
|
||||
"\n",
|
||||
"It's rainy!\n",
|
||||
"\n",
|
||||
"call_model:\n",
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"Compared to San Francisco, which is sunny, Boston, MA is experiencing rainy weather.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"user_message = {\"role\": \"user\", \"content\": \"How does it compare to Boston, MA?\"}\n",
|
||||
"print(user_message)\n",
|
||||
"\n",
|
||||
"for step in agent.stream([user_message], config):\n",
|
||||
" for task_name, message in step.items():\n",
|
||||
" if task_name == \"agent\":\n",
|
||||
" continue # Just print task updates\n",
|
||||
" print(f\"\\n{task_name}:\")\n",
|
||||
" message.pretty_print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"In the [LangSmith trace](https://smith.langchain.com/public/20a1116b-bb3b-44c1-8765-7a28663439d9/r), we can see that the full conversational context is retained in each model call."
|
||||
]
|
||||
}
|
||||
],
|
||||
"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.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user