mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-19 14:15:44 +02:00
Compare commits
260
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d3527db38 | ||
|
|
36ed1a5d4b | ||
|
|
7637a769dd | ||
|
|
3bbe09dac4 | ||
|
|
947f8c8c1d | ||
|
|
84cb3ea151 | ||
|
|
f4dea9c5f7 | ||
|
|
a8758661bc | ||
|
|
8912d50312 | ||
|
|
8d23929288 | ||
|
|
9b6100bc4f | ||
|
|
238b562e78 | ||
|
|
7fcc0da4e3 | ||
|
|
d7eff6ab85 | ||
|
|
330c91f622 | ||
|
|
3dbf6f144e | ||
|
|
8c2fefcb28 | ||
|
|
c88d289a73 | ||
|
|
d12f5c6d8b | ||
|
|
9ff54e029e | ||
|
|
2a89c7a5b4 | ||
|
|
f2ec99b9a8 | ||
|
|
eb387cc946 | ||
|
|
54b53f384d | ||
|
|
65784d8174 | ||
|
|
507930e5b7 | ||
|
|
85e698e20b | ||
|
|
bf4dc5d5f6 | ||
|
|
ce0fad8ddb | ||
|
|
1315c0d743 | ||
|
|
ada751d19b | ||
|
|
f310716921 | ||
|
|
568044171b | ||
|
|
1d0f3577a7 | ||
|
|
3979bdb792 | ||
|
|
d8d663ccd5 | ||
|
|
25a72e77ef | ||
|
|
da806c466d | ||
|
|
49c316578b | ||
|
|
7039a54871 | ||
|
|
ef17e0351a | ||
|
|
0d9c0732d4 | ||
|
|
a2cfe694f1 | ||
|
|
858c166cae | ||
|
|
03785c7d83 | ||
|
|
93cb2a7730 | ||
|
|
4b48e71d2c | ||
|
|
b7744aff9b | ||
|
|
4a4dd16535 | ||
|
|
ef790a57c6 | ||
|
|
c49692d794 | ||
|
|
3cda14b069 | ||
|
|
66a13b8865 | ||
|
|
58e139e7fd | ||
|
|
a8d860273b | ||
|
|
3ac4cdf3d4 | ||
|
|
3a524e0e56 | ||
|
|
134f8faf8c | ||
|
|
904a1a3471 | ||
|
|
cb30f68642 | ||
|
|
15692acef9 | ||
|
|
610257665f | ||
|
|
bc482431c3 | ||
|
|
155e0c66d5 | ||
|
|
ca63a06549 | ||
|
|
e8c553c41e | ||
|
|
beafddf7c8 | ||
|
|
1e6da19257 | ||
|
|
e0898409b9 | ||
|
|
bc86757e73 | ||
|
|
71442916e5 | ||
|
|
1f29925034 | ||
|
|
4c4d7054ab | ||
|
|
1333d8b478 | ||
|
|
2bac0d0f41 | ||
|
|
ed7b2c9e8a | ||
|
|
0597aedaff | ||
|
|
82db383199 | ||
|
|
dec7eb6f58 | ||
|
|
065055e587 | ||
|
|
4162be8119 | ||
|
|
6ece7124ed | ||
|
|
ffa9b8672a | ||
|
|
19b382335f | ||
|
|
c72acc9145 | ||
|
|
c30aa1ca13 | ||
|
|
15c3105748 | ||
|
|
0b7f451b40 | ||
|
|
75dec9b924 | ||
|
|
8090ca67c5 | ||
|
|
9f6e57d2a7 | ||
|
|
72893d9abb | ||
|
|
7074604204 | ||
|
|
0720b931e8 | ||
|
|
4935cf52bf | ||
|
|
45e3d1a3f1 | ||
|
|
acd8acf237 | ||
|
|
3ec419a2f6 | ||
|
|
8a00a0026e | ||
|
|
7e32de9405 | ||
|
|
96af4c72ce | ||
|
|
6c7d9c35bc | ||
|
|
5654d8fc86 | ||
|
|
f93512e3b3 | ||
|
|
a261e1a497 | ||
|
|
38daba5259 | ||
|
|
078f9f7275 | ||
|
|
22f5367af7 | ||
|
|
4e2b508ebb | ||
|
|
7ec3c0e827 | ||
|
|
82408bacf1 | ||
|
|
9a306ce931 | ||
|
|
a9a59dd4e4 | ||
|
|
a545a70afb | ||
|
|
4521f9312d | ||
|
|
d67419522d | ||
|
|
d85e267a83 | ||
|
|
426125cf96 | ||
|
|
4827377191 | ||
|
|
0b6088f913 | ||
|
|
578ec48031 | ||
|
|
e7bc74e918 | ||
|
|
0a87b9fa1c | ||
|
|
37848a5361 | ||
|
|
9147d05cc4 | ||
|
|
46171dd759 | ||
|
|
47ed3d97e9 | ||
|
|
06c2481783 | ||
|
|
1907646bd4 | ||
|
|
3a65f83ae1 | ||
|
|
57811a6bfd | ||
|
|
02697b5712 | ||
|
|
e76f4cc434 | ||
|
|
f51e7ea9a4 | ||
|
|
14ec51601c | ||
|
|
14976d4c56 | ||
|
|
a94168af7f | ||
|
|
2f41b2891b | ||
|
|
c857a77dd1 | ||
|
|
5fb2c2c6f8 | ||
|
|
4250ff92b8 | ||
|
|
7fd4a9ed30 | ||
|
|
2106b5e4a6 | ||
|
|
1af0367b34 | ||
|
|
f037a2e9cb | ||
|
|
aa1a6be160 | ||
|
|
c9e6ee6da7 | ||
|
|
bd7b9cca21 | ||
|
|
b228fc1a9b | ||
|
|
c3794f1fd3 | ||
|
|
4e1db854f6 | ||
|
|
630d9c79ed | ||
|
|
8f8f3849fc | ||
|
|
656f89e16a | ||
|
|
9b90a24d94 | ||
|
|
77d7deb033 | ||
|
|
7ca37afc74 | ||
|
|
2bc0e2df42 | ||
|
|
1dc09dc45f | ||
|
|
1cc02825ea | ||
|
|
d1b7a787ee | ||
|
|
f8b053a286 | ||
|
|
ebf060675d | ||
|
|
3d33b575d6 | ||
|
|
314488b5c5 | ||
|
|
6e4dc26890 | ||
|
|
9cfb4b01c8 | ||
|
|
e7d2621a05 | ||
|
|
c0c534cbe3 | ||
|
|
7aaeedd7ba | ||
|
|
e15d56c2f2 | ||
|
|
8a7c6b4fa7 | ||
|
|
2e073473dc | ||
|
|
31dcd15927 | ||
|
|
1bd40b2ebf | ||
|
|
b5429b6342 | ||
|
|
fc95028738 | ||
|
|
e916cab08f | ||
|
|
e4f1f90127 | ||
|
|
a16a33806c | ||
|
|
007f419bdc | ||
|
|
5bee9e92b5 | ||
|
|
6a68581a9f | ||
|
|
78781b0a90 | ||
|
|
d15c5a46de | ||
|
|
ce37fd3a04 | ||
|
|
64636ee978 | ||
|
|
388a9643a5 | ||
|
|
c9f4b5aede | ||
|
|
855139d869 | ||
|
|
d07cbd34ce | ||
|
|
c701fef634 | ||
|
|
2c8fc3c9c6 | ||
|
|
45054df71a | ||
|
|
8ad797e261 | ||
|
|
2dbfc8aca3 | ||
|
|
7fa97898aa | ||
|
|
3b2d5f02e1 | ||
|
|
34fd833b04 | ||
|
|
6531ec7669 | ||
|
|
728b283063 | ||
|
|
a3dd43b39b | ||
|
|
41a6eb7cf4 | ||
|
|
409b915a3f | ||
|
|
c04133b1df | ||
|
|
d1450a463e | ||
|
|
8daeb56194 | ||
|
|
3b56cdf524 | ||
|
|
d9618880a3 | ||
|
|
aa1b94166c | ||
|
|
307ae4cf66 | ||
|
|
392891f5fc | ||
|
|
58887a5a3b | ||
|
|
0a25e865b9 | ||
|
|
a01c1c2bfa | ||
|
|
3295274711 | ||
|
|
0135c6f743 | ||
|
|
e2a72bf511 | ||
|
|
519b3b9ce1 | ||
|
|
0456f52195 | ||
|
|
b5caf1ad3f | ||
|
|
f65d9b2b7d | ||
|
|
408676913c | ||
|
|
21c842baf6 | ||
|
|
a03886b9e4 | ||
|
|
f7bda4c940 | ||
|
|
abe9b7c08e | ||
|
|
6d4cdc9456 | ||
|
|
5033044587 | ||
|
|
246dd0f8c0 | ||
|
|
fb05bdc2bf | ||
|
|
9948125745 | ||
|
|
b8233ded23 | ||
|
|
6a22b7c26f | ||
|
|
322cfc46d3 | ||
|
|
c9d6a41d75 | ||
|
|
a64c0a2679 | ||
|
|
fdc5aece52 | ||
|
|
685db62029 | ||
|
|
4e643baaa6 | ||
|
|
9634468260 | ||
|
|
00c1b6a2b0 | ||
|
|
b45273d112 | ||
|
|
350adae672 | ||
|
|
bdb3832985 | ||
|
|
1fc8718352 | ||
|
|
d98eec6e91 | ||
|
|
659586947d | ||
|
|
c303ef2a2b | ||
|
|
ff92beb88a | ||
|
|
dce73fde66 | ||
|
|
56bf9c9047 | ||
|
|
a7d48465da | ||
|
|
6ae2c6c7b9 | ||
|
|
ae696d4f30 | ||
|
|
b43ef6440f | ||
|
|
e615aabf14 | ||
|
|
92ae8f4817 | ||
|
|
2268802908 | ||
|
|
261cdf88a5 |
@@ -24,13 +24,7 @@ jobs:
|
||||
name: "test #${{ matrix.python-version }}"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: Ana06/get-changed-files@v2.2.0
|
||||
with:
|
||||
filter: "${{ inputs.working-directory }}/**"
|
||||
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
|
||||
if: steps.changed-files.outputs.all
|
||||
uses: "./.github/actions/poetry_setup"
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
@@ -39,20 +33,17 @@ jobs:
|
||||
cache-key: core
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files.outputs.all
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: poetry install --with dev
|
||||
|
||||
- name: Run core tests
|
||||
if: steps.changed-files.outputs.all
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
make test
|
||||
|
||||
- name: Ensure the tests did not create any additional files
|
||||
if: steps.changed-files.outputs.all
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
|
||||
@@ -59,7 +59,7 @@ from langchain_core.messages import HumanMessage
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.tools import tool
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.graph import END, StateGraph, MessagesState
|
||||
from langgraph.graph import END, START, StateGraph, MessagesState
|
||||
from langgraph.prebuilt import ToolNode
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ workflow.add_node("tools", tool_node)
|
||||
|
||||
# Set the entrypoint as `agent`
|
||||
# This means that this node is the first one called
|
||||
workflow.set_entry_point("agent")
|
||||
workflow.add_edge(START, "agent")
|
||||
|
||||
# We now add a conditional edge
|
||||
workflow.add_conditional_edges(
|
||||
|
||||
@@ -38,6 +38,7 @@ _MANUAL = {
|
||||
"visualization.ipynb",
|
||||
"state-model.ipynb",
|
||||
"subgraph.ipynb",
|
||||
"recursion-limit.ipynb",
|
||||
"force-calling-a-tool-first.ipynb",
|
||||
"pass-run-time-values-to-tools.ipynb",
|
||||
"tool-calling.ipynb",
|
||||
@@ -56,10 +57,13 @@ _MANUAL = {
|
||||
"create-react-agent-memory.ipynb",
|
||||
"create-react-agent-hitl.ipynb",
|
||||
"human_in_the_loop/breakpoints.ipynb",
|
||||
"human_in_the_loop/dynamic_breakpoints.ipynb",
|
||||
"human_in_the_loop/time-travel.ipynb",
|
||||
"human_in_the_loop/edit-graph-state.ipynb",
|
||||
"human_in_the_loop/wait-user-input.ipynb",
|
||||
"human_in_the_loop/review-tool-calls.ipynb",
|
||||
"node-retries.ipynb",
|
||||
"react-agent-structured-output.ipynb",
|
||||
],
|
||||
"tutorials": [
|
||||
"introduction.ipynb",
|
||||
|
||||
@@ -10,7 +10,7 @@ The LangGraph Cloud API consists of a few core data models: [Assistants](#assist
|
||||
|
||||
An assistant is a configured instance of a [`CompiledGraph`][compiledgraph]. It abstracts the cognitive architecture of the graph and contains instance specific configuration and metadata. Multiple assistants can reference the same graph but can contain different configuration and metadata, which may differentiate the behavior of the assistants. An assistant (i.e. the graph) is invoked as part of a run.
|
||||
|
||||
The LangGraph Cloud API provides several endpoints for creating and managing assistants. See the <a href="../reference/api/api_ref.html#tag/assistantscreate" target="_blank">API reference</a> for more details.
|
||||
The LangGraph Cloud API provides several endpoints for creating and managing assistants. See the [API reference](../reference/api/api_ref.html#tag/assistantscreate) for more details.
|
||||
|
||||
#### Configuring Assistants
|
||||
|
||||
@@ -24,13 +24,13 @@ The state of a thread at a particular point in time is called a checkpoint.
|
||||
|
||||
For more on threads and checkpoints, see this section of the [LangGraph conceptual guide](../../concepts/low_level.md#checkpointer).
|
||||
|
||||
The LangGraph Cloud API provides several endpoints for creating and managing threads and thread state. See the <a href="../reference/api/api_ref.html#tag/threadscreate" target="_blank">API reference</a> for more details.
|
||||
The LangGraph Cloud API provides several endpoints for creating and managing threads and thread state. See the [API reference](../reference/api/api_ref.html#tag/threadscreate) for more details.
|
||||
|
||||
### Runs
|
||||
|
||||
A run is an invocation of an assistant. Each run may have its own input, configuration, and metadata, which may affect execution and output of the underlying graph. A run can optionally be executed on a thread.
|
||||
|
||||
The LangGraph Cloud API provides several endpoints for creating and managing runs. See the <a href="../reference/api/api_ref.html#tag/runscreate" target="_blank">API reference</a> for more details.
|
||||
The LangGraph Cloud API provides several endpoints for creating and managing runs. See the [API reference](../reference/api/api_ref.html#tag/runscreate) for more details.
|
||||
|
||||
### Cron Jobs
|
||||
|
||||
@@ -41,7 +41,7 @@ It's often useful to run graphs on some schedule. LangGraph Cloud supports cron
|
||||
|
||||
Note that this sends the same input to the thread every time. See the [how-to guide](../how-tos/cloud_examples/cron_jobs.ipynb) for creating cron jobs.
|
||||
|
||||
The LangGraph Cloud API provides several endpoints for creating and managing cron jobs. See the <a href="../reference/api/api_ref.html#tag/runscreate/POST/threads/{thread_id}/runs/crons" target="_blank">API reference</a> for more details.
|
||||
The LangGraph Cloud API provides several endpoints for creating and managing cron jobs. See the [API reference](../reference/api/api_ref.html#tag/runscreate/POST/threads/{thread_id}/runs/crons) for more details.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -59,7 +59,7 @@ Streaming is critical for making LLM applications feel responsive to end users.
|
||||
|
||||
You can also specify multiple streaming modes at the same time. See the [how-to guide](../how-tos/stream_multiple.md) for configuring multiple streaming modes at the same time.
|
||||
|
||||
See the <a href="../reference/api/api_ref.html#tag/runscreate/POST/threads/{thread_id}/runs/stream" target="_blank">API reference</a> for how to create streaming runs.
|
||||
See the [API reference](../reference/api/api_ref.html#tag/runscreate/POST/threads/{thread_id}/runs/stream) for how to create streaming runs.
|
||||
|
||||
### Human-in-the-Loop
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ In the standard LangGraph API configuration, the server uses the compiled graph
|
||||
|
||||
```python
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.graph import END, MessageGraph
|
||||
from langgraph.graph import END, START, MessageGraph
|
||||
|
||||
model = ChatOpenAI(temperature=0)
|
||||
|
||||
@@ -36,7 +36,7 @@ graph_workflow = MessageGraph()
|
||||
|
||||
graph_workflow.add_node("agent", model)
|
||||
graph_workflow.add_edge("agent", END)
|
||||
graph_workflow.set_entry_point("agent")
|
||||
graph_workflow.add_edge(START, "agent")
|
||||
|
||||
agent = graph_workflow.compile()
|
||||
```
|
||||
@@ -60,7 +60,7 @@ To make your graph rebuild on each new run with custom configuration, you need t
|
||||
```python
|
||||
from typing import Annotated, TypedDict
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.graph import END, MessageGraph
|
||||
from langgraph.graph import END, START, MessageGraph
|
||||
from langgraph.graph.state import StateGraph
|
||||
from langgraph.graph.message import add_messages
|
||||
from langgraph.prebuilt import ToolNode
|
||||
@@ -83,7 +83,7 @@ def make_default_graph():
|
||||
|
||||
graph_workflow.add_node("agent", call_model)
|
||||
graph_workflow.add_edge("agent", END)
|
||||
graph_workflow.set_entry_point("agent")
|
||||
graph_workflow.add_edge(START, "agent")
|
||||
|
||||
agent = graph_workflow.compile()
|
||||
return agent
|
||||
@@ -113,7 +113,7 @@ def make_alternative_graph():
|
||||
graph_workflow.add_node("agent", call_model)
|
||||
graph_workflow.add_node("tools", tool_node)
|
||||
graph_workflow.add_edge("tools", "agent")
|
||||
graph_workflow.set_entry_point("agent")
|
||||
graph_workflow.add_edge(START, "agent")
|
||||
graph_workflow.add_conditional_edges("agent", should_continue)
|
||||
|
||||
agent = graph_workflow.compile()
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
# How to Set Up a LangGraph Application for Deployment
|
||||
|
||||
A LangGraph application must be configured with a [LangGraph API configuration file](../reference/cli.md#configuration-file) in order to be deployed to LangGraph Cloud (or to be self-hosted). This how-to guide discusses the basic steps to setup a LangGraph application for deployment using `requirements.txt` to specify project dependencies.
|
||||
A LangGraph application must be configured with a [LangGraph API configuration file](../reference/cli.md#configuration-file) in order to be deployed to LangGraph Cloud (or to be self-hosted). This how-to guide discusses the basic steps to setup a LangGraph application for deployment using `requirements.txt` to specify project dependencies.
|
||||
|
||||
This walkthrough is based on [this repository](https://github.com/langchain-ai/langgraph-example), which you can play around with to learn more about how to setup your LangGraph application for deployment.
|
||||
|
||||
!!! tip "Setup with pyproject.toml"
|
||||
If you prefer using poetry for dependency management, check out [this how-to guide](./setup_pyproject.md) on using `pyproject.toml` for LangGraph Cloud.
|
||||
If you prefer using poetry for dependency management, check out [this how-to guide](./setup_pyproject.md) on using `pyproject.toml` for LangGraph Cloud.
|
||||
|
||||
!!! tip "Setup with a Monorepo"
|
||||
If you are interested in deploying a graph located inside a monorepo, take a look at [this](https://github.com/langchain-ai/langgraph-example-monorepo) repository for an example of how to do so.
|
||||
|
||||
The final repo structure will look something like this:
|
||||
|
||||
```bash
|
||||
my-app/
|
||||
|-- requirements.txt # package dependencies
|
||||
|-- .env # environment variables
|
||||
|-- openai_agent.py # code for an agent
|
||||
|-- anthropic_agent.py # code for another agent
|
||||
|-- langgraph.json # configuration file for LangGraph
|
||||
├── my_agent # all project code lies within here
|
||||
│ ├── utils # utilities for your graph
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── tools.py # tools for your graph
|
||||
│ │ ├── nodes.py # node functions for you graph
|
||||
│ │ └── state.py # state definition of your graph
|
||||
│ ├── requirements.txt # package dependencies
|
||||
│ ├── __init__.py
|
||||
│ └── agent.py # code for constructing your graph
|
||||
├── .env # environment variables
|
||||
└── langgraph.json # configuration file for LangGraph
|
||||
```
|
||||
|
||||
After each step, an example file directory is provided to demonstrate how code can be organized.
|
||||
@@ -23,31 +34,42 @@ After each step, an example file directory is provided to demonstrate how code c
|
||||
Dependencies can optionally be specified in one of the following files: `pyproject.toml`, `setup.py`, or `requirements.txt`. If none of these files is created, then dependencies can be specified later in the [LangGraph API configuration file](#create-langgraph-api-config).
|
||||
|
||||
The dependencies below will be included in the image, you can also use them in your code, as long as with a compatible version range:
|
||||
|
||||
```
|
||||
langgraph>=0.1.19,<0.2.0
|
||||
langchain-core>=0.2.8,<0.3.0
|
||||
langgraph>=0.2.7,<0.3.0
|
||||
langgraph-checkpoint>=1.0.4
|
||||
langchain-core>=0.2.27,<0.3.0
|
||||
langsmith>=0.1.63
|
||||
orjson>=3.10.1
|
||||
httpx>=0.27.0
|
||||
tenacity>=8.3.0
|
||||
uvicorn>=0.29.0
|
||||
orjson>=3.9.7
|
||||
httpx>=0.25.0
|
||||
tenacity>=8.0.0
|
||||
uvicorn>=0.26.0
|
||||
sse-starlette>=2.1.0
|
||||
uvloop>=0.19.0
|
||||
httptools>=0.6.1
|
||||
jsonschema-rs>=0.18.0
|
||||
uvloop>=0.18.0
|
||||
httptools>=0.5.0
|
||||
jsonschema-rs>=0.16.3
|
||||
croniter>=1.0.1
|
||||
structlog>=23.1.0
|
||||
redis>=5.0.0,<6.0.0
|
||||
```
|
||||
|
||||
Example `requirements.txt` file:
|
||||
|
||||
```
|
||||
langgraph
|
||||
langchain_anthropic
|
||||
tavily-python
|
||||
langchain_community
|
||||
langchain_openai
|
||||
|
||||
```
|
||||
|
||||
Example file directory:
|
||||
```
|
||||
|
||||
```bash
|
||||
my-app/
|
||||
|-- requirements.txt # Python packages required for your graph
|
||||
├── my_agent # all project code lies within here
|
||||
│ └── requirements.txt # package dependencies
|
||||
```
|
||||
|
||||
## Specify Environment Variables
|
||||
@@ -55,6 +77,7 @@ my-app/
|
||||
Environment variables can optionally be specified in a file (e.g. `.env`). See the [Environment Variables reference](../reference/env_var.md) to configure additional variables for a deployment.
|
||||
|
||||
Example `.env` file:
|
||||
|
||||
```
|
||||
MY_ENV_VAR_1=foo
|
||||
MY_ENV_VAR_2=bar
|
||||
@@ -62,42 +85,66 @@ OPENAI_API_KEY=key
|
||||
```
|
||||
|
||||
Example file directory:
|
||||
```
|
||||
|
||||
```bash
|
||||
my-app/
|
||||
|-- requirements.txt
|
||||
|-- .env # file with environment variables
|
||||
├── my_agent # all project code lies within here
|
||||
│ └── requirements.txt # package dependencies
|
||||
└── .env # environment variables
|
||||
```
|
||||
|
||||
## Define Graphs
|
||||
|
||||
Implement your graphs! Graphs can be defined in a single file or multiple files. Make note of the variable names of each [CompiledGraph][compiledgraph] to be included in the LangGraph application. The variable names will be used later when creating the [LangGraph API configuration file](../reference/cli.md#configuration-file).
|
||||
|
||||
Example `openai_agent.py` file:
|
||||
Example `agent.py` file, which shows how to import from other modules you define (code for the modules is not shown here, please see [this repo](https://github.com/langchain-ai/langgraph-example) to see their implementation):
|
||||
|
||||
```python
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.graph import END, MessageGraph
|
||||
# my_agent/agent.py
|
||||
from typing import TypedDict, Literal
|
||||
|
||||
model = ChatOpenAI(temperature=0)
|
||||
from langgraph.graph import StateGraph, END, START
|
||||
from my_agent.utils.nodes import call_model, should_continue, tool_node # import nodes
|
||||
from my_agent.utils.state import AgentState # import state
|
||||
|
||||
graph_workflow = MessageGraph()
|
||||
# Define the config
|
||||
class GraphConfig(TypedDict):
|
||||
model_name: Literal["anthropic", "openai"]
|
||||
|
||||
graph_workflow.add_node("agent", model)
|
||||
graph_workflow.add_edge("agent", END)
|
||||
graph_workflow.set_entry_point("agent")
|
||||
workflow = StateGraph(AgentState, config_schema=GraphConfig)
|
||||
workflow.add_node("agent", call_model)
|
||||
workflow.add_node("action", tool_node)
|
||||
workflow.add_edge(START, "agent")
|
||||
workflow.add_conditional_edges(
|
||||
"agent",
|
||||
should_continue,
|
||||
{
|
||||
"continue": "action",
|
||||
"end": END,
|
||||
},
|
||||
)
|
||||
workflow.add_edge("action", "agent")
|
||||
|
||||
agent = graph_workflow.compile()
|
||||
graph = workflow.compile()
|
||||
```
|
||||
|
||||
!!! warning "Assign `CompiledGraph` to Variable"
|
||||
The build process for LangGraph Cloud requires that the `CompiledGraph` object be assigned to a variable at the top-level of a Python module (alternatively, you can provide [a function that creates a graph](./graph_rebuild.md)).
|
||||
The build process for LangGraph Cloud requires that the `CompiledGraph` object be assigned to a variable at the top-level of a Python module (alternatively, you can provide [a function that creates a graph](./graph_rebuild.md)).
|
||||
|
||||
Example file directory:
|
||||
```
|
||||
|
||||
```bash
|
||||
my-app/
|
||||
|-- requirements.txt
|
||||
|-- .env
|
||||
|-- openai_agent.py # code for your graph
|
||||
|-- anthropic_agent.py # code for your graph
|
||||
├── my_agent # all project code lies within here
|
||||
│ ├── utils # utilities for your graph
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── tools.py # tools for your graph
|
||||
│ │ ├── nodes.py # node functions for you graph
|
||||
│ │ └── state.py # state definition of your graph
|
||||
│ ├── requirements.txt # package dependencies
|
||||
│ ├── __init__.py
|
||||
│ └── agent.py # code for constructing your graph
|
||||
└── .env # environment variables
|
||||
```
|
||||
|
||||
## Create LangGraph API Config
|
||||
@@ -105,33 +152,37 @@ my-app/
|
||||
Create a [LangGraph API configuration file](../reference/cli.md#configuration-file) called `langgraph.json`. See the [LangGraph CLI reference](../reference/cli.md#configuration-file) for detailed explanations of each key in the JSON object of the configuration file.
|
||||
|
||||
Example `langgraph.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": [
|
||||
"."
|
||||
],
|
||||
"graphs": {
|
||||
"openai_agent": "./openai_agent.py:agent",
|
||||
"anthropic_agent": "./anthropic_agent.py:agent"
|
||||
},
|
||||
"env": "./.env"
|
||||
"dependencies": ["./my_agent"],
|
||||
"graphs": {
|
||||
"agent": "./my_agent/agent.py:graph"
|
||||
},
|
||||
"env": ".env"
|
||||
}
|
||||
```
|
||||
|
||||
Note that the variable name of the `CompiledGraph` appears at the end of the value of each subkey in the top-level `graphs` key (i.e. `:<variable_name>`).
|
||||
|
||||
!!! warning "Configuration Location"
|
||||
The LangGraph API configuration file must be placed in a directory that is at the same level or higher than the Python files that contain compiled graphs and associated dependencies.
|
||||
The LangGraph API configuration file must be placed in a directory that is at the same level or higher than the Python files that contain compiled graphs and associated dependencies.
|
||||
|
||||
Example file directory:
|
||||
|
||||
```bash
|
||||
my-app/
|
||||
|-- requirements.txt
|
||||
|-- .env
|
||||
|-- openai_agent.py
|
||||
|-- anthropic_agent.py
|
||||
|-- langgraph.json # configuration file for LangGraph
|
||||
├── my_agent # all project code lies within here
|
||||
│ ├── utils # utilities for your graph
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── tools.py # tools for your graph
|
||||
│ │ ├── nodes.py # node functions for you graph
|
||||
│ │ └── state.py # state definition of your graph
|
||||
│ ├── requirements.txt # package dependencies
|
||||
│ ├── __init__.py
|
||||
│ └── agent.py # code for constructing your graph
|
||||
├── .env # environment variables
|
||||
└── langgraph.json # configuration file for LangGraph
|
||||
```
|
||||
|
||||
## Next
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
# How to Set Up a LangGraph Application for Deployment
|
||||
|
||||
A LangGraph application must be configured with a [LangGraph API configuration file](../reference/cli.md#configuration-file) in order to be deployed to LangGraph Cloud (or to be self-hosted). This how-to guide discusses the basic steps to setup a LangGraph application for deployment using `pyproject.toml` to define your package's dependencies. If you prefer using `requirements.txt` for dependency management, check out [this how-to guide](./setup.md).
|
||||
A LangGraph application must be configured with a [LangGraph API configuration file](../reference/cli.md#configuration-file) in order to be deployed to LangGraph Cloud (or to be self-hosted). This how-to guide discusses the basic steps to setup a LangGraph application for deployment using `pyproject.toml` to define your package's dependencies.
|
||||
|
||||
This walkthrough is based on [this repository](https://github.com/langchain-ai/langgraph-example), which you can play around with to learn more about how to setup your LangGraph application for deployment.
|
||||
|
||||
!!! tip "Setup with requirements.txt"
|
||||
If you prefer using `requirements.txt` for dependency management, check out [this how-to guide](./setup.md).
|
||||
|
||||
!!! tip "Setup with a Monorepo"
|
||||
If you are interested in deploying a graph located inside a monorepo, take a look at [this](https://github.com/langchain-ai/langgraph-example-monorepo) repository for an example of how to do so.
|
||||
|
||||
The final repo structure will look something like this:
|
||||
|
||||
```bash
|
||||
my-app/
|
||||
├── my_agent # all project code lies within here
|
||||
│ ├── utils # utilities for your graph
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── tools.py # tools for your graph
|
||||
│ │ ├── nodes.py # node functions for you graph
|
||||
│ │ └── state.py # state definition of your graph
|
||||
│ ├── __init__.py
|
||||
│ └── agent.py # code for your graph
|
||||
│-- .env # environment variables
|
||||
│-- langgraph.json # configuration file for LangGraph
|
||||
│ └── agent.py # code for constructing your graph
|
||||
├── .env # environment variables
|
||||
├── langgraph.json # configuration file for LangGraph
|
||||
└── pyproject.toml # dependencies for your project
|
||||
```
|
||||
|
||||
@@ -22,18 +35,21 @@ Dependencies can optionally be specified in one of the following files: `pyproje
|
||||
|
||||
The dependencies below will be included in the image, you can also use them in your code, as long as with a compatible version range:
|
||||
```
|
||||
langgraph>=0.1.19,<0.2.0
|
||||
langchain-core>=0.2.8,<0.3.0
|
||||
langgraph>=0.2.7,<0.3.0
|
||||
langgraph-checkpoint>=1.0.4
|
||||
langchain-core>=0.2.27,<0.3.0
|
||||
langsmith>=0.1.63
|
||||
orjson>=3.10.1
|
||||
httpx>=0.27.0
|
||||
tenacity>=8.3.0
|
||||
uvicorn>=0.29.0
|
||||
orjson>=3.9.7
|
||||
httpx>=0.25.0
|
||||
tenacity>=8.0.0
|
||||
uvicorn>=0.26.0
|
||||
sse-starlette>=2.1.0
|
||||
uvloop>=0.19.0
|
||||
httptools>=0.6.1
|
||||
jsonschema-rs>=0.18.0
|
||||
uvloop>=0.18.0
|
||||
httptools>=0.5.0
|
||||
jsonschema-rs>=0.16.3
|
||||
croniter>=1.0.1
|
||||
structlog>=24.4.0
|
||||
redis>=5.0.8,<6.0.0
|
||||
```
|
||||
|
||||
Example `pyproject.toml` file:
|
||||
@@ -49,7 +65,7 @@ readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9.0,<3.13"
|
||||
langgraph = "^0.1.7"
|
||||
langgraph = "^0.2.0"
|
||||
langchain-fireworks = "^0.1.3"
|
||||
|
||||
|
||||
@@ -62,9 +78,6 @@ Example file directory:
|
||||
|
||||
```bash
|
||||
my-app/
|
||||
├── my_agent
|
||||
│ ├── __init__.py
|
||||
│ └── agent.py
|
||||
└── pyproject.toml # Python packages required for your graph
|
||||
```
|
||||
|
||||
@@ -84,10 +97,7 @@ Example file directory:
|
||||
|
||||
```bash
|
||||
my-app/
|
||||
├── my_agent
|
||||
│ ├── __init__.py
|
||||
│ └── agent.py
|
||||
|-- .env # file with environment variables
|
||||
├── .env # file with environment variables
|
||||
└── pyproject.toml
|
||||
```
|
||||
|
||||
@@ -95,26 +105,35 @@ my-app/
|
||||
|
||||
Implement your graphs! Graphs can be defined in a single file or multiple files. Make note of the variable names of each [CompiledGraph][compiledgraph] to be included in the LangGraph application. The variable names will be used later when creating the [LangGraph API configuration file](../reference/cli.md#configuration-file).
|
||||
|
||||
Example `agent.py` file:
|
||||
Example `agent.py` file, which shows how to import from other modules you define (code for the modules is not shown here, please see [this repo](https://github.com/langchain-ai/langgraph-example-pyproject) to see their implementation):
|
||||
|
||||
```python
|
||||
# my_agent/agent.py
|
||||
from langchain_fireworks import ChatFireworks
|
||||
from langgraph.graph import END, StateGraph, add_messages
|
||||
from typing_extensions import TypedDict, Annotated
|
||||
from typing import TypedDict, Literal
|
||||
|
||||
model = ChatFireworks(model="accounts/fireworks/models/firefunction-v2", temperature=0)
|
||||
from langgraph.graph import StateGraph, END, START
|
||||
from my_agent.utils.nodes import call_model, should_continue, tool_node # import nodes
|
||||
from my_agent.utils.state import AgentState # import state
|
||||
|
||||
class State(TypedDict):
|
||||
messages: Annotated[list, add_messages]
|
||||
# Define the config
|
||||
class GraphConfig(TypedDict):
|
||||
model_name: Literal["anthropic", "openai"]
|
||||
|
||||
graph_workflow = StateGraph(State)
|
||||
workflow = StateGraph(AgentState, config_schema=GraphConfig)
|
||||
workflow.add_node("agent", call_model)
|
||||
workflow.add_node("action", tool_node)
|
||||
workflow.add_edge(START, "agent")
|
||||
workflow.add_conditional_edges(
|
||||
"agent",
|
||||
should_continue,
|
||||
{
|
||||
"continue": "action",
|
||||
"end": END,
|
||||
},
|
||||
)
|
||||
workflow.add_edge("action", "agent")
|
||||
|
||||
graph_workflow.add_node("agent", model)
|
||||
graph_workflow.add_edge("agent", END)
|
||||
graph_workflow.set_entry_point("agent")
|
||||
|
||||
agent = graph_workflow.compile()
|
||||
graph = workflow.compile()
|
||||
```
|
||||
|
||||
!!! warning "Assign `CompiledGraph` to Variable"
|
||||
@@ -124,10 +143,15 @@ Example file directory:
|
||||
|
||||
```bash
|
||||
my-app/
|
||||
├── my_agent
|
||||
├── my_agent # all project code lies within here
|
||||
│ ├── utils # utilities for your graph
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── tools.py # tools for your graph
|
||||
│ │ ├── nodes.py # node functions for you graph
|
||||
│ │ └── state.py # state definition of your graph
|
||||
│ ├── __init__.py
|
||||
│ └── agent.py # code for your graph
|
||||
|-- .env
|
||||
│ └── agent.py # code for constructing your graph
|
||||
├── .env
|
||||
└── pyproject.toml
|
||||
```
|
||||
|
||||
@@ -141,9 +165,9 @@ Example `langgraph.json` file:
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"my_fantastic_agent": "./my_agent/agent.py:agent"
|
||||
"agent": "./my_agent/agent.py:graph"
|
||||
},
|
||||
"env": "./.env"
|
||||
"env": ".env"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -156,12 +180,17 @@ Example file directory:
|
||||
|
||||
```bash
|
||||
my-app/
|
||||
├── my_agent
|
||||
├── my_agent # all project code lies within here
|
||||
│ ├── utils # utilities for your graph
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── tools.py # tools for your graph
|
||||
│ │ ├── nodes.py # node functions for you graph
|
||||
│ │ └── state.py # state definition of your graph
|
||||
│ ├── __init__.py
|
||||
│ └── agent.py # code for your graph
|
||||
│-- .env
|
||||
│-- langgraph.json # configuration file for LangGraph
|
||||
└── pyproject.toml
|
||||
│ └── agent.py # code for constructing your graph
|
||||
├── .env # environment variables
|
||||
├── langgraph.json # configuration file for LangGraph
|
||||
└── pyproject.toml # dependencies for your project
|
||||
```
|
||||
|
||||
## Next
|
||||
|
||||
@@ -38,6 +38,46 @@ Ready!
|
||||
|
||||
We can now interact with the API server using the LangGraph SDK. First, we need to start our client, select our assistant (in this case a graph we called "agent", make sure to select the proper assistant you wish to test).
|
||||
|
||||
You can either initialize by passing authentication or by setting an environment variable.
|
||||
|
||||
#### Initialize with authentication
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
# only pass the url argument to get_client() if you changed the default port when calling langgraph up
|
||||
client = get_client(url=<DEPLOYMENT_URL>,api_key=<LANGCHAIN_API_KEY>)
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
// only set the apiUrl if you changed the default port when calling langgraph up
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL>, apiKey: <LANGCHAIN_API_KEY> });
|
||||
const assistantId = "agent"
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json'
|
||||
--header 'x-api-key: <LANGCHAIN_API_KEY>'
|
||||
```
|
||||
|
||||
|
||||
#### Initialize with environment variables
|
||||
|
||||
If you have a `LANGCHAIN_API_KEY` set in your environment, you do not need to explicitly pass authentication to the client
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
@@ -60,6 +100,14 @@ We can now interact with the API server using the LangGraph SDK. First, we need
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
Now we can invoke our graph to ensure it is working. Make sure to change the input to match the proper schema for your graph.
|
||||
|
||||
=== "Python"
|
||||
@@ -96,4 +144,39 @@ Now we can invoke our graph to ensure it is working. Make sure to change the inp
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf\"}]},
|
||||
\"stream_mode\": [
|
||||
\"events\"
|
||||
]
|
||||
}" | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "Receiving event of type: ", $0)
|
||||
printf "%s...\n", $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
If your graph works correctly, you should see your graph output displayed in the console. Of course, there are many more ways you might need to test your graph, for a full list of commands you can send with the SDK, see the [Python](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/python_sdk_ref/) and [JS/TS](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/js_ts_sdk_ref/) references.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Studio FAQs
|
||||
|
||||
## Why is my project failing to start?
|
||||
|
||||
There are a few reasons that your project might fail to start, here are some of the most common ones.
|
||||
|
||||
### Docker issues
|
||||
|
||||
LangGraph Studio requires Docker Desktop version 4.24 or higher. Please make sure you have a version of Docker installed that satisfies that requirement and also make sure you have the Docker Desktop app up and running before trying to use LangGraph Studio. In addition, make sure you have docker-compose updated to version 2.22.0 or higher.
|
||||
|
||||
### Configuration or environment issues
|
||||
|
||||
Another reason your project might fail to start is because your configuration file is defined incorrectly, or you are missing required environment variables.
|
||||
|
||||
## How does interrupt work?
|
||||
|
||||
When you select the `Interrupts` dropdown and select a node to interrupt the graph will pause execution before and after (unless the node goes straight to `END`) that node has run. This means that you will be able to both edit the state before the node is ran and the state after the node has ran. This is intended to allow developers more fine-grained control over the behavior of a node and make it easier to observe how the node is behaving. You will not be able to edit the state after the node has ran if the node is the final node in the graph.
|
||||
|
||||
## How do I reload the app?
|
||||
|
||||
If you would like to reload the app, don't use Command+R as you might normally do. Instead, close and reopen the app for a full refresh.
|
||||
|
||||
## How does automatic rebuilding work?
|
||||
|
||||
One of the key features of LangGraph Studio is that it automatically rebuilds your image when you change the source code. This allows for a super fast development and testing cycle which makes it easy to iterate on your graph. There are two different ways that LangGraph rebuilds your image: either by editing the image or completely rebuilding it.
|
||||
|
||||
### Rebuilds from source code changes
|
||||
|
||||
If you modified the source code only (no configuration or dependency changes!) then the image does not require a full rebuild, and LangGraph Studio will only update the relevant parts. The UI status in the bottom left will switch from `Online` to `Stopping` temporarily while the image gets edited. The logs will be shown as this process is happening, and after the image has been edited the status will change back to `Online` and you will be able to run your graph with the modified code!
|
||||
|
||||
|
||||
### Rebuilds from configuration or dependency changes
|
||||
|
||||
If you edit your graph configuration file (`langgraph.json`) or the dependencies (either `pyproject.toml` or `requirements.txt`) then the entire image will be rebuilt. This will cause the UI to switch away from the graph view and start showing the logs of the new image building process. This can take a minute or two, and once it is done your updated image will be ready to use!
|
||||
|
||||
## Why is my graph taking so long to startup?
|
||||
|
||||
The LangGraph Studio interacts with a local LangGraph API server. To stay aligned with ongoing updates, the LangGraph API requires regular rebuilding. As a result, you may occasionally experience slight delays when starting up your project.
|
||||
|
||||
## Why are extra edges showing up in my graph?
|
||||
|
||||
If you don't define your conditional edges carefully, you might notice extra edges appearing in your graph. This is because without proper definition, LangGraph Studio assumes the conditional edge could access all other nodes. In order for this to not be the case, you need to be explicit about how you define the nodes the conditional edge routes to. There are two ways you can do this:
|
||||
|
||||
### Solution 1: Include a path map
|
||||
|
||||
The first way to solve this is to add path maps to your conditional edges. A path map is just a dictionary that maps the possible outputs of your router function with the names of the nodes that each output corresponds to. The path map is passed as the third argument to the `add_conditional_edges` function like so:
|
||||
|
||||
```python
|
||||
graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"})
|
||||
```
|
||||
|
||||
In this case, the routing function returns either True or False, which map to `node_b` and `node_c` respectively.
|
||||
|
||||
### Solution 2: Update the typing of the router
|
||||
|
||||
Instead of passing a path map, you can also be explicit about the typing of your routing function by specifying the nodes it can map to using the `Literal` python definition. Here is an example of how to define a routing function in that way:
|
||||
|
||||
```python
|
||||
def routing_function(state: GraphState) -> Literal["node_b","node_c"]:
|
||||
if state['some_condition'] == True:
|
||||
return "node_a"
|
||||
else:
|
||||
return "node_b"
|
||||
```
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
# Check the Status of your Threads
|
||||
|
||||
## Setup
|
||||
|
||||
To start, we can setup our client with whatever URL you are hosting your graph from:
|
||||
|
||||
### SDK initialization
|
||||
|
||||
First, we need to setup our client so that we can communicate with our hosted graph:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
const assistantId = agent;
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
## Find idle threads
|
||||
|
||||
We can use the following commands to find threads that are idle, which means that all runs executed on the thread have finished running:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
print(await client.threads.search(status="idle",limit=1))
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
console.log(await client.threads.search({status: "idle",limit:1}));
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/search \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"status": "idle", "limit": 1}'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
[{'thread_id': 'cacf79bb-4248-4d01-aabc-938dbd60ed2c',
|
||||
'created_at': '2024-08-14T17:36:38.921660+00:00',
|
||||
'updated_at': '2024-08-14T17:36:38.921660+00:00',
|
||||
'metadata': {'graph_id': 'agent'},
|
||||
'status': 'idle',
|
||||
'config': {'configurable': {}}}]
|
||||
|
||||
|
||||
## Find interrupted threads
|
||||
|
||||
We can use the following commands to find threads that have been interrupted in the middle of a run, which could either mean an error occurred before the run finished or a human-in-the-loop breakpoint was reached and the run is waiting to continue:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
print(await client.threads.search(status="interrupted",limit=1))
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
console.log(await client.threads.search({status: "interrupted",limit:1}));
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/search \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"status": "interrupted", "limit": 1}'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
[{'thread_id': '0d282b22-bbd5-4d95-9c61-04dcc2e302a5',
|
||||
'created_at': '2024-08-14T17:41:50.235455+00:00',
|
||||
'updated_at': '2024-08-14T17:41:50.235455+00:00',
|
||||
'metadata': {'graph_id': 'agent'},
|
||||
'status': 'interrupted',
|
||||
'config': {'configurable': {}}}]
|
||||
|
||||
## Find busy threads
|
||||
|
||||
We can use the following commands to find threads that are busy, meaning they are currently handling the execution of a run:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
print(await client.threads.search(status="busy",limit=1))
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
console.log(await client.threads.search({status: "busy",limit: 1}));
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/search \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"status": "busy", "limit": 1}'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
[{'thread_id': '0d282b22-bbd5-4d95-9c61-04dcc2e302a5',
|
||||
'created_at': '2024-08-14T17:41:50.235455+00:00',
|
||||
'updated_at': '2024-08-14T17:41:50.235455+00:00',
|
||||
'metadata': {'graph_id': 'agent'},
|
||||
'status': 'busy',
|
||||
'config': {'configurable': {}}}]
|
||||
|
||||
## Find specific threads
|
||||
|
||||
You may also want to check the status of specific threads, which you can do in a few ways:
|
||||
|
||||
### Find by ID
|
||||
|
||||
You can use the `get` function to find the status of a specific thread, as long as you have the ID saved
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
print((await client.threads.get(<THREAD_ID>))['status'])
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
console.log((await client.threads.get(<THREAD_ID>)).status);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID> \
|
||||
--header 'Content-Type: application/json' | jq -r '.status'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
'idle'
|
||||
|
||||
### Find by metadata
|
||||
|
||||
The search endpoint for threads also allows you to filter on metadata, which can be helpful if you use metadata to tag threads in order to keep them organized:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
print((await client.threads.search(metadata={"foo":"bar"},limit=1))[0]['status'])
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
console.log((await client.threads.search({metadata: {"foo":"bar"},limit: 1}))[0].status);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/search \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"metadata": {"foo":"bar"}, "limit": 1}' | jq -r '.[0].status'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
'idle'
|
||||
@@ -0,0 +1,132 @@
|
||||
# Copying Threads
|
||||
|
||||
You may wish to copy (i.e. "fork") an existing thread in order to keep the existing thread's history and create independent runs that do not affect the original thread. This guide shows how you can do that.
|
||||
|
||||
## Setup
|
||||
|
||||
This code assumes you already have a thread to copy. You can read about what a thread is [here](https://langchain-ai.github.io/langgraph/cloud/concepts/api/#threads) and learn how to stream a run on a thread in [these how-to guides](https://langchain-ai.github.io/langgraph/cloud/how-tos/#streaming).
|
||||
|
||||
### SDK initialization
|
||||
|
||||
First, we need to setup our client so that we can communicate with our hosted graph:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
client = get_client(url="<DEPLOYMENT_URL>")
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl:"<DEPLOYMENT_URL>" });
|
||||
const assistantId = agent;
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"metadata": {}
|
||||
}'
|
||||
```
|
||||
|
||||
## Copying a thread
|
||||
|
||||
The code below assumes that a thread you'd like to copy already exists.
|
||||
|
||||
Copying a thread will create a new thread with the same history as the existing thread, and then allow you to continue executing runs.
|
||||
|
||||
### Create copy
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
copied_thread = await client.threads.copy(<THREAD_ID>)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
let copiedThread = await client.threads.copy(<THREAD_ID>);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/copy \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
### Verify copy
|
||||
|
||||
We can verify that the history from the prior thread did indeed copy over correctly:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
def remove_thread_id(d):
|
||||
if 'metadata' in d and 'thread_id' in d['metadata']:
|
||||
del d['metadata']['thread_id']
|
||||
return d
|
||||
|
||||
original_thread_history = list(map(remove_thread_id,await client.threads.get_history(<THREAD_ID>)))
|
||||
copied_thread_history = list(map(remove_thread_id,await client.threads.get_history(copied_thread['thread_id'])))
|
||||
|
||||
# Compare the two histories
|
||||
assert original_thread_history == copied_thread_history
|
||||
# if we made it here the assertion passed!
|
||||
print("The histories are the same.")
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
function removeThreadId(d) {
|
||||
if (d.metadata && d.metadata.thread_id) {
|
||||
delete d.metadata.thread_id;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
// Assuming `client.threads.getHistory(threadId)` is an async function that returns a list of dicts
|
||||
async function compareThreadHistories(threadId, copiedThreadId) {
|
||||
const originalThreadHistory = (await client.threads.getHistory(threadId)).map(removeThreadId);
|
||||
const copiedThreadHistory = (await client.threads.getHistory(copiedThreadId)).map(removeThreadId);
|
||||
|
||||
// Compare the two histories
|
||||
console.assert(JSON.stringify(originalThreadHistory) === JSON.stringify(copiedThreadHistory))
|
||||
// if we made it here the assertion passed!
|
||||
console.log("The histories are the same.");
|
||||
}
|
||||
|
||||
// Example usage
|
||||
compareThreadHistories(<THREAD_ID>, copiedThread.thread_id);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
if diff <(
|
||||
curl --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/history | jq -S 'map(del(.metadata.thread_id))'
|
||||
) <(
|
||||
curl --request GET --url <DEPLOYMENT_URL>/threads/<COPIED_THREAD_ID>/history | jq -S 'map(del(.metadata.thread_id))'
|
||||
) >/dev/null; then
|
||||
echo "The histories are the same."
|
||||
else
|
||||
echo "The histories are different."
|
||||
fi
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
The histories are the same.
|
||||
@@ -31,7 +31,7 @@ Then, let's import our required packages and instantiate our client, assistant,
|
||||
from langchain_core.messages import convert_to_messages
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url="whatever-your-deployment-url-is")
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
@@ -42,7 +42,7 @@ Then, let's import our required packages and instantiate our client, assistant,
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
|
||||
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
const assistantId = "agent";
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
@@ -21,7 +21,7 @@ In this how-to we use a simple ReAct style hosted graph (you can see the full co
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
client = get_client(url="whatever-your-deployment-url-is")
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
@@ -31,11 +31,19 @@ In this how-to we use a simple ReAct style hosted graph (you can see the full co
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
const assistantId = "agent"
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
## Adding a breakpoint
|
||||
|
||||
We now want to add a breakpoint in our graph run, which we will do before a tool is called.
|
||||
@@ -82,6 +90,42 @@ And, now let's compile it with a breakpoint before the tool node:
|
||||
console.log("\n\n");
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf\"}]},
|
||||
\"interrupt_before\": [\"action\"],
|
||||
\"stream_mode\": [
|
||||
\"messages\"
|
||||
]
|
||||
}" | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "Receiving event of type: ", $0)
|
||||
printf "%s...\n", $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ First, we need to setup our client so that we can communicate with our hosted gr
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
client = get_client(url="whatever-your-deployment-url-is")
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
@@ -27,11 +27,19 @@ First, we need to setup our client so that we can communicate with our hosted gr
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
const assistantId = "agent";
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
## Editing state
|
||||
|
||||
### Initial invocation
|
||||
@@ -75,6 +83,42 @@ Now let's invoke our graph, making sure to interrupt before the `action` node.
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"search for weather in SF\"}]},
|
||||
\"interrupt_before\": [\"action\"],
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
]
|
||||
}" | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "", $0)
|
||||
event_type = $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'agent': {'messages': [{'content': [{'text': "Certainly! I'll search for the current weather in San Francisco for you using the search function. Here's how I'll do that:", 'type': 'text'}, {'id': 'toolu_01KEJMBFozSiZoS4mAcPZeqQ', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-6dbb0167-f8f6-4e2a-ab68-229b2d1fbb64', 'example': False, 'tool_calls': [{'name': 'search', 'args': {'query': 'current weather in San Francisco'}, 'id': 'toolu_01KEJMBFozSiZoS4mAcPZeqQ'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
|
||||
@@ -129,10 +173,22 @@ Now, let's assume we actually meant to search for the weather in Sidi Frej (anot
|
||||
await client.threads.updateState(thread['thread_id'], {values:{"messages": lastMessage}});
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state | \
|
||||
jq '.values.messages[-1] | (.tool_calls[0].args = {"query": "current weather in Sidi Frej"})' | \
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data @-
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'configurable': {'thread_id': '88d58d3f-4151-47a9-a8e0-e42fdd3527b8',
|
||||
'thread_ts': '1ef3274b-a809-6913-8002-91536ce6554d'}}
|
||||
{'configurable': {'thread_id': '9c8f1a43-9dd8-4017-9271-2c53e57cf66a',
|
||||
'checkpoint_ns': '',
|
||||
'checkpoint_id': '1ef58e7e-3641-649f-8002-8b4305a64858'}}
|
||||
|
||||
|
||||
|
||||
@@ -171,6 +227,40 @@ Now we can resume our graph run but with the updated state:
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
]
|
||||
}"| \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "", $0)
|
||||
event_type = $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'action': {'messages': [{'content': '["I looked up: current weather in Sidi Frej. Result: It\'s sunny in San Francisco, but you better look out if you\'re a Gemini 😈."]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': '1161b8d1-bee4-4188-9be8-698aecb69f10', 'tool_call_id': 'toolu_01KEJMBFozSiZoS4mAcPZeqQ'}]}}
|
||||
|
||||
@@ -0,0 +1,575 @@
|
||||
# Review Tool Calls
|
||||
|
||||
Human-in-the-loop (HIL) interactions are crucial for [agentic systems](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#human-in-the-loop). A common pattern is to add some human in the loop step after certain tool calls. These tool calls often lead to either a function call or saving of some information. Examples include:
|
||||
|
||||
- A tool call to execute SQL, which will then be run by the tool
|
||||
- A tool call to generate a summary, which will then be saved to the State of the graph
|
||||
|
||||
Note that using tool calls is common **whether actually calling tools or not**.
|
||||
|
||||
There are typically a few different interactions you may want to do here:
|
||||
|
||||
1. Approve the tool call and continue
|
||||
2. Modify the tool call manually and then continue
|
||||
3. Give natural language feedback, and then pass that back to the agent instead of continuing
|
||||
|
||||
We can implement this in LangGraph using a [breakpoint](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/breakpoints/): breakpoints allow us to interrupt graph execution before a specific step. At this breakpoint, we can manually update the graph state taking one of the three options above
|
||||
|
||||
## Setup
|
||||
|
||||
We are not going to show the full code for the graph we are hosting, but you can see it [here](../../how-tos/human_in_the_loop/review-tool-calls.ipynb#simple-usage) if you want to. Once this graph is hosted, we are ready to invoke it and wait for user input.
|
||||
|
||||
### SDK initialization
|
||||
|
||||
First, we need to setup our client so that we can communicate with our hosted graph:
|
||||
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
const assistantId = "agent";
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
## Example with no review
|
||||
|
||||
Let's look at an example when no review is required (because no tools are called)
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
input = { 'messages':[{ "role":"user", "content":"hi!" }] }
|
||||
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistant_id,
|
||||
input=input,
|
||||
stream_mode="updates",
|
||||
interrupt_before=["action"],
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const input = {"messages": [{ "role": "human", "content": "hi!"}] }
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
input: input,
|
||||
streamMode: "updates",
|
||||
interruptBefore: ["action"],
|
||||
}
|
||||
);
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'messages': [{'content': 'hi!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '39c51f14-2d5c-4690-883a-d940854b1845', 'example': False}]}
|
||||
{'messages': [{'content': 'hi!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '39c51f14-2d5c-4690-883a-d940854b1845', 'example': False}, {'content': [{'text': "Hello! Welcome. How can I assist you today? Is there anything specific you'd like to know or any information you're looking for?", 'type': 'text', 'index': 0}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-d65e07fb-43ff-4d98-ab6b-6316191b9c8b', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 355, 'output_tokens': 31, 'total_tokens': 386}}]}
|
||||
|
||||
|
||||
If we check the state, we can see that it is finished
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
state = await client.threads.get_state(thread["thread_id"])
|
||||
|
||||
print(state['next'])
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const state = await client.threads.getState(thread["thread_id"]);
|
||||
|
||||
console.log(state.next);
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
[]
|
||||
|
||||
## Example of approving tool
|
||||
|
||||
Let's now look at what it looks like to approve a tool call. Note that we don't need to pass an interrupt to our streaming calls because the graph (defined [here](../../how-tos/human_in_the_loop/review-tool-calls.ipynb#simple-usage)) was already compiled with an interrupt before the `human_review_node`.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]}
|
||||
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
"agent",
|
||||
input=input,
|
||||
stream_mode="values",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]}
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
input: input,
|
||||
streamMode: "values",
|
||||
}
|
||||
);
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '54e19d6e-89fa-44fb-b92c-12e7dd4ddf08', 'example': False}]}
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '54e19d6e-89fa-44fb-b92c-12e7dd4ddf08', 'example': False}, {'content': [{'text': "Certainly! I can help you check the weather in San Francisco. To get this information, I'll use the weather search function. Let me do that for you right away.", 'type': 'text', 'index': 0}, {'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-45a6b6c3-ac69-42a4-8957-d982203d6392', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 90, 'total_tokens': 450}}]}
|
||||
|
||||
|
||||
If we now check, we can see that it is waiting on human review:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
state = await client.threads.get_state(thread["thread_id"])
|
||||
|
||||
print(state['next'])
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const state = await client.threads.getState(thread["thread_id"]);
|
||||
|
||||
console.log(state.next);
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
['human_review_node']
|
||||
|
||||
To approve the tool call, we can just continue the thread with no edits. To do this, we just create a new run with no inputs.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
"agent",
|
||||
input=None,
|
||||
stream_mode="values",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
input: undefined,
|
||||
streamMode: "values",
|
||||
}
|
||||
);
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '54e19d6e-89fa-44fb-b92c-12e7dd4ddf08', 'example': False}, {'content': [{'text': "Certainly! I can help you check the weather in San Francisco. To get this information, I'll use the weather search function. Let me do that for you right away.", 'type': 'text', 'index': 0}, {'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-45a6b6c3-ac69-42a4-8957-d982203d6392', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 90, 'total_tokens': 450}}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '826cd0f2-9cc6-46f0-b7df-daa6a05d13d2', 'tool_call_id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'artifact': None, 'status': 'success'}]}
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '54e19d6e-89fa-44fb-b92c-12e7dd4ddf08', 'example': False}, {'content': [{'text': "Certainly! I can help you check the weather in San Francisco. To get this information, I'll use the weather search function. Let me do that for you right away.", 'type': 'text', 'index': 0}, {'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-45a6b6c3-ac69-42a4-8957-d982203d6392', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 90, 'total_tokens': 450}}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '826cd0f2-9cc6-46f0-b7df-daa6a05d13d2', 'tool_call_id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'artifact': None, 'status': 'success'}, {'content': [{'text': "\n\nGreat news! The weather in San Francisco is sunny today. It's a beautiful day in the city by the bay. Is there anything else you'd like to know about the weather or any other information I can help you with?", 'type': 'text', 'index': 0}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-5d5fd0f1-a939-447e-801a-9aaa812322d3', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 464, 'output_tokens': 50, 'total_tokens': 514}}]}
|
||||
|
||||
## Edit Tool Call
|
||||
|
||||
Let's now say we want to edit the tool call. E.g. change some of the parameters (or even the tool called!) but then execute that tool.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]}
|
||||
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
"agent",
|
||||
input=input,
|
||||
stream_mode="values",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]}
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
input: input,
|
||||
streamMode: "values",
|
||||
}
|
||||
);
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'cec11391-84da-464b-bd2a-bd4f0d93b9ee', 'example': False}]}
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'cec11391-84da-464b-bd2a-bd4f0d93b9ee', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01SunSpDurNfcnXppWLPrtjC', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-6326da9f-6061-4e12-8586-482e32ab4cab', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01SunSpDurNfcnXppWLPrtjC', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}]}
|
||||
|
||||
|
||||
To do this, we first need to update the state. We can do this by passing a message in with the **same** id of the message we want to overwrite. This will have the effect of **replacing** that old message. Note that this is only possible because of the **reducer** we are using that replaces messages with the same ID - read more about that [here](https://langchain-ai.github.io/langgraph/concepts/low_level/#working-with-messages-in-graph-state).
|
||||
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
# To get the ID of the message we want to replace, we need to fetch the current state and find it there.
|
||||
state = await client.threads.get_state(thread['thread_id'])
|
||||
print("Current State:")
|
||||
print(state['values'])
|
||||
print("\nCurrent Tool Call ID:")
|
||||
current_content = state['values']['messages'][-1]['content']
|
||||
current_id = state['values']['messages'][-1]['id']
|
||||
tool_call_id = state['values']['messages'][-1]['tool_calls'][0]['id']
|
||||
print(tool_call_id)
|
||||
|
||||
# We now need to construct a replacement tool call.
|
||||
# We will change the argument to be `San Francisco, USA`
|
||||
# Note that we could change any number of arguments or tool names - it just has to be a valid one
|
||||
new_message = {
|
||||
"role": "assistant",
|
||||
"content": current_content,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": tool_call_id,
|
||||
"name": "weather_search",
|
||||
"args": {"city": "San Francisco, USA"}
|
||||
}
|
||||
],
|
||||
# This is important - this needs to be the same as the message you replacing!
|
||||
# Otherwise, it will show up as a separate message
|
||||
"id": current_id
|
||||
}
|
||||
await client.threads.update_state(
|
||||
# This is the config which represents this thread
|
||||
thread['thread_id'],
|
||||
# This is the updated value we want to push
|
||||
{"messages": [new_message]},
|
||||
# We push this update acting as our human_review_node
|
||||
as_node="human_review_node"
|
||||
)
|
||||
|
||||
print("\nResuming Execution")
|
||||
# Let's now continue executing from here
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
"agent",
|
||||
input=None,
|
||||
stream_mode="values",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const state = await client.threads.getState(thread.thread_id);
|
||||
console.log("Current State:");
|
||||
console.log(state.values);
|
||||
|
||||
console.log("\nCurrent Tool Call ID:");
|
||||
const lastMessage = state.values.messages[state.values.messages.length - 1];
|
||||
const currentContent = lastMessage.content;
|
||||
const currentId = lastMessage.id;
|
||||
const toolCallId = lastMessage.tool_calls[0].id;
|
||||
console.log(toolCallId);
|
||||
|
||||
// Construct a replacement tool call
|
||||
const newMessage = {
|
||||
role: "assistant",
|
||||
content: currentContent,
|
||||
tool_calls: [
|
||||
{
|
||||
id: toolCallId,
|
||||
name: "weather_search",
|
||||
args: { city: "San Francisco, USA" }
|
||||
}
|
||||
],
|
||||
// Ensure the ID is the same as the message you're replacing
|
||||
id: currentId
|
||||
};
|
||||
|
||||
await client.threads.updateState(
|
||||
thread.thread_id, // Thread ID
|
||||
{
|
||||
values: { "messages": [newMessage] }, // Updated message
|
||||
asNode: "human_review_node"
|
||||
} // Acting as human_review_node
|
||||
);
|
||||
|
||||
console.log("\nResuming Execution");
|
||||
// Continue executing from here
|
||||
const streamResponseResumed = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
input: undefined,
|
||||
streamMode: "values",
|
||||
interruptBefore: ["action"],
|
||||
}
|
||||
);
|
||||
for await (const chunk of streamResponseResumed) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
Current State:
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '8713d1fa-9b26-4eab-b768-dafdaac70590', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-ede13f26-daf5-4d8f-817a-7611075bbcf1', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}]}
|
||||
|
||||
Current Tool Call ID:
|
||||
toolu_01VzagzsUGZsNMwW1wHkcw7h
|
||||
|
||||
Resuming Execution
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '8713d1fa-9b26-4eab-b768-dafdaac70590', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-ede13f26-daf5-4d8f-817a-7611075bbcf1', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '7fc7d463-66bf-4555-9929-6af483de169b', 'tool_call_id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'artifact': None, 'status': 'success'}]}
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '8713d1fa-9b26-4eab-b768-dafdaac70590', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-ede13f26-daf5-4d8f-817a-7611075bbcf1', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '7fc7d463-66bf-4555-9929-6af483de169b', 'tool_call_id': 'toolu_01VzagzsUGZsNMwW1wHkcw7h', 'artifact': None, 'status': 'success'}, {'content': [{'text': "\n\nBased on the search result, the weather in San Francisco is sunny! It's a beautiful day in the city by the bay. Is there anything else you'd like to know about the weather or any other information I can help you with?", 'type': 'text', 'index': 0}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-d90ce97a-39f9-4330-985e-67c5f351a0c5', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 455, 'output_tokens': 52, 'total_tokens': 507}}]}
|
||||
|
||||
## Give feedback to a tool call
|
||||
|
||||
Sometimes, you may not want to execute a tool call, but you also may not want to ask the user to manually modify the tool call. In that case it may be better to get natural language feedback from the user. You can then insert these feedback as a mock **RESULT** of the tool call.
|
||||
|
||||
There are multiple ways to do this:
|
||||
|
||||
You could add a new message to the state (representing the "result" of a tool call)
|
||||
You could add TWO new messages to the state - one representing an "error" from the tool call, other HumanMessage representing the feedback
|
||||
Both are similar in that they involve adding messages to the state. The main difference lies in the logic AFTER the `human_node` and how it handles different types of messages.
|
||||
|
||||
For this example we will just add a single tool call representing the feedback. Let's see this in action!
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]}
|
||||
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
"agent",
|
||||
input=input,
|
||||
stream_mode="values",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]}
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
input: input,
|
||||
streamMode: "values",
|
||||
}
|
||||
);
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c80f13d0-674d-4233-b6a0-3940509d3cf3', 'example': False}]}
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c80f13d0-674d-4233-b6a0-3940509d3cf3', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_016XyTdFA8NuPWeLyZPSzoM3', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-4911ac27-3d7c-4edf-a3ca-c2908e3922eb', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_016XyTdFA8NuPWeLyZPSzoM3', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}]}
|
||||
|
||||
To do this, we first need to update the state. We can do this by passing a message in with the same **tool call id** of the tool call we want to respond to. Note that this is a **different*** ID from above
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
# To get the ID of the message we want to replace, we need to fetch the current state and find it there.
|
||||
state = await client.threads.get_state(thread['thread_id'])
|
||||
print("Current State:")
|
||||
print(state['values'])
|
||||
print("\nCurrent Tool Call ID:")
|
||||
tool_call_id = state['values']['messages'][-1]['tool_calls'][0]['id']
|
||||
print(tool_call_id)
|
||||
|
||||
# We now need to construct a replacement tool call.
|
||||
# We will change the argument to be `San Francisco, USA`
|
||||
# Note that we could change any number of arguments or tool names - it just has to be a valid one
|
||||
new_message = {
|
||||
"role": "tool",
|
||||
# This is our natural language feedback
|
||||
"content": "User requested changes: pass in the country as well",
|
||||
"name": "weather_search",
|
||||
"tool_call_id": tool_call_id
|
||||
}
|
||||
await client.threads.update_state(
|
||||
# This is the config which represents this thread
|
||||
thread['thread_id'],
|
||||
# This is the updated value we want to push
|
||||
{"messages": [new_message]},
|
||||
# We push this update acting as our human_review_node
|
||||
as_node="human_review_node"
|
||||
)
|
||||
|
||||
print("\nResuming execution")
|
||||
# Let's now continue executing from here
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
"agent",
|
||||
input=None,
|
||||
stream_mode="values",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const state = await client.threads.getState(thread.thread_id);
|
||||
console.log("Current State:");
|
||||
console.log(state.values);
|
||||
|
||||
console.log("\nCurrent Tool Call ID:");
|
||||
const lastMessage = state.values.messages[state.values.messages.length - 1];
|
||||
const toolCallId = lastMessage.tool_calls[0].id;
|
||||
console.log(toolCallId);
|
||||
|
||||
// Construct a replacement tool call
|
||||
const newMessage = {
|
||||
role: "tool",
|
||||
content: "User requested changes: pass in the country as well",
|
||||
name: "weather_search",
|
||||
tool_call_id: toolCallId,
|
||||
};
|
||||
|
||||
await client.threads.updateState(
|
||||
thread.thread_id, // Thread ID
|
||||
{
|
||||
values: { "messages": [newMessage] }, // Updated message
|
||||
asNode: "human_review_node"
|
||||
} // Acting as human_review_node
|
||||
);
|
||||
|
||||
console.log("\nResuming Execution");
|
||||
// Continue executing from here
|
||||
const streamResponseEdited = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
input: undefined,
|
||||
streamMode: "values",
|
||||
interruptBefore: ["action"],
|
||||
}
|
||||
);
|
||||
for await (const chunk of streamResponseEdited) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
Current State:
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '3b2bbc38-d11b-49eb-80c0-c24a40dab5a8', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-c5a50900-abf5-4885-9cdb-da2bf0d892ac', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}]}
|
||||
|
||||
Current Tool Call ID:
|
||||
toolu_01NNw18j57GEGPZvsa9f1wvX
|
||||
|
||||
Resuming execution
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '3b2bbc38-d11b-49eb-80c0-c24a40dab5a8', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-c5a50900-abf5-4885-9cdb-da2bf0d892ac', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}, {'content': 'User requested changes: pass in the country as well', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '787288be-213c-4fd3-8503-4a009bdb1b00', 'tool_call_id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'artifact': None, 'status': 'success'}, {'content': [{'text': '\n\nI apologize for the oversight. It seems the function requires additional information. Let me try again with a more specific request.', 'type': 'text', 'index': 0}, {'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco, USA"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-5c355a56-cfe3-4046-b49f-f5b09fc397ef', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 461, 'output_tokens': 83, 'total_tokens': 544}}]}
|
||||
|
||||
We can see that we now get to another breakpoint - because it went back to the model and got an entirely new prediction of what to call. Let's now approve this one and continue
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
"agent",
|
||||
input=None,
|
||||
stream_mode="values",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const streamResponseResumed = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
input: undefined,
|
||||
streamMode: "values",
|
||||
}
|
||||
);
|
||||
for await (const chunk of streamResponseResumed) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '3b2bbc38-d11b-49eb-80c0-c24a40dab5a8', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-c5a50900-abf5-4885-9cdb-da2bf0d892ac', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}, {'content': 'User requested changes: pass in the country as well', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '787288be-213c-4fd3-8503-4a009bdb1b00', 'tool_call_id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'artifact': None, 'status': 'success'}, {'content': [{'text': '\n\nI apologize for the oversight. It seems the function requires additional information. Let me try again with a more specific request.', 'type': 'text', 'index': 0}, {'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco, USA"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-5c355a56-cfe3-4046-b49f-f5b09fc397ef', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 461, 'output_tokens': 83, 'total_tokens': 544}}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '3b857482-bca2-4a73-a9ab-1f35a3e43e5f', 'tool_call_id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'artifact': None, 'status': 'success'}]}
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '3b2bbc38-d11b-49eb-80c0-c24a40dab5a8', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-c5a50900-abf5-4885-9cdb-da2bf0d892ac', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}, {'content': 'User requested changes: pass in the country as well', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '787288be-213c-4fd3-8503-4a009bdb1b00', 'tool_call_id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'artifact': None, 'status': 'success'}, {'content': [{'text': '\n\nI apologize for the oversight. It seems the function requires additional information. Let me try again with a more specific request.', 'type': 'text', 'index': 0}, {'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco, USA"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-5c355a56-cfe3-4046-b49f-f5b09fc397ef', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 461, 'output_tokens': 83, 'total_tokens': 544}}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '3b857482-bca2-4a73-a9ab-1f35a3e43e5f', 'tool_call_id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'artifact': None, 'status': 'success'}, {'content': [{'text': "\n\nGreat news! The weather in San Francisco is sunny today. Is there anything else you'd like to know about the weather or any other information I can help you with?", 'type': 'text', 'index': 0}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-6a857bb1-f65b-4b86-93d6-c025e003c777', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 557, 'output_tokens': 38, 'total_tokens': 595}}]}
|
||||
@@ -14,7 +14,7 @@ First, we need to setup our client so that we can communicate with our hosted gr
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
client = get_client(url="whatever-your-deployment-url-is")
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
@@ -24,11 +24,20 @@ First, we need to setup our client so that we can communicate with our hosted gr
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
const assistantId = agent;
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data {}
|
||||
```
|
||||
|
||||
## Replay a state
|
||||
|
||||
### Initial invocation
|
||||
@@ -38,7 +47,7 @@ Before replaying a state - we need to create states to replay from! In order to
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
input = { 'messages':[{ "role":"user", "content":"Please search the weather in SF" }] }
|
||||
input = {"messages": [{"role": "user", "content": "Please search the weather in SF"}]}
|
||||
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
@@ -53,7 +62,7 @@ Before replaying a state - we need to create states to replay from! In order to
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const input = {"messages": [{ "role": "human", "content": "Please search the weather in SF"}] }
|
||||
const input = { "messages": [{ "role": "human", "content": "Please search the weather in SF" }] }
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
@@ -69,6 +78,41 @@ Before replaying a state - we need to create states to replay from! In order to
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"Please search the weather in SF\"}]},
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
]
|
||||
}" | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "", $0)
|
||||
event_type = $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
@@ -100,23 +144,35 @@ Now let's get our list of states, and invoke from the third state (right before
|
||||
console.log(stateToReplay['next']);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/history | jq -r '.[2].next'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
['action']
|
||||
|
||||
|
||||
|
||||
To rerun from a state, we need to pass in the `checkpoint_id` into the config of the run like follows:
|
||||
To rerun from a state, we need first issue an empty update to the thread state. Then we need to pass in the resulting `checkpoint_id` as follows:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
state_to_replay = states[2]
|
||||
updated_config = await client.threads.update_state(
|
||||
thread["thread_id"],
|
||||
{"messages": []},
|
||||
checkpoint_id=state_to_replay["checkpoint_id"]
|
||||
)
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistant_id, # graph_id
|
||||
input=None,
|
||||
stream_mode="updates",
|
||||
config={"configurable": {"thread_ts": state_to_replay['checkpoint_id']}}
|
||||
checkpoint_id=updated_config["checkpoint_id"]
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
@@ -125,13 +181,15 @@ To rerun from a state, we need to pass in the `checkpoint_id` into the config of
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const stateToReplay = states[2];
|
||||
const config = await client.threads.updateState(thread["thread_id"], { values: {"messages": [] }, checkpointId: stateToReplay["checkpoint_id"] });
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
input: null,
|
||||
streamMode: "updates",
|
||||
config: {"configurable": {"thread_ts": stateToReplay['checkpoint_id']}},
|
||||
checkpointId: config["checkpoint_id"]
|
||||
}
|
||||
);
|
||||
for await (const chunk of streamResponse) {
|
||||
@@ -141,6 +199,51 @@ To rerun from a state, we need to pass in the `checkpoint_id` into the config of
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/history | jq -c '
|
||||
.[2] as $state_to_replay |
|
||||
{
|
||||
values: { messages: .[2].values.messages[-1] },
|
||||
checkpoint_id: $state_to_replay.checkpoint_id
|
||||
}' | \
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data @- | jq .checkpoint_id | \
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"checkpoint_id\": \"$1\",
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
]
|
||||
}" | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "", $0)
|
||||
event_type = $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'action': {'messages': [{'content': '["I looked up: current weather in San Francisco. Result: It\'s sunny in San Francisco, but you better look out if you\'re a Gemini 😈."]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': 'eba650e5-400e-4938-8508-f878dcbcc532', 'tool_call_id': 'toolu_011vroKUtWU7SBdrngpgpFMn'}]}}
|
||||
@@ -165,7 +268,7 @@ Let's show how to do this to edit the state at a particular point in time. Let's
|
||||
# Let's now update the args for that tool call
|
||||
last_message['tool_calls'][0]['args'] = {'query': 'current weather in SF'}
|
||||
|
||||
new_state = await client.threads.update_state(thread['thread_id'],{"messages":[last_message]},checkpoint_id=state_to_replay['checkpoint_id'])
|
||||
config = await client.threads.update_state(thread['thread_id'],{"messages":[last_message]},checkpoint_id=state_to_replay['checkpoint_id'])
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
@@ -176,9 +279,26 @@ Let's show how to do this to edit the state at a particular point in time. Let's
|
||||
let lastMessage = stateToReplay['values']['messages'][-1];
|
||||
|
||||
// Let's now update the args for that tool call
|
||||
lastMessage['tool_calls'][0]['args'] = {'query': 'current weather in SF'};
|
||||
lastMessage['tool_calls'][0]['args'] = { 'query': 'current weather in SF' };
|
||||
|
||||
const newState = await client.threads.updateState(thread['thread_id'],{values:{"messages":[lastMessage]},checkpointId:stateToReplay['checkpoint_id']});
|
||||
const config = await client.threads.updateState(thread['thread_id'], { values: { "messages": [lastMessage] }, checkpointId: stateToReplay['checkpoint_id'] });
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl -s --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/history | \
|
||||
jq -c '
|
||||
.[2] as $state_to_replay |
|
||||
.[2].values.messages[-1].tool_calls[0].args.query = "current weather in SF" |
|
||||
{
|
||||
values: { messages: .[2].values.messages[-1] },
|
||||
checkpoint_id: $state_to_replay.checkpoint_id
|
||||
}' | \
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data @-
|
||||
```
|
||||
|
||||
Now we can rerun our graph with this new config, starting from the `new_state`, which is a branch of our `state_to_replay`:
|
||||
@@ -191,7 +311,7 @@ Now we can rerun our graph with this new config, starting from the `new_state`,
|
||||
assistant["assistant_id"], # graph_id
|
||||
input=None,
|
||||
stream_mode="updates",
|
||||
config={"configurable": {"thread_ts": new_state['configurable']['thread_ts']}}
|
||||
checkpoint_id=config['checkpoint_id']
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
@@ -206,7 +326,7 @@ Now we can rerun our graph with this new config, starting from the `new_state`,
|
||||
{
|
||||
input: null,
|
||||
streamMode: "updates",
|
||||
config: {"configurable": {"thread_ts": newState['configurable']['thread_ts']}},
|
||||
checkpointId: config['checkpoint_id'],
|
||||
}
|
||||
);
|
||||
for await (const chunk of streamResponse) {
|
||||
@@ -216,6 +336,43 @@ Now we can rerun our graph with this new config, starting from the `new_state`,
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl -s --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state | \
|
||||
jq -c '.checkpoint_id' | \
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"checkpoint_id\": \"$1\",
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
]
|
||||
}" | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "", $0)
|
||||
event_type = $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ First, we need to setup our client so that we can communicate with our hosted gr
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
client = get_client(url="whatever-your-deployment-url-is")
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
@@ -34,11 +34,19 @@ First, we need to setup our client so that we can communicate with our hosted gr
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
const assistantId = "agent";
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
## Waiting for user input
|
||||
|
||||
### Initial invocation
|
||||
@@ -80,6 +88,42 @@ Now, let's invoke our graph by interrupting before `ask_human` node:
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"Use the search tool to ask the user where they are, then look up the weather there\"}]},
|
||||
\"interrupt_before\": [\"ask_human\"],
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
]
|
||||
}" | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "", $0)
|
||||
event_type = $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
@@ -117,11 +161,31 @@ Because we are treating this as a tool call, we will need to update the state as
|
||||
await client.threads.updateState(thread['thread_id'], {values: {"messages": toolMessage}, asNode:"ask_human"})
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state \
|
||||
| jq -r '.values.messages[-1].tool_calls[0].id' \
|
||||
| sh -c '
|
||||
TOOL_CALL_ID="$1"
|
||||
|
||||
# Construct the JSON payload
|
||||
JSON_PAYLOAD=$(printf "{\"messages\": [{\"tool_call_id\": \"%s\", \"type\": \"tool\", \"content\": \"san francisco\"}], \"as_node\": \"ask_human\"}" "$TOOL_CALL_ID")
|
||||
|
||||
# Send the updated state
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state \
|
||||
--header "Content-Type: application/json" \
|
||||
--data "${JSON_PAYLOAD}"
|
||||
' _
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'configurable': {'thread_id': '10d0ee61-db47-48fc-a58c-109a1e68cd73',
|
||||
'thread_ts': '1ef32729-3cc3-6647-8002-14dcb621b46e'}}
|
||||
|
||||
{'configurable': {'thread_id': 'a9f322ae-4ed1-41ec-942b-38cb3d342c3a',
|
||||
'checkpoint_ns': '',
|
||||
'checkpoint_id': '1ef58e97-a623-63dd-8002-39a9a9b20be3'}}
|
||||
|
||||
|
||||
### Invoking after receiving human input
|
||||
@@ -133,7 +197,7 @@ We can now tell the agent to continue. We can just pass in None as the input to
|
||||
```python
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistant_id, # graph_id
|
||||
assistant_id,
|
||||
input=None,
|
||||
stream_mode="updates",
|
||||
):
|
||||
@@ -158,6 +222,40 @@ We can now tell the agent to continue. We can just pass in None as the input to
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
]
|
||||
}"| \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "", $0)
|
||||
event_type = $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "" && event_type != "metadata") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'agent': {'messages': [{'content': [{'text': "Thank you for letting me know that you're in San Francisco. Now, I'll use the search function to look up the weather in San Francisco.", 'type': 'text'}, {'id': 'toolu_01K57ofmgG2wyJ8tYJjbq5k7', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-241baed7-db5e-44ce-ac3c-56431705c22b', 'example': False, 'tool_calls': [{'name': 'search', 'args': {'query': 'current weather in San Francisco'}, 'id': 'toolu_01K57ofmgG2wyJ8tYJjbq5k7'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
|
||||
|
||||
@@ -46,6 +46,7 @@ When creating complex graphs, leaving every decision up to the LLM can be danger
|
||||
- [How to wait for user input](./human_in_the_loop_user_input.md)
|
||||
- [How to edit graph state](./human_in_the_loop_edit_state.md)
|
||||
- [How to replay and branch from prior states](./human_in_the_loop_time_travel.md)
|
||||
- [How to review tool calls](./human_in_the_loop_review_tool_calls.md)
|
||||
|
||||
## LangGraph Studio
|
||||
|
||||
@@ -72,3 +73,5 @@ Other guides that may prove helpful!
|
||||
- [How to configure agents](cloud_examples/configuration_cloud.ipynb)
|
||||
- [How to convert LangGraph calls to LangGraph cloud calls](cloud_examples/langgraph_to_langgraph_cloud.ipynb)
|
||||
- [How to integrate webhooks](cloud_examples/webhooks.ipynb)
|
||||
- [How to copy threads](./copy_threads.md)
|
||||
- [How to check status of your threads](./check_thread_status.md)
|
||||
|
||||
@@ -29,7 +29,7 @@ Now, let's import our required packages and instantiate our client, assistant, a
|
||||
from langchain_core.messages import convert_to_messages
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url="whatever-your-deployment-url-is")
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
@@ -39,7 +39,7 @@ Now, let's import our required packages and instantiate our client, assistant, a
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
const assistantId = "agent";
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
@@ -28,7 +28,7 @@ Now, let's import our required packages and instantiate our client, assistant, a
|
||||
from langchain_core.messages import convert_to_messages
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url="whatever-your-deployment-url-is")
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
@@ -38,7 +38,7 @@ Now, let's import our required packages and instantiate our client, assistant, a
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
const assistantId = "agent";
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
@@ -30,7 +30,7 @@ Now, let's import our required packages and instantiate our client, assistant, a
|
||||
from langchain_core.messages import convert_to_messages
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url="whatever-your-deployment-url-is")
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
@@ -40,7 +40,7 @@ Now, let's import our required packages and instantiate our client, assistant, a
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
const assistantId = "agent";
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
@@ -9,7 +9,7 @@ First let's set up our client and thread:
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url="whatever-your-deployment-url-is")
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# create thread
|
||||
thread = await client.threads.create()
|
||||
print(thread)
|
||||
@@ -20,7 +20,7 @@ First let's set up our client and thread:
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// create thread
|
||||
const thread = await client.threads.create();
|
||||
console.log(thread)
|
||||
|
||||
@@ -6,7 +6,7 @@ This guide covers how to stream events from your graph (`stream_mode="events"`).
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url="whatever-your-deployment-url-is")
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# create thread
|
||||
thread = await client.threads.create()
|
||||
print(thread)
|
||||
@@ -17,12 +17,19 @@ This guide covers how to stream events from your graph (`stream_mode="events"`).
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// create thread
|
||||
const thread = await client.threads.create();
|
||||
console.log(thread)
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
@@ -30,7 +37,9 @@ Output:
|
||||
{'thread_id': '3f4c64e0-f792-4a5e-aa07-a4404e06e0bd',
|
||||
'created_at': '2024-06-24T22:16:29.301522+00:00',
|
||||
'updated_at': '2024-06-24T22:16:29.301522+00:00',
|
||||
'metadata': {}}
|
||||
'metadata': {},
|
||||
'status': 'idle',
|
||||
'config': {}}
|
||||
|
||||
|
||||
|
||||
@@ -91,6 +100,41 @@ Streaming events produces responses containing an `event` key (in addition to ot
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"What's the weather in sf\"}]},
|
||||
\"stream_mode\": [
|
||||
\"events\"
|
||||
]
|
||||
}" | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "Receiving event of type: ", $0)
|
||||
printf "%s...\n", $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
Receiving new event of type: metadata...
|
||||
@@ -258,9 +302,11 @@ Token-by-token streaming can be implemented with the `events` streaming mode. Th
|
||||
):
|
||||
if (
|
||||
chunk.event == "events" and
|
||||
chunk.data["event"] == "on_chat_model_stream"
|
||||
chunk.data["event"] == "on_chat_model_stream" and
|
||||
len(chunk.data["data"]["chunk"]["content"]) > 0 and
|
||||
'text' in chunk.data["data"]["chunk"]["content"][0]
|
||||
):
|
||||
llm_response += chunk.data["data"]["chunk"]["content"]
|
||||
llm_response += chunk.data["data"]["chunk"]["content"][0]['text']
|
||||
print(llm_response)
|
||||
```
|
||||
|
||||
@@ -278,21 +324,88 @@ Token-by-token streaming can be implemented with the `events` streaming mode. Th
|
||||
}
|
||||
);
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.event === "events" && chunk.data.event === "on_chat_model_stream") {
|
||||
llmResponse += chunk.data.data.chunk.content;
|
||||
if (chunk.event === "events" && chunk.data.event === "on_chat_model_stream" && chunk.data.chunk.content.length > 0 && 'text' in chunk.data.chunk.content[0]) {
|
||||
llmResponse += chunk.data.data.chunk.content[0].text;
|
||||
console.log(llmResponse);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"What's the weather in sf\"}]},
|
||||
\"stream_mode\": [
|
||||
\"events\"
|
||||
]
|
||||
}" | sed 's/\r$//' | awk '
|
||||
/^event:/ { event = $2 }
|
||||
/^data:/ {
|
||||
json_data = substr($0, index($0, $2))
|
||||
|
||||
if (event == "events") {
|
||||
print json_data
|
||||
}
|
||||
}' | jq -r '
|
||||
select(.event == "on_chat_model_stream") |
|
||||
.data.chunk.content[] | .text // empty
|
||||
' | awk '
|
||||
BEGIN { llm_response="" }
|
||||
$0 != "" && $0 != "null" {
|
||||
llm_response = llm_response $0
|
||||
print llm_response
|
||||
}'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
b
|
||||
be
|
||||
beg
|
||||
begi
|
||||
begin
|
||||
begine
|
||||
beginen
|
||||
beginend
|
||||
The
|
||||
The search
|
||||
The search results provide
|
||||
The search results provide the current weather conditions
|
||||
The search results provide the current weather conditions in San Francisco.
|
||||
The search results provide the current weather conditions in San Francisco. According
|
||||
The search results provide the current weather conditions in San Francisco. According to the data,
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12,
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024,
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C).
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The win
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is bl
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 k
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph).
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70%
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70% and visibility
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70% and visibility is 6
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70% and visibility is 6 miles
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70% and visibility is 6 miles (10 km
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70% and visibility is 6 miles (10 km).
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70% and visibility is 6 miles (10 km). Overall
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70% and visibility is 6 miles (10 km). Overall, it appears
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70% and visibility is 6 miles (10 km). Overall, it appears to be a nice
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70% and visibility is 6 miles (10 km). Overall, it appears to be a nice sunny day in San
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 3:19 PM on August 12, 2024, the weather in San Francisco is sunny with a temperature of 60.8°F (16°C). The wind is blowing from the west-southwest at 13.4 mph (21.6 kph). The humidity is 70% and visibility is 6 miles (10 km). Overall, it appears to be a nice sunny day in San Francisco.
|
||||
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ First let's set up our client and thread:
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url="whatever-your-deployment-url-is")
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# create thread
|
||||
thread = await client.threads.create()
|
||||
print(thread)
|
||||
@@ -52,20 +52,30 @@ First let's set up our client and thread:
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// create thread
|
||||
const thread = await client.threads.create();
|
||||
console.log(thread)
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'thread_id': 'e1431c95-e241-4d1d-a252-27eceb1e5c86',
|
||||
'created_at': '2024-06-21T15:48:59.808924+00:00',
|
||||
'updated_at': '2024-06-21T15:48:59.808924+00:00',
|
||||
'metadata': {}}
|
||||
'metadata': {},
|
||||
'status': 'idle',
|
||||
'config': {}}
|
||||
|
||||
Let's also define a helper function for better formatting of the tool calls in messages
|
||||
Let's also define a helper function for better formatting of the tool calls in messages (for CURL we will define a helper script called `process_stream.sh`)
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -95,6 +105,69 @@ Let's also define a helper function for better formatting of the tool calls in m
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
# process_stream.sh
|
||||
|
||||
format_tool_calls() {
|
||||
echo "$1" | jq -r 'map("Tool Call ID: \(.id), Function: \(.name), Arguments: \(.args)") | join("\n")'
|
||||
}
|
||||
|
||||
process_data_item() {
|
||||
local data_item="$1"
|
||||
|
||||
if echo "$data_item" | jq -e '.role == "user"' > /dev/null; then
|
||||
echo "Human: $(echo "$data_item" | jq -r '.content')"
|
||||
else
|
||||
local tool_calls=$(echo "$data_item" | jq -r '.tool_calls // []')
|
||||
local invalid_tool_calls=$(echo "$data_item" | jq -r '.invalid_tool_calls // []')
|
||||
local content=$(echo "$data_item" | jq -r '.content // ""')
|
||||
local response_metadata=$(echo "$data_item" | jq -r '.response_metadata // {}')
|
||||
|
||||
if [ -n "$content" ] && [ "$content" != "null" ]; then
|
||||
echo "AI: $content"
|
||||
fi
|
||||
|
||||
if [ "$tool_calls" != "[]" ]; then
|
||||
echo "Tool Calls:"
|
||||
format_tool_calls "$tool_calls"
|
||||
fi
|
||||
|
||||
if [ "$invalid_tool_calls" != "[]" ]; then
|
||||
echo "Invalid Tool Calls:"
|
||||
format_tool_calls "$invalid_tool_calls"
|
||||
fi
|
||||
|
||||
if [ "$response_metadata" != "{}" ]; then
|
||||
local finish_reason=$(echo "$response_metadata" | jq -r '.finish_reason // "N/A"')
|
||||
echo "Response Metadata: Finish Reason - $finish_reason"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
while IFS=': ' read -r key value; do
|
||||
case "$key" in
|
||||
event)
|
||||
event="$value"
|
||||
;;
|
||||
data)
|
||||
if [ "$event" = "metadata" ]; then
|
||||
run_id=$(echo "$value" | jq -r '.run_id')
|
||||
echo "Metadata: Run ID - $run_id"
|
||||
echo "------------------------------------------------"
|
||||
elif [ "$event" = "messages/partial" ]; then
|
||||
echo "$value" | jq -c '.[]' | while read -r data_item; do
|
||||
process_data_item "$data_item"
|
||||
done
|
||||
echo "------------------------------------------------"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
```
|
||||
|
||||
|
||||
Now we can stream by messages, which will return complete messages (at the end of node execution) as well as tokens for any messages generated inside a node:
|
||||
|
||||
=== "Python"
|
||||
@@ -201,6 +274,23 @@ Now we can stream by messages, which will return complete messages (at the end o
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"config\":{\"configurable\":{\"model_name\":\"openai\"}},
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"What's the weather in sf\"}]},
|
||||
\"stream_mode\": [
|
||||
\"messages\"
|
||||
]
|
||||
}" | sed 's/\r$//' | ./process_stream.sh
|
||||
```
|
||||
|
||||
|
||||
Output:
|
||||
|
||||
Metadata: Run ID - 1ef2fe5c-6a1d-6575-bc09-d7832711c17e
|
||||
|
||||
@@ -9,7 +9,7 @@ First let's set up our client and thread:
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url="whatever-your-deployment-url-is")
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# create thread
|
||||
thread = await client.threads.create()
|
||||
print(thread)
|
||||
@@ -20,19 +20,28 @@ First let's set up our client and thread:
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// create thread
|
||||
const thread = await client.threads.create();
|
||||
console.log(thread)
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4',
|
||||
'created_at': '2024-06-24T21:30:07.980789+00:00',
|
||||
'updated_at': '2024-06-24T21:30:07.980789+00:00',
|
||||
'metadata': {}}
|
||||
'metadata': {},
|
||||
'status': 'idle',
|
||||
'config': {}}
|
||||
|
||||
When configuring multiple streaming modes for a run, responses for each respective mode will be produced. In the following example, note that a `list` of modes (`messages`, `events`, `debug`) is passed to the `stream_mode` parameter and the response contains `events`, `debug`, `messages/complete`, `messages/metadata`, and `messages/partial` event types.
|
||||
|
||||
@@ -90,6 +99,43 @@ When configuring multiple streaming modes for a run, responses for each respecti
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"What's the weather in SF?\"}]},
|
||||
\"stream_mode\": [
|
||||
\"messages\",
|
||||
\"events\",
|
||||
\"debug\"
|
||||
]
|
||||
}" | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "Receiving event of type: ", $0)
|
||||
printf "%s...\n", $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
Receiving new event of type: metadata...
|
||||
|
||||
@@ -16,7 +16,7 @@ First let's set up our client and thread:
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url="whatever-your-deployment-url-is")
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# create thread
|
||||
thread = await client.threads.create()
|
||||
print(thread)
|
||||
@@ -27,19 +27,28 @@ First let's set up our client and thread:
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl:"whatever-your-deployment-url-is" });
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// create thread
|
||||
const thread = await client.threads.create();
|
||||
console.log(thread)
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'thread_id': '979e3c89-a702-4882-87c2-7a59a250ce16',
|
||||
'created_at': '2024-06-21T15:22:07.453100+00:00',
|
||||
'updated_at': '2024-06-21T15:22:07.453100+00:00',
|
||||
'metadata': {}}
|
||||
'metadata': {},
|
||||
'status': 'idle',
|
||||
'config': {}}
|
||||
|
||||
Now we can stream by updates, which outputs updates made to the state by each node after it has executed:
|
||||
|
||||
@@ -93,6 +102,41 @@ Now we can stream by updates, which outputs updates made to the state by each no
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"What's the weather in la\"}]},
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
]
|
||||
}" | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "Receiving event of type: ", $0)
|
||||
printf "%s...\n", $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
Receiving new event of type: metadata...
|
||||
|
||||
@@ -16,7 +16,7 @@ First let's set up our client and thread:
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url="whatever-your-deployment-url-is")
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# create thread
|
||||
thread = await client.threads.create()
|
||||
print(thread)
|
||||
@@ -27,18 +27,28 @@ First let's set up our client and thread:
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: "whatever-your-deployment-url-is" });
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// create thread
|
||||
const thread = await client.threads.create();
|
||||
console.log(thread)
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4',
|
||||
'created_at': '2024-06-24T21:30:07.980789+00:00',
|
||||
'updated_at': '2024-06-24T21:30:07.980789+00:00',
|
||||
'metadata': {}}
|
||||
'metadata': {},
|
||||
'status': 'idle',
|
||||
'config': {}}
|
||||
|
||||
Now we can stream by values, which streams the full state of the graph after each node has finished executing:
|
||||
|
||||
@@ -60,7 +70,6 @@ Now we can stream by values, which streams the full state of the graph after eac
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
|
||||
```js
|
||||
const input = {"messages": [{"role": "human", "content": "what's the weather in la"}]}
|
||||
@@ -80,6 +89,41 @@ Now we can stream by values, which streams the full state of the graph after eac
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in la\"}]},
|
||||
\"stream_mode\": [
|
||||
\"values\"
|
||||
]
|
||||
}" | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^event:/ {
|
||||
if (data_content != "") {
|
||||
print data_content "\n"
|
||||
}
|
||||
sub(/^event: /, "Receiving event of type: ", $0)
|
||||
printf "%s...\n", $0
|
||||
data_content = ""
|
||||
}
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "") {
|
||||
print data_content "\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
|
||||
Output:
|
||||
|
||||
@@ -149,6 +193,34 @@ If we want to just get the final result, we can use this endpoint and just keep
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in la\"}]},
|
||||
\"stream_mode\": [
|
||||
\"values\"
|
||||
]
|
||||
}" | \
|
||||
sed 's/\r$//' | \
|
||||
awk '
|
||||
/^data:/ {
|
||||
sub(/^data: /, "", $0)
|
||||
data_content = $0
|
||||
}
|
||||
END {
|
||||
if (data_content != "") {
|
||||
print data_content
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
|
||||
Output:
|
||||
|
||||
{'messages': [{'content': 'what's the weather in la',
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 884 KiB |
@@ -23,6 +23,8 @@ The LangGraph Cloud API exposes functionality of your LangGraph application thro
|
||||
|
||||
LangGraph Cloud is seamlessly integrated with [LangSmith](https://www.langchain.com/langsmith) and is accessible from within the LangSmith UI.
|
||||
|
||||
LangGraph Cloud applications can be tested and debugged using the [LangGraph Studio Desktop](https://github.com/langchain-ai/langgraph-studio).
|
||||
|
||||
## Key Features
|
||||
|
||||
The LangGraph Cloud API supports key LangGraph features in addition to new functionality for enabling complex, agentic workflows.
|
||||
|
||||
@@ -66,6 +66,16 @@ Now that we have set everything up on our local file system, we are ready to hos
|
||||
|
||||
## Test the graph build locally
|
||||
|
||||
### Using LangGraph Studio Desktop (recommended)
|
||||
|
||||

|
||||
|
||||
Testing your graph locally is easy with LangGraph Studio Desktop. LangGraph Studio offers a new way to develop LLM applications by providing a specialized agent IDE that enables visualization, interaction, and debugging of complex agentic applications
|
||||
|
||||
With visual graphs and the ability to edit state, you can better understand agent workflows and iterate faster. LangGraph Studio integrates with [LangSmith](https://smith.langchain.com) so you can collaborate with teammates to debug failure modes.
|
||||
|
||||
### Using the LangGraph CLI
|
||||
|
||||
Before deploying to the cloud, we probably want to test the building of our graph locally. This is useful to make sure we have configured our [CLI configuration file][langgraph.json] correctly and our graph runs.
|
||||
|
||||
In order to do this we can first install the LangGraph CLI
|
||||
|
||||
@@ -3,3 +3,20 @@
|
||||
The LangGraph Cloud API reference is available with each deployment at the `/docs` URL path (e.g. `http://localhost:8124/docs`).
|
||||
|
||||
Click <a href="/langgraph/cloud/reference/api/api_ref.html" target="_blank">here</a> to view the API reference.
|
||||
|
||||
## Authentication
|
||||
|
||||
For deployments to LangGraph Cloud, authentication is required. Pass the `X-Api-Key` header with each request to the LangGraph Cloud API. The value of the header should be set to a valid LangSmith API key for the organization where the API is deployed.
|
||||
|
||||
Example `curl` command:
|
||||
```shell
|
||||
curl --request POST \
|
||||
--url http://localhost:8124/assistants/search \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'X-Api-Key: LANGSMITH_API_KEY' \
|
||||
--data '{
|
||||
"metadata": {},
|
||||
"limit": 10,
|
||||
"offset": 0
|
||||
}'
|
||||
```
|
||||
|
||||
@@ -56,6 +56,25 @@ This is a pretty advanced interaction pattern. In this interaction pattern, the
|
||||
|
||||
See [this guide](../how-tos/human_in_the_loop/time-travel.ipynb) for how to do this in LangGraph.
|
||||
|
||||
## Review Tool Calls
|
||||
|
||||
This is a specific type of human-in-the-loop interaction but it's worth calling out because it is so common. A lot of agent decisions are made via tool calling, so having a clear UX for reviewing tool calls is handy.
|
||||
|
||||
A tool call consists of:
|
||||
- The name of the tool to call
|
||||
- Arguments to pass to the tool
|
||||
|
||||
Note that these tool calls can obviously be used for actually calling functions, but they can also be used for other purposes, like to route the agent in a specific direction.
|
||||
You will want to review the tool call for both of these use cases.
|
||||
|
||||
When reviewing tool calls, there are few actions you may want to take.
|
||||
|
||||
1. Approve the tool call (and let the agent continue on its way)
|
||||
2. Manually change the tool call, either the tool name or the tool arguments (and let the agent continue on its way after that)
|
||||
3. Leave feedback on the tool call. This differs from (2) in that you are not changing the tool call directly, but rather leaving natural language feedback suggesting the LLM call it differently (or call a different tool). You could do this by either adding a `ToolMessage` and having the feedback be the result of the tool call, or by adding a `ToolMessage` (that simulates an error) and then a `HumanMessage` (with the feedback).
|
||||
|
||||
See [this guide](../how-tos/human_in_the_loop/review-tool-calls.ipynb) for how to do this in LangGraph.
|
||||
|
||||
## Map-Reduce
|
||||
|
||||
A common pattern in agents is to generate a list of objects, do some work on each of those objects, and then combine the results. This is very similar to the common [map-reduce](https://en.wikipedia.org/wiki/MapReduce) operation. This can be tricky for a few reasons. First, it can be tough to define a structured graph ahead of time because the length of the list of objects may be unknown. Second, in order to do this map-reduce you need multiple versions of the state to exist... but the graph shares a common shared state, so how can this be?
|
||||
|
||||
@@ -401,7 +401,17 @@ def node_a(state, config):
|
||||
...
|
||||
```
|
||||
|
||||
See [this guide](../how-tos/configuration.ipynb) for a full breakdown on configuration
|
||||
See [this guide](../how-tos/configuration.ipynb) for a full breakdown on configuration.
|
||||
|
||||
### Recursion Limit
|
||||
|
||||
The recursion limit sets the maximum number of [super-steps](#graphs) the graph can execute during a single execution. Once the limit is reached, LangGraph will raise `GraphRecursionError`. By default this value is set to 25 steps. The recursion limit can be set on any graph at runtime, and is passed to `.invoke`/`.stream` via the config dictionary. Importantly, `recursion_limit` is a standalone `config` key and should not be passed inside the `configurable` key as all other user-defined configuration. See the example below:
|
||||
|
||||
```python
|
||||
graph.invoke(inputs, config={"recursion_limit": 5, "configurable":{"llm": "anthropic"}})
|
||||
```
|
||||
|
||||
Read [this how-to] to learn more about how the recursion limit works.
|
||||
|
||||
## Breakpoints
|
||||
|
||||
@@ -433,4 +443,17 @@ LangGraph is built with first class support for streaming. There are several dif
|
||||
- [`"updates`](../how-tos/stream-updates.ipynb): This streams the updates to the state after each step of the graph. If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are streamed separately.
|
||||
- `"debug"`: This streams as much information as possible throughout the execution of the graph.
|
||||
|
||||
In addition, you can use the [`astream_events`](../how-tos/streaming-events-from-within-tools.ipynb) method to stream back events that happen _inside_ nodes. This is useful for [streaming tokens of LLM calls](../how-tos/streaming-tokens.ipynb).
|
||||
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) among other things.
|
||||
|
||||
Under the hood, the compiled graph and each of its nodes are turned into [runnables](https://python.langchain.com/v0.2/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`:
|
||||
|
||||
* each node (runnable) emits `on_chain_start` when it starts execution, `on_chain_stream` during the node execution and `on_chain_end` when the node finishes. Node events will have the node name in the event's `name` field
|
||||
* the graph will emit `on_chain_start` in the beginning of the graph execution, `on_chain_stream` during each node execution and `on_chain_end` when the graph finishes. Graph events will have the `LangGraph` in the event's `name` field
|
||||
* LangGraph writes to state channels emit `on_chain_start` and `on_chain_end` events
|
||||
|
||||
Additionally, any events that are created inside your nodes (LLM events, tool events, manually emitted events, etc.) will also be visible in the output of `.astream_events`.
|
||||
|
||||
You can find a detailed table of all callback events and triggers [here](https://python.langchain.com/v0.2/docs/concepts/#callback-events).
|
||||
|
||||
!!! 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).
|
||||
@@ -15,7 +15,7 @@ These how-to guides show how to achieve that controllability.
|
||||
- [How to create subgraphs](subgraph.ipynb)
|
||||
- [How to create branches for parallel execution](branching.ipynb)
|
||||
- [How to create map-reduce branches for parallel execution](map-reduce.ipynb)
|
||||
|
||||
- [How to control graph recursion limit](recursion-limit.ipynb)
|
||||
|
||||
## Persistence
|
||||
|
||||
@@ -35,9 +35,11 @@ One of LangGraph's main benefits is that it makes human-in-the-loop workflows ea
|
||||
These guides cover common examples of that.
|
||||
|
||||
- [How to add breakpoints](human_in_the_loop/breakpoints.ipynb)
|
||||
- [How to add dynamic breakpoints](human_in_the_loop/dynamic_breakpoints.ipynb)
|
||||
- [How to edit graph state](human_in_the_loop/edit-graph-state.ipynb)
|
||||
- [How to wait for user input](human_in_the_loop/wait-user-input.ipynb)
|
||||
- [How to view and update past graph state](human_in_the_loop/time-travel.ipynb)
|
||||
- [Review tool calls](human_in_the_loop/review-tool-calls.ipynb)
|
||||
|
||||
## Streaming
|
||||
|
||||
@@ -77,6 +79,7 @@ These guides show how to use different streaming modes.
|
||||
- [How to use a Pydantic model as your state](state-model.ipynb)
|
||||
- [How to use a context object in state](state-context-key.ipynb)
|
||||
- [How to add node retries](node-retries.ipynb)
|
||||
- [How to force function calling agent to structure output](react-agent-structured-output.ipynb)
|
||||
|
||||
## Prebuilt ReAct Agent
|
||||
|
||||
|
||||
@@ -65,4 +65,3 @@ Learn from example implementations of graphs designed for specific scenarios and
|
||||
- [Web Navigation](web-navigation/web_voyager.ipynb): Build an agent that can navigate and interact with websites
|
||||
- [Competitive Programming](usaco/usaco.ipynb): Build an agent with few-shot "episodic memory" and human-in-the-loop collaboration to solve problems from the USA Computing Olympiad; adapted from the ["Can Language Models Solve Olympiad Programming?"](https://arxiv.org/abs/2404.10952v1) paper by Shi, Tang, Narasimhan, and Yao.
|
||||
- [Complex data extraction](extraction/retries.ipynb): Build an agent that can use function calling to do complex extraction tasks
|
||||
-
|
||||
@@ -129,6 +129,7 @@ nav:
|
||||
- Create subgraphs: how-tos/subgraph.ipynb
|
||||
- Create branches for parallel execution: how-tos/branching.ipynb
|
||||
- Create map-reduce branches for parallel execution: how-tos/map-reduce.ipynb
|
||||
- Control graph recursion limit: how-tos/recursion-limit.ipynb
|
||||
- Persistence:
|
||||
- Add persistence ("memory"): how-tos/persistence.ipynb
|
||||
- Manage conversation history: how-tos/memory/manage-conversation-history.ipynb
|
||||
@@ -139,9 +140,11 @@ nav:
|
||||
- Create custom checkpointer using Redis: how-tos/persistence_redis.ipynb
|
||||
- Human-in-the-loop:
|
||||
- Add breakpoints: how-tos/human_in_the_loop/breakpoints.ipynb
|
||||
- Add dynamic breakpoints: how-tos/human_in_the_loop/dynamic_breakpoints.ipynb
|
||||
- Wait for user input: how-tos/human_in_the_loop/wait-user-input.ipynb
|
||||
- View and update past graph state: how-tos/human_in_the_loop/time-travel.ipynb
|
||||
- Edit graph state: how-tos/human_in_the_loop/edit-graph-state.ipynb
|
||||
- Review tool calls: how-tos/human_in_the_loop/review-tool-calls.ipynb
|
||||
- Streaming:
|
||||
- Stream full state: how-tos/stream-values.ipynb
|
||||
- Stream state updates: how-tos/stream-updates.ipynb
|
||||
@@ -168,6 +171,7 @@ nav:
|
||||
- Visualize your graph: how-tos/visualization.ipynb
|
||||
- Add runtime configuration: how-tos/configuration.ipynb
|
||||
- Add node retries: how-tos/node-retries.ipynb
|
||||
- How to force function calling agent to structure output: how-tos/react-agent-structured-output.ipynb
|
||||
- Prebuilt ReAct Agent:
|
||||
- Create a ReAct agent: how-tos/create-react-agent.ipynb
|
||||
- Add memory to a ReAct agent: how-tos/create-react-agent-memory.ipynb
|
||||
@@ -215,6 +219,7 @@ nav:
|
||||
- Wait for User Input: "cloud/how-tos/human_in_the_loop_user_input.md"
|
||||
- Edit Graph State: "cloud/how-tos/human_in_the_loop_edit_state.md"
|
||||
- Replay and Branch from Prior States: "cloud/how-tos/human_in_the_loop_time_travel.md"
|
||||
- Review Tool Calls: "cloud/how-tos/human_in_the_loop_review_tool_calls.md"
|
||||
- LangGraph Studio:
|
||||
- Test Cloud Deployment: "cloud/how-tos/test_deployment.md"
|
||||
- Test Local Deployment: "cloud/how-tos/test_local_deployment.md"
|
||||
@@ -229,6 +234,8 @@ nav:
|
||||
- Configure Agents: "cloud/how-tos/cloud_examples/configuration_cloud.ipynb"
|
||||
- Convert LangGraph calls to LangGraph Cloud calls: "cloud/how-tos/cloud_examples/langgraph_to_langgraph_cloud.ipynb"
|
||||
- Integrate Webhooks: 'cloud/how-tos/cloud_examples/webhooks.ipynb'
|
||||
- Copy Threads: 'cloud/how-tos/copy_threads.md'
|
||||
- Check Status of Threads: "cloud/how-tos/check_thread_status.md"
|
||||
- Conceptual Guides:
|
||||
- API Concepts: "cloud/concepts/api.md"
|
||||
- Cloud Concepts: "cloud/concepts/cloud.md"
|
||||
@@ -239,6 +246,8 @@ nav:
|
||||
- JS/TS: "cloud/reference/sdk/js_ts_sdk_ref.md"
|
||||
- CLI: "cloud/reference/cli.md"
|
||||
- Environment Variables: "cloud/reference/env_var.md"
|
||||
- FAQ:
|
||||
- Studio: "cloud/faq/studio.md"
|
||||
|
||||
markdown_extensions:
|
||||
- abbr
|
||||
|
||||
@@ -26,7 +26,10 @@
|
||||
"id": "0d30b6f7-3bec-4d9f-af50-43dfdc81ae6c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["# %%capture --no-stderr\n# %pip install -U langgraph langchain langchain_openai"]
|
||||
"source": [
|
||||
"# %%capture --no-stderr\n",
|
||||
"# %pip install -U langgraph langchain langchain_openai"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -34,7 +37,24 @@
|
||||
"id": "30c2f3de-c730-4aec-85a6-af2c2f058803",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n\n\n_set_if_undefined(\"OPENAI_API_KEY\")\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\n\n# Optional, add tracing in LangSmith.\n# This will help you visualize and debug the control flow\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Agent Simulation Evaluation\""]
|
||||
"source": [
|
||||
"import getpass\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _set_if_undefined(var: str):\n",
|
||||
" if not os.environ.get(var):\n",
|
||||
" os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"_set_if_undefined(\"OPENAI_API_KEY\")\n",
|
||||
"_set_if_undefined(\"LANGCHAIN_API_KEY\")\n",
|
||||
"\n",
|
||||
"# Optional, add tracing in LangSmith.\n",
|
||||
"# This will help you visualize and debug the control flow\n",
|
||||
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
|
||||
"os.environ[\"LANGCHAIN_PROJECT\"] = \"Agent Simulation Evaluation\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -55,7 +75,24 @@
|
||||
"id": "828479af-cf9c-4888-a365-599643a96b55",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from typing import List\n\nimport openai\n\n\n# This is flexible, but you can define your agent here, or call your agent API here.\ndef my_chat_bot(messages: List[dict]) -> dict:\n system_message = {\n \"role\": \"system\",\n \"content\": \"You are a customer support agent for an airline.\",\n }\n messages = [system_message] + messages\n completion = openai.chat.completions.create(\n messages=messages, model=\"gpt-3.5-turbo\"\n )\n return completion.choices[0].message.model_dump()"]
|
||||
"source": [
|
||||
"from typing import List\n",
|
||||
"\n",
|
||||
"import openai\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# This is flexible, but you can define your agent here, or call your agent API here.\n",
|
||||
"def my_chat_bot(messages: List[dict]) -> dict:\n",
|
||||
" system_message = {\n",
|
||||
" \"role\": \"system\",\n",
|
||||
" \"content\": \"You are a customer support agent for an airline.\",\n",
|
||||
" }\n",
|
||||
" messages = [system_message] + messages\n",
|
||||
" completion = openai.chat.completions.create(\n",
|
||||
" messages=messages, model=\"gpt-3.5-turbo\"\n",
|
||||
" )\n",
|
||||
" return completion.choices[0].message.model_dump()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -77,7 +114,9 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["my_chat_bot([{\"role\": \"user\", \"content\": \"hi!\"}])"]
|
||||
"source": [
|
||||
"my_chat_bot([{\"role\": \"user\", \"content\": \"hi!\"}])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -96,7 +135,33 @@
|
||||
"id": "32c147df-7f90-4b0d-9a6b-671677020353",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\nfrom langchain_openai import ChatOpenAI\n\nsystem_prompt_template = \"\"\"You are a customer of an airline company. \\\nYou are interacting with a user who is a customer support person. \\\n\n{instructions}\n\nWhen you are finished with the conversation, respond with a single word 'FINISHED'\"\"\"\n\nprompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system_prompt_template),\n MessagesPlaceholder(variable_name=\"messages\"),\n ]\n)\ninstructions = \"\"\"Your name is Harrison. You are trying to get a refund for the trip you took to Alaska. \\\nYou want them to give you ALL the money back. \\\nThis trip happened 5 years ago.\"\"\"\n\nprompt = prompt.partial(name=\"Harrison\", instructions=instructions)\n\nmodel = ChatOpenAI()\n\nsimulated_user = prompt | model"]
|
||||
"source": [
|
||||
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"\n",
|
||||
"system_prompt_template = \"\"\"You are a customer of an airline company. \\\n",
|
||||
"You are interacting with a user who is a customer support person. \\\n",
|
||||
"\n",
|
||||
"{instructions}\n",
|
||||
"\n",
|
||||
"When you are finished with the conversation, respond with a single word 'FINISHED'\"\"\"\n",
|
||||
"\n",
|
||||
"prompt = ChatPromptTemplate.from_messages(\n",
|
||||
" [\n",
|
||||
" (\"system\", system_prompt_template),\n",
|
||||
" MessagesPlaceholder(variable_name=\"messages\"),\n",
|
||||
" ]\n",
|
||||
")\n",
|
||||
"instructions = \"\"\"Your name is Harrison. You are trying to get a refund for the trip you took to Alaska. \\\n",
|
||||
"You want them to give you ALL the money back. \\\n",
|
||||
"This trip happened 5 years ago.\"\"\"\n",
|
||||
"\n",
|
||||
"prompt = prompt.partial(name=\"Harrison\", instructions=instructions)\n",
|
||||
"\n",
|
||||
"model = ChatOpenAI()\n",
|
||||
"\n",
|
||||
"simulated_user = prompt | model"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -115,7 +180,12 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["from langchain_core.messages import HumanMessage\n\nmessages = [HumanMessage(content=\"Hi! How can I help you?\")]\nsimulated_user.invoke({\"messages\": messages})"]
|
||||
"source": [
|
||||
"from langchain_core.messages import HumanMessage\n",
|
||||
"\n",
|
||||
"messages = [HumanMessage(content=\"Hi! How can I help you?\")]\n",
|
||||
"simulated_user.invoke({\"messages\": messages})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -153,7 +223,20 @@
|
||||
"id": "69e2a3a3-40f3-4223-9136-113738440be9",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langchain_community.adapters.openai import convert_message_to_dict\nfrom langchain_core.messages import AIMessage\n\n\ndef chat_bot_node(messages):\n # Convert from LangChain format to the OpenAI format, which our chatbot function expects.\n messages = [convert_message_to_dict(m) for m in messages]\n # Call the chat bot\n chat_bot_response = my_chat_bot(messages)\n # Respond with an AI Message\n return AIMessage(content=chat_bot_response[\"content\"])"]
|
||||
"source": [
|
||||
"from langchain_community.adapters.openai import convert_message_to_dict\n",
|
||||
"from langchain_core.messages import AIMessage\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def chat_bot_node(state):\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
" # Convert from LangChain format to the OpenAI format, which our chatbot function expects.\n",
|
||||
" messages = [convert_message_to_dict(m) for m in messages]\n",
|
||||
" # Call the chat bot\n",
|
||||
" chat_bot_response = my_chat_bot(messages)\n",
|
||||
" # Respond with an AI Message\n",
|
||||
" return {\"messages\": [AIMessage(content=chat_bot_response[\"content\"])]}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -169,7 +252,26 @@
|
||||
"id": "7cad7527-ffa5-4c30-8585-b54a7a18bd98",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["def _swap_roles(messages):\n new_messages = []\n for m in messages:\n if isinstance(m, AIMessage):\n new_messages.append(HumanMessage(content=m.content))\n else:\n new_messages.append(AIMessage(content=m.content))\n return new_messages\n\n\ndef simulated_user_node(messages):\n # Swap roles of messages\n new_messages = _swap_roles(messages)\n # Call the simulated user\n response = simulated_user.invoke({\"messages\": new_messages})\n # This response is an AI message - we need to flip this to be a human message\n return HumanMessage(content=response.content)"]
|
||||
"source": [
|
||||
"def _swap_roles(messages):\n",
|
||||
" new_messages = []\n",
|
||||
" for m in messages:\n",
|
||||
" if isinstance(m, AIMessage):\n",
|
||||
" new_messages.append(HumanMessage(content=m.content))\n",
|
||||
" else:\n",
|
||||
" new_messages.append(AIMessage(content=m.content))\n",
|
||||
" return new_messages\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def simulated_user_node(state):\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
" # Swap roles of messages\n",
|
||||
" new_messages = _swap_roles(messages)\n",
|
||||
" # Call the simulated user\n",
|
||||
" response = simulated_user.invoke({\"messages\": new_messages})\n",
|
||||
" # This response is an AI message - we need to flip this to be a human message\n",
|
||||
" return {\"messages\": [HumanMessage(content=response.content)]}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -192,7 +294,16 @@
|
||||
"id": "28004fbf-a2f3-46b7-bde7-46c7adaf97fb",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["def should_continue(messages):\n if len(messages) > 6:\n return \"end\"\n elif messages[-1].content == \"FINISHED\":\n return \"end\"\n else:\n return \"continue\""]
|
||||
"source": [
|
||||
"def should_continue(state):\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
" if len(messages) > 6:\n",
|
||||
" return \"end\"\n",
|
||||
" elif messages[-1].content == \"FINISHED\":\n",
|
||||
" return \"end\"\n",
|
||||
" else:\n",
|
||||
" return \"continue\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -210,7 +321,37 @@
|
||||
"id": "0b597e4b-4cbb-4bbc-82e5-f7e31275964c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langgraph.graph import END, MessageGraph, START\n\ngraph_builder = MessageGraph()\ngraph_builder.add_node(\"user\", simulated_user_node)\ngraph_builder.add_node(\"chat_bot\", chat_bot_node)\n# Every response from your chat bot will automatically go to the\n# simulated user\ngraph_builder.add_edge(\"chat_bot\", \"user\")\ngraph_builder.add_conditional_edges(\n \"user\",\n should_continue,\n # If the finish criteria are met, we will stop the simulation,\n # otherwise, the virtual user's message will be sent to your chat bot\n {\n \"end\": END,\n \"continue\": \"chat_bot\",\n },\n)\n# The input will first go to your chat bot\ngraph_builder.add_edge(START, \"chat_bot\")\nsimulation = graph_builder.compile()"]
|
||||
"source": [
|
||||
"from langgraph.graph import END, StateGraph, START\n",
|
||||
"from langgraph.graph.message import add_messages\n",
|
||||
"from typing import Annotated\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class State(TypedDict):\n",
|
||||
" messages: Annotated[list, add_messages]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"graph_builder = StateGraph(State)\n",
|
||||
"graph_builder.add_node(\"user\", simulated_user_node)\n",
|
||||
"graph_builder.add_node(\"chat_bot\", chat_bot_node)\n",
|
||||
"# Every response from your chat bot will automatically go to the\n",
|
||||
"# simulated user\n",
|
||||
"graph_builder.add_edge(\"chat_bot\", \"user\")\n",
|
||||
"graph_builder.add_conditional_edges(\n",
|
||||
" \"user\",\n",
|
||||
" should_continue,\n",
|
||||
" # If the finish criteria are met, we will stop the simulation,\n",
|
||||
" # otherwise, the virtual user's message will be sent to your chat bot\n",
|
||||
" {\n",
|
||||
" \"end\": END,\n",
|
||||
" \"continue\": \"chat_bot\",\n",
|
||||
" },\n",
|
||||
")\n",
|
||||
"# The input will first go to your chat bot\n",
|
||||
"graph_builder.add_edge(START, \"chat_bot\")\n",
|
||||
"simulation = graph_builder.compile()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -251,7 +392,13 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["for chunk in simulation.stream([]):\n # Print out all events aside from the final end chunk\n if END not in chunk:\n print(chunk)\n print(\"----\")"]
|
||||
"source": [
|
||||
"for chunk in simulation.stream({}):\n",
|
||||
" # Print out all events aside from the final end chunk\n",
|
||||
" if END not in chunk:\n",
|
||||
" print(chunk)\n",
|
||||
" print(\"----\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -259,7 +406,7 @@
|
||||
"id": "dde4f2b5-cfe8-4ff0-99ea-fe2c5fed70c0",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [""]
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -239,7 +239,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.1"
|
||||
"version": "3.12.2"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -225,7 +225,14 @@
|
||||
"\n",
|
||||
"Define the (`fetch_user_flight_information`) tool to let the agent see the current user's flight information. Then define tools to search for flights and manage the passenger's bookings stored in the SQL database.\n",
|
||||
"\n",
|
||||
"We use `ensure_config` to pass in the `passenger_id` in via configurable parameters. The LLM never has to provide these explicitly, they are provided for a given invocation of the graph so that each user cannot access other passengers' booking information."
|
||||
"We the can [access the RunnableConfig](https://python.langchain.com/v0.2/docs/how_to/tool_configure/#inferring-by-parameter-type) for a given run to check the `passenger_id` of the user accessing this application. The LLM never has to provide these explicitly, they are provided for a given invocation of the graph so that each user cannot access other passengers' booking information.\n",
|
||||
"\n",
|
||||
"<div class=\"admonition warning\">\n",
|
||||
" <p class=\"admonition-title\">Compatibility</p>\n",
|
||||
" <p>\n",
|
||||
" This tutorial expects `langchain-core>=0.2.16` to use the injected RunnableConfig. Prior to that, you'd use `ensure_config` to collect the config from context.\n",
|
||||
" </p>\n",
|
||||
"</div> \n"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -240,18 +247,17 @@
|
||||
"from typing import Optional\n",
|
||||
"\n",
|
||||
"import pytz\n",
|
||||
"from langchain_core.runnables import ensure_config\n",
|
||||
"from langchain_core.runnables import RunnableConfig\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def fetch_user_flight_information() -> list[dict]:\n",
|
||||
"def fetch_user_flight_information(config: RunnableConfig) -> list[dict]:\n",
|
||||
" \"\"\"Fetch all tickets for the user along with corresponding flight information and seat assignments.\n",
|
||||
"\n",
|
||||
" Returns:\n",
|
||||
" A list of dictionaries where each dictionary contains the ticket details,\n",
|
||||
" associated flight details, and the seat assignments for each ticket belonging to the user.\n",
|
||||
" \"\"\"\n",
|
||||
" config = ensure_config() # Fetch from the context\n",
|
||||
" configuration = config.get(\"configurable\", {})\n",
|
||||
" passenger_id = configuration.get(\"passenger_id\", None)\n",
|
||||
" if not passenger_id:\n",
|
||||
@@ -328,9 +334,10 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def update_ticket_to_new_flight(ticket_no: str, new_flight_id: int) -> str:\n",
|
||||
"def update_ticket_to_new_flight(\n",
|
||||
" ticket_no: str, new_flight_id: int, *, config: RunnableConfig\n",
|
||||
") -> str:\n",
|
||||
" \"\"\"Update the user's ticket to a new valid flight.\"\"\"\n",
|
||||
" config = ensure_config()\n",
|
||||
" configuration = config.get(\"configurable\", {})\n",
|
||||
" passenger_id = configuration.get(\"passenger_id\", None)\n",
|
||||
" if not passenger_id:\n",
|
||||
@@ -396,9 +403,8 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def cancel_ticket(ticket_no: str) -> str:\n",
|
||||
"def cancel_ticket(ticket_no: str, *, config: RunnableConfig) -> str:\n",
|
||||
" \"\"\"Cancel the user's ticket and remove it from the database.\"\"\"\n",
|
||||
" config = ensure_config()\n",
|
||||
" configuration = config.get(\"configurable\", {})\n",
|
||||
" passenger_id = configuration.get(\"passenger_id\", None)\n",
|
||||
" if not passenger_id:\n",
|
||||
@@ -4407,7 +4413,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.2"
|
||||
"version": "3.12.2"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
+113
-30
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -33,15 +33,19 @@
|
||||
"from langgraph.graph import StateGraph, START, END\n",
|
||||
"from typing import TypedDict\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class InputState(TypedDict):\n",
|
||||
" question: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class OutputState(TypedDict):\n",
|
||||
" answer: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def answer_node(state: InputState):\n",
|
||||
" return {\"answer\": \"bye\"}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"graph = StateGraph(input=InputState, output=OutputState)\n",
|
||||
"graph.add_node(answer_node)\n",
|
||||
"graph.add_edge(START, \"answer_node\")\n",
|
||||
|
||||
+30
-37
@@ -174,7 +174,7 @@
|
||||
"id": "b6c1dcd9-fb86-4649-81b4-ff6ce20a2e46",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"**Notice** how the `chatbot` node function takes the current `State` as input and returns an updated `messages` list. This is the basic pattern for all LangGraph node functions.\n",
|
||||
"**Notice** how the `chatbot` node function takes the current `State` as input and returns a dictionary containing an updated `messages` list under the key \"messages\". This is the basic pattern for all LangGraph node functions.\n",
|
||||
"\n",
|
||||
"The `add_messages` function in our `State` will append the llm's response messages to whatever messages are already in the state.\n",
|
||||
"\n",
|
||||
@@ -1256,19 +1256,10 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"execution_count": 7,
|
||||
"id": "5a81608a-373a-4339-b1c6-65b73a92b983",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"/Users/wfh/code/lc/langchain/libs/core/langchain_core/_api/beta_decorator.py:87: LangChainBetaWarning: The method `ChatAnthropic.bind_tools` is in beta. It is actively being worked on, so the API may change.\n",
|
||||
" warn_beta(\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import Annotated\n",
|
||||
"\n",
|
||||
@@ -1320,12 +1311,12 @@
|
||||
"id": "813505b2-18c1-46e9-b891-20a34232808b",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Now, compile the graph, specifying to `interrupt_before` the `action` node."
|
||||
"Now, compile the graph, specifying to `interrupt_before` the `tools` node."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": 8,
|
||||
"id": "b0883e32-1a39-4ce9-ae32-bbd66708fd84",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -1334,14 +1325,14 @@
|
||||
" checkpointer=memory,\n",
|
||||
" # This is new!\n",
|
||||
" interrupt_before=[\"tools\"],\n",
|
||||
" # Note: can also interrupt __after__ actions, if desired.\n",
|
||||
" # Note: can also interrupt __after__ tools, if desired.\n",
|
||||
" # interrupt_after=[\"tools\"]\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"execution_count": 9,
|
||||
"id": "9f318020-ab7e-415b-a5e2-eddec6d9f3a6",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -1354,10 +1345,10 @@
|
||||
"I'm learning LangGraph. Could you do some research on it for me?\n",
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"[{'text': \"Okay, let's do some research on LangGraph:\", 'type': 'text'}, {'id': 'toolu_01Be7aRgMEv9cg6ezaFjiCry', 'input': {'query': 'LangGraph'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n",
|
||||
"[{'text': \"Okay, let's look up some information on LangGraph:\", 'type': 'text'}, {'id': 'toolu_01XoHVKTRbipJokQorfifzvh', 'input': {'query': 'LangGraph'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n",
|
||||
"Tool Calls:\n",
|
||||
" tavily_search_results_json (toolu_01Be7aRgMEv9cg6ezaFjiCry)\n",
|
||||
" Call ID: toolu_01Be7aRgMEv9cg6ezaFjiCry\n",
|
||||
" tavily_search_results_json (toolu_01XoHVKTRbipJokQorfifzvh)\n",
|
||||
" Call ID: toolu_01XoHVKTRbipJokQorfifzvh\n",
|
||||
" Args:\n",
|
||||
" query: LangGraph\n"
|
||||
]
|
||||
@@ -1385,17 +1376,17 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"execution_count": 10,
|
||||
"id": "9bb7af46-9b4f-4bb1-b8b9-e9ddf7dbc82c",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"('action',)"
|
||||
"('tools',)"
|
||||
]
|
||||
},
|
||||
"execution_count": 4,
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
@@ -1410,12 +1401,12 @@
|
||||
"id": "89326046-2b11-4812-8b6d-8780306ec275",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"**Notice** that unlike last time, the \"next\" node is set to **'action'**. We've interrupted here! Let's check the tool invocation."
|
||||
"**Notice** that unlike last time, the \"next\" node is set to **'tools'**. We've interrupted here! Let's check the tool invocation."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"execution_count": 11,
|
||||
"id": "3facda0a-e6ad-4b28-b627-753ad8c90c15",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -1424,10 +1415,11 @@
|
||||
"text/plain": [
|
||||
"[{'name': 'tavily_search_results_json',\n",
|
||||
" 'args': {'query': 'LangGraph'},\n",
|
||||
" 'id': 'toolu_01Be7aRgMEv9cg6ezaFjiCry'}]"
|
||||
" 'id': 'toolu_01XoHVKTRbipJokQorfifzvh',\n",
|
||||
" 'type': 'tool_call'}]"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
@@ -1449,7 +1441,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"execution_count": 12,
|
||||
"id": "effb95d9-b7d5-40c5-9253-253d193b23b2",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -1460,18 +1452,19 @@
|
||||
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
|
||||
"Name: tavily_search_results_json\n",
|
||||
"\n",
|
||||
"[{\"url\": \"https://github.com/langchain-ai/langgraph\", \"content\": \"LangGraph is a Python package that extends LangChain Expression Language with the ability to coordinate multiple chains across multiple steps of computation in a cyclic manner. It is inspired by Pregel and Apache Beam and can be used for agent-like behaviors, such as chatbots, with LLMs.\"}, {\"url\": \"https://langchain-ai.github.io/langgraph//\", \"content\": \"LangGraph is a library for building stateful, multi-actor applications with LLMs, built on top of (and intended to be used with) LangChain . It extends the LangChain Expression Language with the ability to coordinate multiple chains (or actors) across multiple steps of computation in a cyclic manner. It is inspired by Pregel and Apache Beam .\"}]\n",
|
||||
"[{\"url\": \"https://langchain-ai.github.io/langgraph/tutorials/\", \"content\": \"LangGraph is a framework for building language agents as graphs. Learn how to use LangGraph to create chatbots, code assistants, planning agents, reflection agents, and more with these notebooks.\"}, {\"url\": \"https://github.com/langchain-ai/langgraph\", \"content\": \"LangGraph is a library for creating stateful, multi-actor applications with LLMs, using cycles, controllability, and persistence. Learn how to use LangGraph with examples, integration with LangChain, and streaming support.\"}]\n",
|
||||
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||
"\n",
|
||||
"Based on the search results, LangGraph seems to be a Python library that extends the LangChain library to enable more complex, multi-step interactions with large language models (LLMs). Some key points:\n",
|
||||
"Based on the search results, LangGraph seems to be a framework for building language-based AI agents and applications using language models. It provides a modular, graph-based approach for creating chatbots, code assistants, planning agents, and other language-centric applications.\n",
|
||||
"\n",
|
||||
"- LangGraph allows coordinating multiple \"chains\" (or actors) over multiple steps of computation, in a cyclic manner. This enables more advanced agent-like behaviors like chatbots.\n",
|
||||
"- It is inspired by distributed graph processing frameworks like Pregel and Apache Beam.\n",
|
||||
"- LangGraph is built on top of the LangChain library, which provides a framework for building applications with LLMs.\n",
|
||||
"Some key things I learned about LangGraph:\n",
|
||||
"\n",
|
||||
"So in summary, LangGraph appears to be a powerful tool for building more sophisticated applications and agents using large language models, by allowing you to coordinate multiple steps and actors in a flexible, graph-like manner. It extends the capabilities of the base LangChain library.\n",
|
||||
"- It is designed to make it easier to build stateful, multi-actor applications using large language models (LLMs).\n",
|
||||
"- It provides features like cycles, controllability, and persistence to help manage the complexity of these types of applications.\n",
|
||||
"- LangGraph can be integrated with the LangChain library, which provides additional tools for building LLM-powered applications.\n",
|
||||
"- The framework includes examples and tutorials to help get started with using LangGraph.\n",
|
||||
"\n",
|
||||
"Let me know if you need any clarification or have additional questions!\n"
|
||||
"Overall, LangGraph seems like a promising approach for building more advanced, graph-based language applications on top of large language models. Let me know if you need any other details on LangGraph and how it works!\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -1563,7 +1556,7 @@
|
||||
"source": [
|
||||
"## Part 5: Manually Updating the State\n",
|
||||
"\n",
|
||||
"In the previous section, we showed how to interrupt a graph so that a human could inspect its actions. This lets the human `read` the state, but if they want to change they agent's course, they'll need to have `write` access.\n",
|
||||
"In the previous section, we showed how to interrupt a graph so that a human could inspect its actions. This lets the human `read` the state, but if they want to change their agent's course, they'll need to have `write` access.\n",
|
||||
"\n",
|
||||
"Thankfully, LangGraph lets you **manually update state**! Updating the state lets you control the agent's trajectory by modifying its actions (even modifying the past!). This capability is particularly useful when you want to correct the agent's mistakes, explore alternative paths, or guide the agent towards a specific goal.\n",
|
||||
"\n",
|
||||
@@ -3068,9 +3061,9 @@
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "langgraph",
|
||||
"display_name": "env",
|
||||
"language": "python",
|
||||
"name": "langgraph"
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -328,9 +328,9 @@
|
||||
" \"set more_information_needed False and populate a blank string for the query.\"\n",
|
||||
" )\n",
|
||||
" input_messages = [system] + state[\"messages\"]\n",
|
||||
" response = llm.bind_tools(\n",
|
||||
" [QueryForTools], tool_choice=True\n",
|
||||
" ).invoke(input_messages)\n",
|
||||
" response = llm.bind_tools([QueryForTools], tool_choice=True).invoke(\n",
|
||||
" input_messages\n",
|
||||
" )\n",
|
||||
" query = response.tool_calls[0][\"args\"][\"query\"]\n",
|
||||
" tool_documents = vector_store.similarity_search(query)\n",
|
||||
" if hack_remove_tool_condition:\n",
|
||||
|
||||
@@ -268,7 +268,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"def filter_messages(messages: list):\n",
|
||||
" # This is very simple helper function which only ever uses the last two messages\n",
|
||||
" # This is very simple helper function which only ever uses the last message\n",
|
||||
" return messages[-1:]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
@@ -372,9 +372,9 @@
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"display_name": "langgraph-example-dev",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
"name": "langgraph-example-dev"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
@@ -386,7 +386,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.1"
|
||||
"version": "3.11.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -329,6 +329,7 @@
|
||||
"\n",
|
||||
"tools = [get_context, cite_context_sources]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Define the function that calls the model\n",
|
||||
"def call_model(state, config):\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
|
||||
@@ -72,12 +72,12 @@
|
||||
"# Node to retrieve documents\n",
|
||||
"def retrieve_documents(state: QueryOutputState) -> DocumentOutputState:\n",
|
||||
" # Replace this with real logic\n",
|
||||
" return {\"docs\": [state['query']] * 2}\n",
|
||||
" return {\"docs\": [state[\"query\"]] * 2}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Node to generate answer\n",
|
||||
"def generate(state: GenerateInputState) -> OverallState:\n",
|
||||
" return {\"answer\": \"\\n\\n\".join(state['docs'] + [state['question']])}\n",
|
||||
" return {\"answer\": \"\\n\\n\".join(state[\"docs\"] + [state[\"question\"]])}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"graph = StateGraph(OverallState)\n",
|
||||
|
||||
@@ -11,10 +11,16 @@
|
||||
"\n",
|
||||
"When creating any LangGraph workflow, you can set them up to persist their state by doing using the following:\n",
|
||||
"\n",
|
||||
"1. A [Checkpointer](https://langchain-ai.github.io/langgraph/reference/checkpoints/#basecheckpointsaver), such as the [AsyncSqliteSaver](https://langchain-ai.github.io/langgraph/reference/checkpoints/#asyncsqlitesaver)\n",
|
||||
"1. A [Checkpointer](https://langchain-ai.github.io/langgraph/reference/checkpoints/#basecheckpointsaver).\n",
|
||||
"2. Call `compile(checkpointer=my_checkpointer)` when compiling the graph.\n",
|
||||
"\n",
|
||||
"Example:\n",
|
||||
"There are several options for checkpointers to use.\n",
|
||||
"\n",
|
||||
"1. [MemorySaver](https://langchain-ai.github.io/langgraph/reference/checkpoints/#memorysaver) is an in-memory key-value store for Graph state.\n",
|
||||
"2. [SqliteSaver](https://langchain-ai.github.io/langgraph/reference/checkpoints/#sqlitesaver) allows you to save to a Sqlite db locally or in memory.\n",
|
||||
"3. There are various external databases that can be used for persistence, such as [Postgres](https://langchain-ai.github.io/langgraph/how-tos/persistence_postgres/), [MongoDB](https://langchain-ai.github.io/langgraph/how-tos/persistence_mongodb/), and [Redis](https://langchain-ai.github.io/langgraph/how-tos/persistence_redis/).\n",
|
||||
" \n",
|
||||
"Here is an example using [MemorySaver](https://langchain-ai.github.io/langgraph/reference/checkpoints/#memorysaver) in memory:\n",
|
||||
"```python\n",
|
||||
"from langgraph.graph import StateGraph\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
@@ -581,7 +587,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
"version": "3.12.2"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
+920
-1010
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,13 @@
|
||||
"\n",
|
||||
"When creating LangGraph agents, you can also set them up so that they persist their state. This allows you to do things like interact with an agent multiple times and have it remember previous interactions.\n",
|
||||
"\n",
|
||||
"This example shows how to use `Postgres` as the backend for persisting checkpoint state using [`langgraph-checkpoint-postgres`](https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-postgres) library."
|
||||
"This example shows how to use `Postgres` as the backend for persisting checkpoint state using [`langgraph-checkpoint-postgres`](https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-postgres) library.\n",
|
||||
"\n",
|
||||
"To start a Postgres database to work with you can do the following:\n",
|
||||
"\n",
|
||||
"```\n",
|
||||
"$ cd libs/langgraph\n",
|
||||
"$ make start-postgres"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -38,7 +44,7 @@
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdin",
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"OPENAI_API_KEY: ········\n"
|
||||
@@ -74,7 +80,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import Literal\n",
|
||||
"from langchain_core.runnables import ConfigurableField\n",
|
||||
"\n",
|
||||
"from langchain_core.tools import tool\n",
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"from langgraph.prebuilt import create_react_agent\n",
|
||||
@@ -102,7 +108,11 @@
|
||||
"id": "e9342c62-dbb4-40f6-9271-7393f1ca48c4",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Use sync connection"
|
||||
"## Use sync connection\n",
|
||||
"\n",
|
||||
"This sets up a synchronous connection to the database. \n",
|
||||
"\n",
|
||||
"Synchronous connections execute operations in a blocking manner, meaning each operation waits for completion before moving to the next one. The `DB_URI` is the database connection URI, with the protocol used for connecting to a PostgreSQL database, authentication, and host where database is running. The connection_kwargs dictionary defines additional parameters for the database connection."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -124,10 +134,9 @@
|
||||
"source": [
|
||||
"from psycopg.rows import dict_row\n",
|
||||
"\n",
|
||||
"connection_kwargs ={\n",
|
||||
"connection_kwargs = {\n",
|
||||
" \"autocommit\": True,\n",
|
||||
" \"prepare_threshold\": 0,\n",
|
||||
" \"row_factory\": dict_row,\n",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
@@ -136,7 +145,11 @@
|
||||
"id": "e39fc712-9e1c-4831-9077-dd07b0c13594",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### With a connection pool"
|
||||
"### With a connection pool\n",
|
||||
"\n",
|
||||
"This manages a pool of reusable database connections: \n",
|
||||
"- Advantages: Efficient resource utilization, improved performance for frequent connections\n",
|
||||
"- Best for: Applications with many short-lived database operations\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -152,7 +165,7 @@
|
||||
" # Example configuration\n",
|
||||
" conninfo=DB_URI,\n",
|
||||
" max_size=20,\n",
|
||||
" kwargs=connection_kwargs\n",
|
||||
" kwargs=connection_kwargs,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"with pool.connection() as conn:\n",
|
||||
@@ -237,7 +250,11 @@
|
||||
"id": "967c95c7-e392-4819-bd71-f29e91c68df3",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### With a connection"
|
||||
"### With a connection\n",
|
||||
"\n",
|
||||
"This creates a single, dedicated connection to the database:\n",
|
||||
"- Advantages: Simple to use, suitable for longer transactions\n",
|
||||
"- Best for: Applications with fewer, longer-lived database operations"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -252,6 +269,8 @@
|
||||
"\n",
|
||||
"with Connection.connect(DB_URI, **connection_kwargs) as conn:\n",
|
||||
" checkpointer = PostgresSaver(conn)\n",
|
||||
" # NOTE: you need to call .setup() the first time you're using your checkpointer\n",
|
||||
" # checkpointer.setup()\n",
|
||||
" graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n",
|
||||
" config = {\"configurable\": {\"thread_id\": \"2\"}}\n",
|
||||
" res = graph.invoke({\"messages\": [(\"human\", \"what's the weather in sf\")]}, config)\n",
|
||||
@@ -285,7 +304,11 @@
|
||||
"id": "49fb52fd-af31-4603-889d-66d783244bce",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### With a connection string"
|
||||
"### With a connection string\n",
|
||||
"\n",
|
||||
"This creates a connection based on a connection string:\n",
|
||||
"- Advantages: Simplicity, encapsulates connection details\n",
|
||||
"- Best for: Quick setup or when connection details are provided as a string"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -295,8 +318,6 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from psycopg import Connection\n",
|
||||
"\n",
|
||||
"with PostgresSaver.from_conn_string(DB_URI) as checkpointer:\n",
|
||||
" graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n",
|
||||
" config = {\"configurable\": {\"thread_id\": \"3\"}}\n",
|
||||
@@ -335,7 +356,11 @@
|
||||
"id": "c0a47d3e-e588-48fc-a5d4-2145dff17e77",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Use async connection"
|
||||
"## Use async connection\n",
|
||||
"\n",
|
||||
"This sets up an asynchronous connection to the database. \n",
|
||||
"\n",
|
||||
"Async connections allow non-blocking database operations. This means other parts of your application can continue running while waiting for database operations to complete. It's particularly useful in high-concurrency scenarios or when dealing with I/O-bound operations."
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -364,14 +389,12 @@
|
||||
"source": [
|
||||
"from psycopg_pool import AsyncConnectionPool\n",
|
||||
"\n",
|
||||
"pool = AsyncConnectionPool(\n",
|
||||
"async with AsyncConnectionPool(\n",
|
||||
" # Example configuration\n",
|
||||
" conninfo=DB_URI,\n",
|
||||
" max_size=20,\n",
|
||||
" kwargs=connection_kwargs\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"async with pool.connection() as conn:\n",
|
||||
" kwargs=connection_kwargs,\n",
|
||||
") as pool, pool.connection() as conn:\n",
|
||||
" checkpointer = AsyncPostgresSaver(conn)\n",
|
||||
"\n",
|
||||
" # NOTE: you need to call .setup() the first time you're using your checkpointer\n",
|
||||
@@ -527,9 +550,9 @@
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "langgraph-postgres",
|
||||
"display_name": "langgraph",
|
||||
"language": "python",
|
||||
"name": "langgraph-postgres"
|
||||
"name": "langgraph"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
|
||||
+1012
-839
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,10 @@
|
||||
"id": "969fb438",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["%%capture --no-stderr\n%pip install -U --quiet langchain-community tiktoken langchain-openai langchainhub chromadb langchain langgraph langchain-text-splitters"]
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U --quiet langchain-community tiktoken langchain-openai langchainhub chromadb langchain langgraph langchain-text-splitters"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -28,7 +31,22 @@
|
||||
"id": "e4958a8c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["import getpass\nimport os\n\n\ndef _set_env(key: str):\n if key not in os.environ:\n os.environ[key] = getpass.getpass(f\"{key}:\")\n\n\n_set_env(\"OPENAI_API_KEY\")\n\n# (Optional) For tracing\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
|
||||
"source": [
|
||||
"import getpass\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _set_env(key: str):\n",
|
||||
" if key not in os.environ:\n",
|
||||
" os.environ[key] = getpass.getpass(f\"{key}:\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"_set_env(\"OPENAI_API_KEY\")\n",
|
||||
"\n",
|
||||
"# (Optional) For tracing\n",
|
||||
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
|
||||
"_set_env(\"LANGCHAIN_API_KEY\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -46,7 +64,34 @@
|
||||
"id": "e50c9efe-4abe-42fa-b35a-05eeeede9ec6",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_openai import OpenAIEmbeddings\nfrom langchain_text_splitters import RecursiveCharacterTextSplitter\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=100, chunk_overlap=50\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=OpenAIEmbeddings(),\n)\nretriever = vectorstore.as_retriever()"]
|
||||
"source": [
|
||||
"from langchain_community.document_loaders import WebBaseLoader\n",
|
||||
"from langchain_community.vectorstores import Chroma\n",
|
||||
"from langchain_openai import OpenAIEmbeddings\n",
|
||||
"from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
|
||||
"\n",
|
||||
"urls = [\n",
|
||||
" \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n",
|
||||
" \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n",
|
||||
" \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"docs = [WebBaseLoader(url).load() for url in urls]\n",
|
||||
"docs_list = [item for sublist in docs for item in sublist]\n",
|
||||
"\n",
|
||||
"text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n",
|
||||
" chunk_size=100, chunk_overlap=50\n",
|
||||
")\n",
|
||||
"doc_splits = text_splitter.split_documents(docs_list)\n",
|
||||
"\n",
|
||||
"# Add to vectorDB\n",
|
||||
"vectorstore = Chroma.from_documents(\n",
|
||||
" documents=doc_splits,\n",
|
||||
" collection_name=\"rag-chroma\",\n",
|
||||
" embedding=OpenAIEmbeddings(),\n",
|
||||
")\n",
|
||||
"retriever = vectorstore.as_retriever()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -62,7 +107,17 @@
|
||||
"id": "0b97bdd8-d7e3-444d-ac96-5ef4725f9048",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langchain.tools.retriever import create_retriever_tool\n\nretriever_tool = create_retriever_tool(\n retriever,\n \"retrieve_blog_posts\",\n \"Search and return information about Lilian Weng blog posts on LLM agents, prompt engineering, and adversarial attacks on LLMs.\",\n)\n\ntools = [retriever_tool]"]
|
||||
"source": [
|
||||
"from langchain.tools.retriever import create_retriever_tool\n",
|
||||
"\n",
|
||||
"retriever_tool = create_retriever_tool(\n",
|
||||
" retriever,\n",
|
||||
" \"retrieve_blog_posts\",\n",
|
||||
" \"Search and return information about Lilian Weng blog posts on LLM agents, prompt engineering, and adversarial attacks on LLMs.\",\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"tools = [retriever_tool]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -86,7 +141,19 @@
|
||||
"id": "0e378706-47d5-425a-8ba0-57b9acffbd0c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\nfrom langgraph.graph.message import add_messages\n\n\nclass AgentState(TypedDict):\n # The add_messages function defines how an update should be processed\n # Default is to replace. add_messages says \"append\"\n messages: Annotated[Sequence[BaseMessage], add_messages]"]
|
||||
"source": [
|
||||
"from typing import Annotated, Sequence, TypedDict\n",
|
||||
"\n",
|
||||
"from langchain_core.messages import BaseMessage\n",
|
||||
"\n",
|
||||
"from langgraph.graph.message import add_messages\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class AgentState(TypedDict):\n",
|
||||
" # The add_messages function defines how an update should be processed\n",
|
||||
" # Default is to replace. add_messages says \"append\"\n",
|
||||
" messages: Annotated[Sequence[BaseMessage], add_messages]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"attachments": {
|
||||
@@ -129,7 +196,173 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["from typing import Annotated, Literal, Sequence, TypedDict\n\nfrom langchain import hub\nfrom langchain_core.messages import BaseMessage, HumanMessage\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.prompts import PromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\nfrom langgraph.prebuilt import tools_condition\n\n### Edges\n\n\ndef grade_documents(state) -> Literal[\"generate\", \"rewrite\"]:\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (messages): The current state\n\n Returns:\n str: A decision for whether the documents are relevant or not\n \"\"\"\n\n print(\"---CHECK RELEVANCE---\")\n\n # Data model\n class grade(BaseModel):\n \"\"\"Binary score for relevance check.\"\"\"\n\n binary_score: str = Field(description=\"Relevance score 'yes' or 'no'\")\n\n # LLM\n model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n\n # LLM with tool and validation\n llm_with_tool = model.with_structured_output(grade)\n\n # Prompt\n prompt = PromptTemplate(\n template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n Here is the retrieved document: \\n\\n {context} \\n\\n\n Here is the user question: {question} \\n\n If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\",\n input_variables=[\"context\", \"question\"],\n )\n\n # Chain\n chain = prompt | llm_with_tool\n\n messages = state[\"messages\"]\n last_message = messages[-1]\n\n question = messages[0].content\n docs = last_message.content\n\n scored_result = chain.invoke({\"question\": question, \"context\": docs})\n\n score = scored_result.binary_score\n\n if score == \"yes\":\n print(\"---DECISION: DOCS RELEVANT---\")\n return \"generate\"\n\n else:\n print(\"---DECISION: DOCS NOT RELEVANT---\")\n print(score)\n return \"rewrite\"\n\n\n### Nodes\n\n\ndef agent(state):\n \"\"\"\n Invokes the agent model to generate a response based on the current state. Given\n the question, it will decide to retrieve using the retriever tool, or simply end.\n\n Args:\n state (messages): The current state\n\n Returns:\n dict: The updated state with the agent response appended to messages\n \"\"\"\n print(\"---CALL AGENT---\")\n messages = state[\"messages\"]\n model = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4-turbo\")\n model = model.bind_tools(tools)\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\ndef rewrite(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (messages): The current state\n\n Returns:\n dict: The updated state with re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n messages = state[\"messages\"]\n question = messages[0].content\n\n msg = [\n HumanMessage(\n content=f\"\"\" \\n \n Look at the input and try to reason about the underlying semantic intent / meaning. \\n \n Here is the initial question:\n \\n ------- \\n\n {question} \n \\n ------- \\n\n Formulate an improved question: \"\"\",\n )\n ]\n\n # Grader\n model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n response = model.invoke(msg)\n return {\"messages\": [response]}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (messages): The current state\n\n Returns:\n dict: The updated state with re-phrased question\n \"\"\"\n print(\"---GENERATE---\")\n messages = state[\"messages\"]\n question = messages[0].content\n last_message = messages[-1]\n\n question = messages[0].content\n docs = last_message.content\n\n # Prompt\n prompt = hub.pull(\"rlm/rag-prompt\")\n\n # LLM\n llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0, streaming=True)\n\n # Post-processing\n def format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n # Chain\n rag_chain = prompt | llm | StrOutputParser()\n\n # Run\n response = rag_chain.invoke({\"context\": docs, \"question\": question})\n return {\"messages\": [response]}\n\n\nprint(\"*\" * 20 + \"Prompt[rlm/rag-prompt]\" + \"*\" * 20)\nprompt = hub.pull(\"rlm/rag-prompt\").pretty_print() # Show what the prompt looks like"]
|
||||
"source": [
|
||||
"from typing import Annotated, Literal, Sequence, TypedDict\n",
|
||||
"\n",
|
||||
"from langchain import hub\n",
|
||||
"from langchain_core.messages import BaseMessage, HumanMessage\n",
|
||||
"from langchain_core.output_parsers import StrOutputParser\n",
|
||||
"from langchain_core.prompts import PromptTemplate\n",
|
||||
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"\n",
|
||||
"from langgraph.prebuilt import tools_condition\n",
|
||||
"\n",
|
||||
"### Edges\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def grade_documents(state) -> Literal[\"generate\", \"rewrite\"]:\n",
|
||||
" \"\"\"\n",
|
||||
" Determines whether the retrieved documents are relevant to the question.\n",
|
||||
"\n",
|
||||
" Args:\n",
|
||||
" state (messages): The current state\n",
|
||||
"\n",
|
||||
" Returns:\n",
|
||||
" str: A decision for whether the documents are relevant or not\n",
|
||||
" \"\"\"\n",
|
||||
"\n",
|
||||
" print(\"---CHECK RELEVANCE---\")\n",
|
||||
"\n",
|
||||
" # Data model\n",
|
||||
" class grade(BaseModel):\n",
|
||||
" \"\"\"Binary score for relevance check.\"\"\"\n",
|
||||
"\n",
|
||||
" binary_score: str = Field(description=\"Relevance score 'yes' or 'no'\")\n",
|
||||
"\n",
|
||||
" # LLM\n",
|
||||
" model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n",
|
||||
"\n",
|
||||
" # LLM with tool and validation\n",
|
||||
" llm_with_tool = model.with_structured_output(grade)\n",
|
||||
"\n",
|
||||
" # Prompt\n",
|
||||
" prompt = PromptTemplate(\n",
|
||||
" template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n",
|
||||
" Here is the retrieved document: \\n\\n {context} \\n\\n\n",
|
||||
" Here is the user question: {question} \\n\n",
|
||||
" If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n",
|
||||
" Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\",\n",
|
||||
" input_variables=[\"context\", \"question\"],\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" # Chain\n",
|
||||
" chain = prompt | llm_with_tool\n",
|
||||
"\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
" last_message = messages[-1]\n",
|
||||
"\n",
|
||||
" question = messages[0].content\n",
|
||||
" docs = last_message.content\n",
|
||||
"\n",
|
||||
" scored_result = chain.invoke({\"question\": question, \"context\": docs})\n",
|
||||
"\n",
|
||||
" score = scored_result.binary_score\n",
|
||||
"\n",
|
||||
" if score == \"yes\":\n",
|
||||
" print(\"---DECISION: DOCS RELEVANT---\")\n",
|
||||
" return \"generate\"\n",
|
||||
"\n",
|
||||
" else:\n",
|
||||
" print(\"---DECISION: DOCS NOT RELEVANT---\")\n",
|
||||
" print(score)\n",
|
||||
" return \"rewrite\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"### Nodes\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def agent(state):\n",
|
||||
" \"\"\"\n",
|
||||
" Invokes the agent model to generate a response based on the current state. Given\n",
|
||||
" the question, it will decide to retrieve using the retriever tool, or simply end.\n",
|
||||
"\n",
|
||||
" Args:\n",
|
||||
" state (messages): The current state\n",
|
||||
"\n",
|
||||
" Returns:\n",
|
||||
" dict: The updated state with the agent response appended to messages\n",
|
||||
" \"\"\"\n",
|
||||
" print(\"---CALL AGENT---\")\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
" model = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4-turbo\")\n",
|
||||
" model = model.bind_tools(tools)\n",
|
||||
" response = model.invoke(messages)\n",
|
||||
" # We return a list, because this will get added to the existing list\n",
|
||||
" return {\"messages\": [response]}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def rewrite(state):\n",
|
||||
" \"\"\"\n",
|
||||
" Transform the query to produce a better question.\n",
|
||||
"\n",
|
||||
" Args:\n",
|
||||
" state (messages): The current state\n",
|
||||
"\n",
|
||||
" Returns:\n",
|
||||
" dict: The updated state with re-phrased question\n",
|
||||
" \"\"\"\n",
|
||||
"\n",
|
||||
" print(\"---TRANSFORM QUERY---\")\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
" question = messages[0].content\n",
|
||||
"\n",
|
||||
" msg = [\n",
|
||||
" HumanMessage(\n",
|
||||
" content=f\"\"\" \\n \n",
|
||||
" Look at the input and try to reason about the underlying semantic intent / meaning. \\n \n",
|
||||
" Here is the initial question:\n",
|
||||
" \\n ------- \\n\n",
|
||||
" {question} \n",
|
||||
" \\n ------- \\n\n",
|
||||
" Formulate an improved question: \"\"\",\n",
|
||||
" )\n",
|
||||
" ]\n",
|
||||
"\n",
|
||||
" # Grader\n",
|
||||
" model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n",
|
||||
" response = model.invoke(msg)\n",
|
||||
" return {\"messages\": [response]}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def generate(state):\n",
|
||||
" \"\"\"\n",
|
||||
" Generate answer\n",
|
||||
"\n",
|
||||
" Args:\n",
|
||||
" state (messages): The current state\n",
|
||||
"\n",
|
||||
" Returns:\n",
|
||||
" dict: The updated state with re-phrased question\n",
|
||||
" \"\"\"\n",
|
||||
" print(\"---GENERATE---\")\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
" question = messages[0].content\n",
|
||||
" last_message = messages[-1]\n",
|
||||
"\n",
|
||||
" docs = last_message.content\n",
|
||||
"\n",
|
||||
" # Prompt\n",
|
||||
" prompt = hub.pull(\"rlm/rag-prompt\")\n",
|
||||
"\n",
|
||||
" # LLM\n",
|
||||
" llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0, streaming=True)\n",
|
||||
"\n",
|
||||
" # Post-processing\n",
|
||||
" def format_docs(docs):\n",
|
||||
" return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
|
||||
"\n",
|
||||
" # Chain\n",
|
||||
" rag_chain = prompt | llm | StrOutputParser()\n",
|
||||
"\n",
|
||||
" # Run\n",
|
||||
" response = rag_chain.invoke({\"context\": docs, \"question\": question})\n",
|
||||
" return {\"messages\": [response]}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(\"*\" * 20 + \"Prompt[rlm/rag-prompt]\" + \"*\" * 20)\n",
|
||||
"prompt = hub.pull(\"rlm/rag-prompt\").pretty_print() # Show what the prompt looks like"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -150,7 +383,48 @@
|
||||
"id": "8718a37f-83c2-4f16-9850-e61e0f49c3d4",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langgraph.graph import END, StateGraph, START\nfrom langgraph.prebuilt import ToolNode\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the nodes we will cycle between\nworkflow.add_node(\"agent\", agent) # agent\nretrieve = ToolNode([retriever_tool])\nworkflow.add_node(\"retrieve\", retrieve) # retrieval\nworkflow.add_node(\"rewrite\", rewrite) # Re-writing the question\nworkflow.add_node(\n \"generate\", generate\n) # Generating a response after we know the documents are relevant\n# Call agent node to decide to retrieve or not\nworkflow.add_edge(START, \"agent\")\n\n# Decide whether to retrieve\nworkflow.add_conditional_edges(\n \"agent\",\n # Assess agent decision\n tools_condition,\n {\n # Translate the condition outputs to nodes in our graph\n \"tools\": \"retrieve\",\n END: END,\n },\n)\n\n# Edges taken after the `action` node is called.\nworkflow.add_conditional_edges(\n \"retrieve\",\n # Assess agent decision\n grade_documents,\n)\nworkflow.add_edge(\"generate\", END)\nworkflow.add_edge(\"rewrite\", \"agent\")\n\n# Compile\ngraph = workflow.compile()"]
|
||||
"source": [
|
||||
"from langgraph.graph import END, StateGraph, START\n",
|
||||
"from langgraph.prebuilt import ToolNode\n",
|
||||
"\n",
|
||||
"# Define a new graph\n",
|
||||
"workflow = StateGraph(AgentState)\n",
|
||||
"\n",
|
||||
"# Define the nodes we will cycle between\n",
|
||||
"workflow.add_node(\"agent\", agent) # agent\n",
|
||||
"retrieve = ToolNode([retriever_tool])\n",
|
||||
"workflow.add_node(\"retrieve\", retrieve) # retrieval\n",
|
||||
"workflow.add_node(\"rewrite\", rewrite) # Re-writing the question\n",
|
||||
"workflow.add_node(\n",
|
||||
" \"generate\", generate\n",
|
||||
") # Generating a response after we know the documents are relevant\n",
|
||||
"# Call agent node to decide to retrieve or not\n",
|
||||
"workflow.add_edge(START, \"agent\")\n",
|
||||
"\n",
|
||||
"# Decide whether to retrieve\n",
|
||||
"workflow.add_conditional_edges(\n",
|
||||
" \"agent\",\n",
|
||||
" # Assess agent decision\n",
|
||||
" tools_condition,\n",
|
||||
" {\n",
|
||||
" # Translate the condition outputs to nodes in our graph\n",
|
||||
" \"tools\": \"retrieve\",\n",
|
||||
" END: END,\n",
|
||||
" },\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Edges taken after the `action` node is called.\n",
|
||||
"workflow.add_conditional_edges(\n",
|
||||
" \"retrieve\",\n",
|
||||
" # Assess agent decision\n",
|
||||
" grade_documents,\n",
|
||||
")\n",
|
||||
"workflow.add_edge(\"generate\", END)\n",
|
||||
"workflow.add_edge(\"rewrite\", \"agent\")\n",
|
||||
"\n",
|
||||
"# Compile\n",
|
||||
"graph = workflow.compile()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -169,7 +443,15 @@
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
|
||||
"source": [
|
||||
"from IPython.display import Image, display\n",
|
||||
"\n",
|
||||
"try:\n",
|
||||
" display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\n",
|
||||
"except Exception:\n",
|
||||
" # This requires some extra dependencies and is optional\n",
|
||||
" pass"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -203,7 +485,21 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["import pprint\n\ninputs = {\n \"messages\": [\n (\"user\", \"What does Lilian Weng say about the types of agent memory?\"),\n ]\n}\nfor output in graph.stream(inputs):\n for key, value in output.items():\n pprint.pprint(f\"Output from node '{key}':\")\n pprint.pprint(\"---\")\n pprint.pprint(value, indent=2, width=80, depth=None)\n pprint.pprint(\"\\n---\\n\")"]
|
||||
"source": [
|
||||
"import pprint\n",
|
||||
"\n",
|
||||
"inputs = {\n",
|
||||
" \"messages\": [\n",
|
||||
" (\"user\", \"What does Lilian Weng say about the types of agent memory?\"),\n",
|
||||
" ]\n",
|
||||
"}\n",
|
||||
"for output in graph.stream(inputs):\n",
|
||||
" for key, value in output.items():\n",
|
||||
" pprint.pprint(f\"Output from node '{key}':\")\n",
|
||||
" pprint.pprint(\"---\")\n",
|
||||
" pprint.pprint(value, indent=2, width=80, depth=None)\n",
|
||||
" pprint.pprint(\"\\n---\\n\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -211,7 +507,7 @@
|
||||
"id": "189333cc-5d34-4869-9f9b-741210e1096f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [""]
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to force function calling agent to structure output\n",
|
||||
"\n",
|
||||
"You might want your agent to return its output in a structured format. For example, if the output of the agent is used by some other downstream software, you may want the output to be in the same structured format every time the agent is invoked to ensure consistency.\n",
|
||||
"\n",
|
||||
"This guide shows how you can do this. We will be using a basic [ReAct agent](https://langchain-ai.github.io/langgraph/how-tos/create-react-agent/) (a model node and a tool-calling node) together with a third node at the end that will format response for the user.\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"### Structured Output\n",
|
||||
"\n",
|
||||
"First we need to define how we want to structure our output. To do this, we will use the `with_structured_output` method from LangChain, which you can read more about [here](https://python.langchain.com/v0.2/docs/how_to/structured_output/)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from pydantic import BaseModel, Field\n",
|
||||
"\n",
|
||||
"class WeatherResponse(BaseModel):\n",
|
||||
" temperature: float = Field(description=\"The temperature in fahrenheit\")\n",
|
||||
" wind_directon: str = Field(description=\"The direction of the wind in abbreviated form\")\n",
|
||||
" wind_speed: float = Field(description=\"The speed of the wind in km/h\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Graph State\n",
|
||||
"\n",
|
||||
"We can now define our graph state:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import Annotated, Any\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph.message import add_messages\n",
|
||||
"\n",
|
||||
"class AgentState(TypedDict):\n",
|
||||
" # list of chat messages from user, LLM, and tools\n",
|
||||
" messages: Annotated[list, add_messages]\n",
|
||||
" # Final structured response from the agent\n",
|
||||
" final_response: WeatherResponse"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Tools + Models\n",
|
||||
"\n",
|
||||
"We can now instantiate the tools and models we are going to use in our graph. We are going to use a single tool in this example for finding the weather, and we are going to have two models in our graph, one that does the function calling and one that does the responding."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import Literal\n",
|
||||
"from langchain_core.tools import tool\n",
|
||||
"from langchain_anthropic import ChatAnthropic\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 is cloudy in NYC, with 5 mph winds in the North-East direction and a temperature of 70 degrees\"\n",
|
||||
" elif city == \"sf\":\n",
|
||||
" return \"It is 75 degrees and sunny in SF, with 3 mph winds in the South-East direction\"\n",
|
||||
" else:\n",
|
||||
" raise AssertionError(\"Unknown city\")\n",
|
||||
" \n",
|
||||
"tools = [get_weather]\n",
|
||||
" \n",
|
||||
"model = ChatAnthropic(model=\"claude-3-opus-20240229\")\n",
|
||||
" \n",
|
||||
"model_with_tools = model.bind_tools(tools)\n",
|
||||
"model_with_structured_output = model.with_structured_output(WeatherResponse)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Define Graph\n",
|
||||
"\n",
|
||||
"Now that we have defined our tools and models, we can define our graph."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 18,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.graph import StateGraph, END\n",
|
||||
"from langgraph.prebuilt import ToolNode\n",
|
||||
"from langchain_core.messages import HumanMessage\n",
|
||||
"\n",
|
||||
"# Define the function that determines whether to continue or not\n",
|
||||
"def should_continue(state: AgentState):\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
" last_message = messages[-1]\n",
|
||||
" # If there is no function call, then we respond to the user\n",
|
||||
" if not last_message.tool_calls:\n",
|
||||
" return \"respond\"\n",
|
||||
" # Otherwise if there is, we continue\n",
|
||||
" else:\n",
|
||||
" return \"continue\"\n",
|
||||
"\n",
|
||||
"# Define the function that calls the model\n",
|
||||
"def call_model(state: AgentState):\n",
|
||||
" response = model_with_tools.invoke(state['messages'])\n",
|
||||
" # We return a list, because this will get added to the existing list\n",
|
||||
" return {\"messages\": [response]}\n",
|
||||
"\n",
|
||||
"# Define the function that responds to the user\n",
|
||||
"def respond(state: AgentState):\n",
|
||||
" # We call the model with structured output in order to return the same format to the user every time\n",
|
||||
" # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n",
|
||||
" # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n",
|
||||
" response = model_with_structured_output.invoke([HumanMessage(content=state['messages'][-2].content)])\n",
|
||||
" # We return the final answer\n",
|
||||
" return {\"final_response\": response}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Define a new graph\n",
|
||||
"workflow = StateGraph(AgentState)\n",
|
||||
"\n",
|
||||
"# Define the two nodes we will cycle between\n",
|
||||
"workflow.add_node(\"agent\", call_model)\n",
|
||||
"workflow.add_node(\"respond\", respond)\n",
|
||||
"workflow.add_node(\"tools\", ToolNode(tools))\n",
|
||||
"\n",
|
||||
"# Set the entrypoint as `agent`\n",
|
||||
"# This means that this node is the first one called\n",
|
||||
"workflow.set_entry_point(\"agent\")\n",
|
||||
"\n",
|
||||
"# We now add a conditional edge\n",
|
||||
"workflow.add_conditional_edges(\n",
|
||||
" \"agent\",\n",
|
||||
" should_continue,\n",
|
||||
" {\n",
|
||||
" \"continue\": \"tools\",\n",
|
||||
" \"respond\": \"respond\",\n",
|
||||
" },\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"workflow.add_edge(\"tools\", \"agent\")\n",
|
||||
"workflow.add_edge(\"respond\", END)\n",
|
||||
"graph = workflow.compile()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Usage\n",
|
||||
"\n",
|
||||
"We can now invoke our graph to verify that the output is being structured as desired:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 19,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"answer = graph.invoke(input={\"messages\": [(\"human\", \"what's the weather in SF?\")]})['final_response']"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"WeatherResponse(temperature=75.0, wind_directon='SE', wind_speed=3.0)"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"answer"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"As we can see, the agent returned a `WeatherResponse` object as we expected. If would now be easy to use this agent in a more complex software stack without having to worry about the output of the agent not matching the format expected from the next step in the stack."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -40,7 +40,10 @@
|
||||
"id": "1b64a6f6-1d32-48be-92b5-66c3b04b17f7",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["%pip install -U --quiet langgraph langchain_anthropic\n%pip install -U --quiet tavily-python"]
|
||||
"source": [
|
||||
"%pip install -U --quiet langgraph langchain_anthropic\n",
|
||||
"%pip install -U --quiet tavily-python"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -48,7 +51,25 @@
|
||||
"id": "a917bb70-f84c-48e6-8d32-d14f9df2ca2f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str) -> None:\n if os.environ.get(var):\n return\n os.environ[var] = getpass.getpass(var)\n\n\n# Optional: Configure tracing to visualize and debug the agent\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Reflexion\"\n\n_set_if_undefined(\"ANTHROPIC_API_KEY\")\n_set_if_undefined(\"TAVILY_API_KEY\")"]
|
||||
"source": [
|
||||
"import getpass\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _set_if_undefined(var: str) -> None:\n",
|
||||
" if os.environ.get(var):\n",
|
||||
" return\n",
|
||||
" os.environ[var] = getpass.getpass(var)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Optional: Configure tracing to visualize and debug the agent\n",
|
||||
"_set_if_undefined(\"LANGCHAIN_API_KEY\")\n",
|
||||
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
|
||||
"os.environ[\"LANGCHAIN_PROJECT\"] = \"Reflexion\"\n",
|
||||
"\n",
|
||||
"_set_if_undefined(\"ANTHROPIC_API_KEY\")\n",
|
||||
"_set_if_undefined(\"TAVILY_API_KEY\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -56,7 +77,15 @@
|
||||
"id": "567b6c4a",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langchain_anthropic import ChatAnthropic\n\nllm = ChatAnthropic(model=\"claude-3-sonnet-20240229\")\n# You could also use OpenAI or another provider\n# from langchain_openai import ChatOpenAI\n\n# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")"]
|
||||
"source": [
|
||||
"from langchain_anthropic import ChatAnthropic\n",
|
||||
"\n",
|
||||
"llm = ChatAnthropic(model=\"claude-3-sonnet-20240229\")\n",
|
||||
"# You could also use OpenAI or another provider\n",
|
||||
"# from langchain_openai import ChatOpenAI\n",
|
||||
"\n",
|
||||
"# llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -81,7 +110,13 @@
|
||||
"id": "5a2ac853-b8a6-40de-b7fe-3f9f3c5ca4d2",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_community.utilities.tavily_search import TavilySearchAPIWrapper\n\nsearch = TavilySearchAPIWrapper()\ntavily_tool = TavilySearchResults(api_wrapper=search, max_results=5)"]
|
||||
"source": [
|
||||
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
|
||||
"from langchain_community.utilities.tavily_search import TavilySearchAPIWrapper\n",
|
||||
"\n",
|
||||
"search = TavilySearchAPIWrapper()\n",
|
||||
"tavily_tool = TavilySearchResults(api_wrapper=search, max_results=5)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -97,7 +132,54 @@
|
||||
"id": "5fffa8d5-068a-4f0b-adfc-b4daf30ef294",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langchain_core.messages import HumanMessage, ToolMessage\nfrom langchain_core.output_parsers.openai_tools import PydanticToolsParser\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\nfrom langchain_core.pydantic_v1 import BaseModel, Field, ValidationError\n\n\nclass Reflection(BaseModel):\n missing: str = Field(description=\"Critique of what is missing.\")\n superfluous: str = Field(description=\"Critique of what is superfluous\")\n\n\nclass AnswerQuestion(BaseModel):\n \"\"\"Answer the question. Provide an answer, reflection, and then follow up with search queries to improve the answer.\"\"\"\n\n answer: str = Field(description=\"~250 word detailed answer to the question.\")\n reflection: Reflection = Field(description=\"Your reflection on the initial answer.\")\n search_queries: list[str] = Field(\n description=\"1-3 search queries for researching improvements to address the critique of your current answer.\"\n )\n\n\nclass ResponderWithRetries:\n def __init__(self, runnable, validator):\n self.runnable = runnable\n self.validator = validator\n\n def respond(self, state: list):\n response = []\n for attempt in range(3):\n response = self.runnable.invoke(\n {\"messages\": state}, {\"tags\": [f\"attempt:{attempt}\"]}\n )\n try:\n self.validator.invoke(response)\n return response\n except ValidationError as e:\n state = state + [\n response,\n ToolMessage(\n content=f\"{repr(e)}\\n\\nPay close attention to the function schema.\\n\\n\"\n + self.validator.schema_json()\n + \" Respond by fixing all validation errors.\",\n tool_call_id=response.tool_calls[0][\"id\"],\n ),\n ]\n return response"]
|
||||
"source": [
|
||||
"from langchain_core.messages import HumanMessage, ToolMessage\n",
|
||||
"from langchain_core.output_parsers.openai_tools import PydanticToolsParser\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
|
||||
"from langchain_core.pydantic_v1 import BaseModel, Field, ValidationError\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Reflection(BaseModel):\n",
|
||||
" missing: str = Field(description=\"Critique of what is missing.\")\n",
|
||||
" superfluous: str = Field(description=\"Critique of what is superfluous\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class AnswerQuestion(BaseModel):\n",
|
||||
" \"\"\"Answer the question. Provide an answer, reflection, and then follow up with search queries to improve the answer.\"\"\"\n",
|
||||
"\n",
|
||||
" answer: str = Field(description=\"~250 word detailed answer to the question.\")\n",
|
||||
" reflection: Reflection = Field(description=\"Your reflection on the initial answer.\")\n",
|
||||
" search_queries: list[str] = Field(\n",
|
||||
" description=\"1-3 search queries for researching improvements to address the critique of your current answer.\"\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class ResponderWithRetries:\n",
|
||||
" def __init__(self, runnable, validator):\n",
|
||||
" self.runnable = runnable\n",
|
||||
" self.validator = validator\n",
|
||||
"\n",
|
||||
" def respond(self, state: list):\n",
|
||||
" response = []\n",
|
||||
" for attempt in range(3):\n",
|
||||
" response = self.runnable.invoke(\n",
|
||||
" {\"messages\": state}, {\"tags\": [f\"attempt:{attempt}\"]}\n",
|
||||
" )\n",
|
||||
" try:\n",
|
||||
" self.validator.invoke(response)\n",
|
||||
" return response\n",
|
||||
" except ValidationError as e:\n",
|
||||
" state = state + [\n",
|
||||
" response,\n",
|
||||
" ToolMessage(\n",
|
||||
" content=f\"{repr(e)}\\n\\nPay close attention to the function schema.\\n\\n\"\n",
|
||||
" + self.validator.schema_json()\n",
|
||||
" + \" Respond by fixing all validation errors.\",\n",
|
||||
" tool_call_id=response.tool_calls[0][\"id\"],\n",
|
||||
" ),\n",
|
||||
" ]\n",
|
||||
" return response"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -114,7 +196,40 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["import datetime\n\nactor_prompt_template = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are expert researcher.\nCurrent time: {time}\n\n1. {first_instruction}\n2. Reflect and critique your answer. Be severe to maximize improvement.\n3. Recommend search queries to research information and improve your answer.\"\"\",\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n (\n \"user\",\n \"\\n\\n<system>Reflect on the user's original question and the\"\n \" actions taken thus far. Respond using the {function_name} function.</reminder>\",\n ),\n ]\n).partial(\n time=lambda: datetime.datetime.now().isoformat(),\n)\ninitial_answer_chain = actor_prompt_template.partial(\n first_instruction=\"Provide a detailed ~250 word answer.\",\n function_name=AnswerQuestion.__name__,\n) | llm.bind_tools(tools=[AnswerQuestion])\nvalidator = PydanticToolsParser(tools=[AnswerQuestion])\n\nfirst_responder = ResponderWithRetries(\n runnable=initial_answer_chain, validator=validator\n)"]
|
||||
"source": [
|
||||
"import datetime\n",
|
||||
"\n",
|
||||
"actor_prompt_template = ChatPromptTemplate.from_messages(\n",
|
||||
" [\n",
|
||||
" (\n",
|
||||
" \"system\",\n",
|
||||
" \"\"\"You are expert researcher.\n",
|
||||
"Current time: {time}\n",
|
||||
"\n",
|
||||
"1. {first_instruction}\n",
|
||||
"2. Reflect and critique your answer. Be severe to maximize improvement.\n",
|
||||
"3. Recommend search queries to research information and improve your answer.\"\"\",\n",
|
||||
" ),\n",
|
||||
" MessagesPlaceholder(variable_name=\"messages\"),\n",
|
||||
" (\n",
|
||||
" \"user\",\n",
|
||||
" \"\\n\\n<system>Reflect on the user's original question and the\"\n",
|
||||
" \" actions taken thus far. Respond using the {function_name} function.</reminder>\",\n",
|
||||
" ),\n",
|
||||
" ]\n",
|
||||
").partial(\n",
|
||||
" time=lambda: datetime.datetime.now().isoformat(),\n",
|
||||
")\n",
|
||||
"initial_answer_chain = actor_prompt_template.partial(\n",
|
||||
" first_instruction=\"Provide a detailed ~250 word answer.\",\n",
|
||||
" function_name=AnswerQuestion.__name__,\n",
|
||||
") | llm.bind_tools(tools=[AnswerQuestion])\n",
|
||||
"validator = PydanticToolsParser(tools=[AnswerQuestion])\n",
|
||||
"\n",
|
||||
"first_responder = ResponderWithRetries(\n",
|
||||
" runnable=initial_answer_chain, validator=validator\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -122,7 +237,10 @@
|
||||
"id": "5922e1fe-7533-4f41-8b1d-d812707c1968",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["example_question = \"Why is reflection useful in AI?\"\ninitial = first_responder.respond([HumanMessage(content=example_question)])"]
|
||||
"source": [
|
||||
"example_question = \"Why is reflection useful in AI?\"\n",
|
||||
"initial = first_responder.respond([HumanMessage(content=example_question)])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -140,7 +258,38 @@
|
||||
"id": "2605fd8d-c663-446f-ba25-751190195749",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["revise_instructions = \"\"\"Revise your previous answer using the new information.\n - You should use the previous critique to add important information to your answer.\n - You MUST include numerical citations in your revised answer to ensure it can be verified.\n - Add a \"References\" section to the bottom of your answer (which does not count towards the word limit). In form of:\n - [1] https://example.com\n - [2] https://example.com\n - You should use the previous critique to remove superfluous information from your answer and make SURE it is not more than 250 words.\n\"\"\"\n\n\n# Extend the initial answer schema to include references.\n# Forcing citation in the model encourages grounded responses\nclass ReviseAnswer(AnswerQuestion):\n \"\"\"Revise your original answer to your question. Provide an answer, reflection,\n\n cite your reflection with references, and finally\n add search queries to improve the answer.\"\"\"\n\n references: list[str] = Field(\n description=\"Citations motivating your updated answer.\"\n )\n\n\nrevision_chain = actor_prompt_template.partial(\n first_instruction=revise_instructions,\n function_name=ReviseAnswer.__name__,\n) | llm.bind_tools(tools=[ReviseAnswer])\nrevision_validator = PydanticToolsParser(tools=[ReviseAnswer])\n\nrevisor = ResponderWithRetries(runnable=revision_chain, validator=revision_validator)"]
|
||||
"source": [
|
||||
"revise_instructions = \"\"\"Revise your previous answer using the new information.\n",
|
||||
" - You should use the previous critique to add important information to your answer.\n",
|
||||
" - You MUST include numerical citations in your revised answer to ensure it can be verified.\n",
|
||||
" - Add a \"References\" section to the bottom of your answer (which does not count towards the word limit). In form of:\n",
|
||||
" - [1] https://example.com\n",
|
||||
" - [2] https://example.com\n",
|
||||
" - You should use the previous critique to remove superfluous information from your answer and make SURE it is not more than 250 words.\n",
|
||||
"\"\"\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Extend the initial answer schema to include references.\n",
|
||||
"# Forcing citation in the model encourages grounded responses\n",
|
||||
"class ReviseAnswer(AnswerQuestion):\n",
|
||||
" \"\"\"Revise your original answer to your question. Provide an answer, reflection,\n",
|
||||
"\n",
|
||||
" cite your reflection with references, and finally\n",
|
||||
" add search queries to improve the answer.\"\"\"\n",
|
||||
"\n",
|
||||
" references: list[str] = Field(\n",
|
||||
" description=\"Citations motivating your updated answer.\"\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"revision_chain = actor_prompt_template.partial(\n",
|
||||
" first_instruction=revise_instructions,\n",
|
||||
" function_name=ReviseAnswer.__name__,\n",
|
||||
") | llm.bind_tools(tools=[ReviseAnswer])\n",
|
||||
"revision_validator = PydanticToolsParser(tools=[ReviseAnswer])\n",
|
||||
"\n",
|
||||
"revisor = ResponderWithRetries(runnable=revision_chain, validator=revision_validator)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -159,7 +308,25 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["import json\n\nrevised = revisor.respond(\n [\n HumanMessage(content=example_question),\n initial,\n ToolMessage(\n tool_call_id=initial.tool_calls[0][\"id\"],\n content=json.dumps(\n tavily_tool.invoke(\n {\"query\": initial.tool_calls[0][\"args\"][\"search_queries\"][0]}\n )\n ),\n ),\n ]\n)\nrevised"]
|
||||
"source": [
|
||||
"import json\n",
|
||||
"\n",
|
||||
"revised = revisor.respond(\n",
|
||||
" [\n",
|
||||
" HumanMessage(content=example_question),\n",
|
||||
" initial,\n",
|
||||
" ToolMessage(\n",
|
||||
" tool_call_id=initial.tool_calls[0][\"id\"],\n",
|
||||
" content=json.dumps(\n",
|
||||
" tavily_tool.invoke(\n",
|
||||
" {\"query\": initial.tool_calls[0][\"args\"][\"search_queries\"][0]}\n",
|
||||
" )\n",
|
||||
" ),\n",
|
||||
" ),\n",
|
||||
" ]\n",
|
||||
")\n",
|
||||
"revised"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -177,7 +344,24 @@
|
||||
"id": "fccd6a17",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langchain_core.tools import StructuredTool\n\nfrom langgraph.prebuilt import ToolNode\n\n\ndef run_queries(search_queries: list[str], **kwargs):\n \"\"\"Run the generated queries.\"\"\"\n return tavily_tool.batch([{\"query\": query} for query in search_queries])\n\n\ntool_node = ToolNode(\n [\n StructuredTool.from_function(run_queries, name=AnswerQuestion.__name__),\n StructuredTool.from_function(run_queries, name=ReviseAnswer.__name__),\n ]\n)"]
|
||||
"source": [
|
||||
"from langchain_core.tools import StructuredTool\n",
|
||||
"\n",
|
||||
"from langgraph.prebuilt import ToolNode\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def run_queries(search_queries: list[str], **kwargs):\n",
|
||||
" \"\"\"Run the generated queries.\"\"\"\n",
|
||||
" return tavily_tool.batch([{\"query\": query} for query in search_queries])\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"tool_node = ToolNode(\n",
|
||||
" [\n",
|
||||
" StructuredTool.from_function(run_queries, name=AnswerQuestion.__name__),\n",
|
||||
" StructuredTool.from_function(run_queries, name=ReviseAnswer.__name__),\n",
|
||||
" ]\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -196,7 +380,56 @@
|
||||
"id": "3c57318f-a30c-4dbd-9b88-f2633e8cb3b1",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from typing import Literal\n\nfrom langgraph.graph import END, MessageGraph, START\n\nMAX_ITERATIONS = 5\nbuilder = MessageGraph()\nbuilder.add_node(\"draft\", first_responder.respond)\n\n\nbuilder.add_node(\"execute_tools\", tool_node)\nbuilder.add_node(\"revise\", revisor.respond)\n# draft -> execute_tools\nbuilder.add_edge(\"draft\", \"execute_tools\")\n# execute_tools -> revise\nbuilder.add_edge(\"execute_tools\", \"revise\")\n\n# Define looping logic:\n\n\ndef _get_num_iterations(state: list):\n i = 0\n for m in state[::-1]:\n if m.type not in {\"tool\", \"ai\"}:\n break\n i += 1\n return i\n\n\ndef event_loop(state: list) -> Literal[\"execute_tools\", \"__end__\"]:\n # in our case, we'll just stop after N plans\n num_iterations = _get_num_iterations(state)\n if num_iterations > MAX_ITERATIONS:\n return END\n return \"execute_tools\"\n\n\n# revise -> execute_tools OR end\nbuilder.add_conditional_edges(\"revise\", event_loop)\nbuilder.add_edge(START, \"draft\")\ngraph = builder.compile()"]
|
||||
"source": [
|
||||
"from typing import Literal\n",
|
||||
"\n",
|
||||
"from langgraph.graph import END, StateGraph, START\n",
|
||||
"from langgraph.graph.message import add_messages\n",
|
||||
"from typing import Annotated\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class State(TypedDict):\n",
|
||||
" messages: Annotated[list, add_messages]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"MAX_ITERATIONS = 5\n",
|
||||
"builder = StateGraph(State)\n",
|
||||
"builder.add_node(\"draft\", first_responder.respond)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"builder.add_node(\"execute_tools\", tool_node)\n",
|
||||
"builder.add_node(\"revise\", revisor.respond)\n",
|
||||
"# draft -> execute_tools\n",
|
||||
"builder.add_edge(\"draft\", \"execute_tools\")\n",
|
||||
"# execute_tools -> revise\n",
|
||||
"builder.add_edge(\"execute_tools\", \"revise\")\n",
|
||||
"\n",
|
||||
"# Define looping logic:\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _get_num_iterations(state: list):\n",
|
||||
" i = 0\n",
|
||||
" for m in state[::-1]:\n",
|
||||
" if m.type not in {\"tool\", \"ai\"}:\n",
|
||||
" break\n",
|
||||
" i += 1\n",
|
||||
" return i\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def event_loop(state: list) -> Literal[\"execute_tools\", \"__end__\"]:\n",
|
||||
" # in our case, we'll just stop after N plans\n",
|
||||
" num_iterations = _get_num_iterations(state)\n",
|
||||
" if num_iterations > MAX_ITERATIONS:\n",
|
||||
" return END\n",
|
||||
" return \"execute_tools\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# revise -> execute_tools OR end\n",
|
||||
"builder.add_conditional_edges(\"revise\", event_loop)\n",
|
||||
"builder.add_edge(START, \"draft\")\n",
|
||||
"graph = builder.compile()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -215,7 +448,15 @@
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"]
|
||||
"source": [
|
||||
"from IPython.display import Image, display\n",
|
||||
"\n",
|
||||
"try:\n",
|
||||
" display(Image(graph.get_graph().draw_mermaid_png()))\n",
|
||||
"except Exception:\n",
|
||||
" # This requires some extra dependencies and is optional\n",
|
||||
" pass"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -330,7 +571,15 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["events = graph.stream(\n [HumanMessage(content=\"How should we handle the climate crisis?\")],\n stream_mode=\"values\",\n)\nfor i, step in enumerate(events):\n print(f\"Step {i}\")\n step[-1].pretty_print()"]
|
||||
"source": [
|
||||
"events = graph.stream(\n",
|
||||
" [HumanMessage(content=\"How should we handle the climate crisis?\")],\n",
|
||||
" stream_mode=\"values\",\n",
|
||||
")\n",
|
||||
"for i, step in enumerate(events):\n",
|
||||
" print(f\"Step {i}\")\n",
|
||||
" step[-1].pretty_print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
|
||||
+734
-38
@@ -48,7 +48,10 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n%pip install -U langchain_community langchain_openai langgraph wikipedia scikit-learn langchain_fireworks\n# We use one or the other search engine below\n%pip install -U duckduckgo tavily-python"
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langchain_community langchain_openai langgraph wikipedia scikit-learn langchain_fireworks\n",
|
||||
"# We use one or the other search engine below\n",
|
||||
"%pip install -U duckduckgo tavily-python"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -57,7 +60,10 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Uncomment if you want to draw the pretty graph diagrams.\n# If you are on MacOS, you will need to run brew install graphviz before installing and update some environment flags\n# ! brew install graphviz\n# !CFLAGS=\"-I $(brew --prefix graphviz)/include\" LDFLAGS=\"-L $(brew --prefix graphviz)/lib\" pip install -U pygraphviz"
|
||||
"# Uncomment if you want to draw the pretty graph diagrams.\n",
|
||||
"# If you are on MacOS, you will need to run brew install graphviz before installing and update some environment flags\n",
|
||||
"# ! brew install graphviz\n",
|
||||
"# !CFLAGS=\"-I $(brew --prefix graphviz)/include\" LDFLAGS=\"-L $(brew --prefix graphviz)/lib\" pip install -U pygraphviz"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -66,7 +72,21 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import getpass\nimport os\n\n\ndef _set_env(var: str):\n if os.environ.get(var):\n return\n os.environ[var] = getpass.getpass(var + \":\")\n\n\n# Set for tracing\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"STORM\"\n_set_env(\"LANGCHAIN_API_KEY\")\n_set_env(\"OPENAI_API_KEY\")"
|
||||
"import getpass\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _set_env(var: str):\n",
|
||||
" if os.environ.get(var):\n",
|
||||
" return\n",
|
||||
" os.environ[var] = getpass.getpass(var + \":\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Set for tracing\n",
|
||||
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
|
||||
"os.environ[\"LANGCHAIN_PROJECT\"] = \"STORM\"\n",
|
||||
"_set_env(\"LANGCHAIN_API_KEY\")\n",
|
||||
"_set_env(\"OPENAI_API_KEY\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -84,7 +104,12 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n\nfast_llm = ChatOpenAI(model=\"gpt-3.5-turbo\")\n# Uncomment for a Fireworks model\n# fast_llm = ChatFireworks(model=\"accounts/fireworks/models/firefunction-v1\", max_tokens=32_000)\nlong_context_llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")"
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"\n",
|
||||
"fast_llm = ChatOpenAI(model=\"gpt-3.5-turbo\")\n",
|
||||
"# Uncomment for a Fireworks model\n",
|
||||
"# fast_llm = ChatFireworks(model=\"accounts/fireworks/models/firefunction-v1\", max_tokens=32_000)\n",
|
||||
"long_context_llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -112,7 +137,64 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from typing import List, Optional\n\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\ndirect_gen_outline_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a Wikipedia writer. Write an outline for a Wikipedia page about a user-provided topic. Be comprehensive and specific.\",\n ),\n (\"user\", \"{topic}\"),\n ]\n)\n\n\nclass Subsection(BaseModel):\n subsection_title: str = Field(..., title=\"Title of the subsection\")\n description: str = Field(..., title=\"Content of the subsection\")\n\n @property\n def as_str(self) -> str:\n return f\"### {self.subsection_title}\\n\\n{self.description}\".strip()\n\n\nclass Section(BaseModel):\n section_title: str = Field(..., title=\"Title of the section\")\n description: str = Field(..., title=\"Content of the section\")\n subsections: Optional[List[Subsection]] = Field(\n default=None,\n title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n )\n\n @property\n def as_str(self) -> str:\n subsections = \"\\n\\n\".join(\n f\"### {subsection.subsection_title}\\n\\n{subsection.description}\"\n for subsection in self.subsections or []\n )\n return f\"## {self.section_title}\\n\\n{self.description}\\n\\n{subsections}\".strip()\n\n\nclass Outline(BaseModel):\n page_title: str = Field(..., title=\"Title of the Wikipedia page\")\n sections: List[Section] = Field(\n default_factory=list,\n title=\"Titles and descriptions for each section of the Wikipedia page.\",\n )\n\n @property\n def as_str(self) -> str:\n sections = \"\\n\\n\".join(section.as_str for section in self.sections)\n return f\"# {self.page_title}\\n\\n{sections}\".strip()\n\n\ngenerate_outline_direct = direct_gen_outline_prompt | fast_llm.with_structured_output(\n Outline\n)"
|
||||
"from typing import List, Optional\n",
|
||||
"\n",
|
||||
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||
"from langchain_core.pydantic_v1 import BaseModel, Field\n",
|
||||
"\n",
|
||||
"direct_gen_outline_prompt = ChatPromptTemplate.from_messages(\n",
|
||||
" [\n",
|
||||
" (\n",
|
||||
" \"system\",\n",
|
||||
" \"You are a Wikipedia writer. Write an outline for a Wikipedia page about a user-provided topic. Be comprehensive and specific.\",\n",
|
||||
" ),\n",
|
||||
" (\"user\", \"{topic}\"),\n",
|
||||
" ]\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Subsection(BaseModel):\n",
|
||||
" subsection_title: str = Field(..., title=\"Title of the subsection\")\n",
|
||||
" description: str = Field(..., title=\"Content of the subsection\")\n",
|
||||
"\n",
|
||||
" @property\n",
|
||||
" def as_str(self) -> str:\n",
|
||||
" return f\"### {self.subsection_title}\\n\\n{self.description}\".strip()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Section(BaseModel):\n",
|
||||
" section_title: str = Field(..., title=\"Title of the section\")\n",
|
||||
" description: str = Field(..., title=\"Content of the section\")\n",
|
||||
" subsections: Optional[List[Subsection]] = Field(\n",
|
||||
" default=None,\n",
|
||||
" title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" @property\n",
|
||||
" def as_str(self) -> str:\n",
|
||||
" subsections = \"\\n\\n\".join(\n",
|
||||
" f\"### {subsection.subsection_title}\\n\\n{subsection.description}\"\n",
|
||||
" for subsection in self.subsections or []\n",
|
||||
" )\n",
|
||||
" return f\"## {self.section_title}\\n\\n{self.description}\\n\\n{subsections}\".strip()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Outline(BaseModel):\n",
|
||||
" page_title: str = Field(..., title=\"Title of the Wikipedia page\")\n",
|
||||
" sections: List[Section] = Field(\n",
|
||||
" default_factory=list,\n",
|
||||
" title=\"Titles and descriptions for each section of the Wikipedia page.\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" @property\n",
|
||||
" def as_str(self) -> str:\n",
|
||||
" sections = \"\\n\\n\".join(section.as_str for section in self.sections)\n",
|
||||
" return f\"# {self.page_title}\\n\\n{sections}\".strip()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"generate_outline_direct = direct_gen_outline_prompt | fast_llm.with_structured_output(\n",
|
||||
" Outline\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -145,7 +227,11 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"example_topic = \"Impact of million-plus token context window language models on RAG\"\n\ninitial_outline = generate_outline_direct.invoke({\"topic\": example_topic})\n\nprint(initial_outline.as_str)"
|
||||
"example_topic = \"Impact of million-plus token context window language models on RAG\"\n",
|
||||
"\n",
|
||||
"initial_outline = generate_outline_direct.invoke({\"topic\": example_topic})\n",
|
||||
"\n",
|
||||
"print(initial_outline.as_str)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -165,7 +251,25 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"gen_related_topics_prompt = ChatPromptTemplate.from_template(\n \"\"\"I'm writing a Wikipedia page for a topic mentioned below. Please identify and recommend some Wikipedia pages on closely related subjects. I'm looking for examples that provide insights into interesting aspects commonly associated with this topic, or examples that help me understand the typical content and structure included in Wikipedia pages for similar topics.\n\nPlease list the as many subjects and urls as you can.\n\nTopic of interest: {topic}\n\"\"\"\n)\n\n\nclass RelatedSubjects(BaseModel):\n topics: List[str] = Field(\n description=\"Comprehensive list of related subjects as background research.\",\n )\n\n\nexpand_chain = gen_related_topics_prompt | fast_llm.with_structured_output(\n RelatedSubjects\n)"
|
||||
"gen_related_topics_prompt = ChatPromptTemplate.from_template(\n",
|
||||
" \"\"\"I'm writing a Wikipedia page for a topic mentioned below. Please identify and recommend some Wikipedia pages on closely related subjects. I'm looking for examples that provide insights into interesting aspects commonly associated with this topic, or examples that help me understand the typical content and structure included in Wikipedia pages for similar topics.\n",
|
||||
"\n",
|
||||
"Please list the as many subjects and urls as you can.\n",
|
||||
"\n",
|
||||
"Topic of interest: {topic}\n",
|
||||
"\"\"\"\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class RelatedSubjects(BaseModel):\n",
|
||||
" topics: List[str] = Field(\n",
|
||||
" description=\"Comprehensive list of related subjects as background research.\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"expand_chain = gen_related_topics_prompt | fast_llm.with_structured_output(\n",
|
||||
" RelatedSubjects\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -185,7 +289,8 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"related_subjects = await expand_chain.ainvoke({\"topic\": example_topic})\nrelated_subjects"
|
||||
"related_subjects = await expand_chain.ainvoke({\"topic\": example_topic})\n",
|
||||
"related_subjects"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -204,7 +309,49 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"class Editor(BaseModel):\n affiliation: str = Field(\n description=\"Primary affiliation of the editor.\",\n )\n name: str = Field(\n description=\"Name of the editor.\", pattern=r\"^[a-zA-Z0-9_-]{1,64}$\"\n )\n role: str = Field(\n description=\"Role of the editor in the context of the topic.\",\n )\n description: str = Field(\n description=\"Description of the editor's focus, concerns, and motives.\",\n )\n\n @property\n def persona(self) -> str:\n return f\"Name: {self.name}\\nRole: {self.role}\\nAffiliation: {self.affiliation}\\nDescription: {self.description}\\n\"\n\n\nclass Perspectives(BaseModel):\n editors: List[Editor] = Field(\n description=\"Comprehensive list of editors with their roles and affiliations.\",\n # Add a pydantic validation/restriction to be at most M editors\n )\n\n\ngen_perspectives_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You need to select a diverse (and distinct) group of Wikipedia editors who will work together to create a comprehensive article on the topic. Each of them represents a different perspective, role, or affiliation related to this topic.\\\n You can use other Wikipedia pages of related topics for inspiration. For each editor, add a description of what they will focus on.\n\n Wiki page outlines of related topics for inspiration:\n {examples}\"\"\",\n ),\n (\"user\", \"Topic of interest: {topic}\"),\n ]\n)\n\ngen_perspectives_chain = gen_perspectives_prompt | ChatOpenAI(\n model=\"gpt-3.5-turbo\"\n).with_structured_output(Perspectives)"
|
||||
"class Editor(BaseModel):\n",
|
||||
" affiliation: str = Field(\n",
|
||||
" description=\"Primary affiliation of the editor.\",\n",
|
||||
" )\n",
|
||||
" name: str = Field(\n",
|
||||
" description=\"Name of the editor.\", pattern=r\"^[a-zA-Z0-9_-]{1,64}$\"\n",
|
||||
" )\n",
|
||||
" role: str = Field(\n",
|
||||
" description=\"Role of the editor in the context of the topic.\",\n",
|
||||
" )\n",
|
||||
" description: str = Field(\n",
|
||||
" description=\"Description of the editor's focus, concerns, and motives.\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" @property\n",
|
||||
" def persona(self) -> str:\n",
|
||||
" return f\"Name: {self.name}\\nRole: {self.role}\\nAffiliation: {self.affiliation}\\nDescription: {self.description}\\n\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Perspectives(BaseModel):\n",
|
||||
" editors: List[Editor] = Field(\n",
|
||||
" description=\"Comprehensive list of editors with their roles and affiliations.\",\n",
|
||||
" # Add a pydantic validation/restriction to be at most M editors\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"gen_perspectives_prompt = ChatPromptTemplate.from_messages(\n",
|
||||
" [\n",
|
||||
" (\n",
|
||||
" \"system\",\n",
|
||||
" \"\"\"You need to select a diverse (and distinct) group of Wikipedia editors who will work together to create a comprehensive article on the topic. Each of them represents a different perspective, role, or affiliation related to this topic.\\\n",
|
||||
" You can use other Wikipedia pages of related topics for inspiration. For each editor, add a description of what they will focus on.\n",
|
||||
"\n",
|
||||
" Wiki page outlines of related topics for inspiration:\n",
|
||||
" {examples}\"\"\",\n",
|
||||
" ),\n",
|
||||
" (\"user\", \"Topic of interest: {topic}\"),\n",
|
||||
" ]\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"gen_perspectives_chain = gen_perspectives_prompt | ChatOpenAI(\n",
|
||||
" model=\"gpt-3.5-turbo\"\n",
|
||||
").with_structured_output(Perspectives)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -213,7 +360,37 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_community.retrievers import WikipediaRetriever\nfrom langchain_core.runnables import RunnableLambda\nfrom langchain_core.runnables import chain as as_runnable\n\nwikipedia_retriever = WikipediaRetriever(load_all_available_meta=True, top_k_results=1)\n\n\ndef format_doc(doc, max_length=1000):\n related = \"- \".join(doc.metadata[\"categories\"])\n return f\"### {doc.metadata['title']}\\n\\nSummary: {doc.page_content}\\n\\nRelated\\n{related}\"[\n :max_length\n ]\n\n\ndef format_docs(docs):\n return \"\\n\\n\".join(format_doc(doc) for doc in docs)\n\n\n@as_runnable\nasync def survey_subjects(topic: str):\n related_subjects = await expand_chain.ainvoke({\"topic\": topic})\n retrieved_docs = await wikipedia_retriever.abatch(\n related_subjects.topics, return_exceptions=True\n )\n all_docs = []\n for docs in retrieved_docs:\n if isinstance(docs, BaseException):\n continue\n all_docs.extend(docs)\n formatted = format_docs(all_docs)\n return await gen_perspectives_chain.ainvoke({\"examples\": formatted, \"topic\": topic})"
|
||||
"from langchain_community.retrievers import WikipediaRetriever\n",
|
||||
"from langchain_core.runnables import RunnableLambda\n",
|
||||
"from langchain_core.runnables import chain as as_runnable\n",
|
||||
"\n",
|
||||
"wikipedia_retriever = WikipediaRetriever(load_all_available_meta=True, top_k_results=1)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def format_doc(doc, max_length=1000):\n",
|
||||
" related = \"- \".join(doc.metadata[\"categories\"])\n",
|
||||
" return f\"### {doc.metadata['title']}\\n\\nSummary: {doc.page_content}\\n\\nRelated\\n{related}\"[\n",
|
||||
" :max_length\n",
|
||||
" ]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def format_docs(docs):\n",
|
||||
" return \"\\n\\n\".join(format_doc(doc) for doc in docs)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@as_runnable\n",
|
||||
"async def survey_subjects(topic: str):\n",
|
||||
" related_subjects = await expand_chain.ainvoke({\"topic\": topic})\n",
|
||||
" retrieved_docs = await wikipedia_retriever.abatch(\n",
|
||||
" related_subjects.topics, return_exceptions=True\n",
|
||||
" )\n",
|
||||
" all_docs = []\n",
|
||||
" for docs in retrieved_docs:\n",
|
||||
" if isinstance(docs, BaseException):\n",
|
||||
" continue\n",
|
||||
" all_docs.extend(docs)\n",
|
||||
" formatted = format_docs(all_docs)\n",
|
||||
" return await gen_perspectives_chain.ainvoke({\"examples\": formatted, \"topic\": topic})"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -280,7 +457,40 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import Annotated\n\nfrom langchain_core.messages import AnyMessage\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import END, StateGraph, START\n\n\ndef add_messages(left, right):\n if not isinstance(left, list):\n left = [left]\n if not isinstance(right, list):\n right = [right]\n return left + right\n\n\ndef update_references(references, new_references):\n if not references:\n references = {}\n references.update(new_references)\n return references\n\n\ndef update_editor(editor, new_editor):\n # Can only set at the outset\n if not editor:\n return new_editor\n return editor\n\n\nclass InterviewState(TypedDict):\n messages: Annotated[List[AnyMessage], add_messages]\n references: Annotated[Optional[dict], update_references]\n editor: Annotated[Optional[Editor], update_editor]"
|
||||
"from typing import Annotated\n",
|
||||
"\n",
|
||||
"from langchain_core.messages import AnyMessage\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"from langgraph.graph import END, StateGraph, START\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def add_messages(left, right):\n",
|
||||
" if not isinstance(left, list):\n",
|
||||
" left = [left]\n",
|
||||
" if not isinstance(right, list):\n",
|
||||
" right = [right]\n",
|
||||
" return left + right\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def update_references(references, new_references):\n",
|
||||
" if not references:\n",
|
||||
" references = {}\n",
|
||||
" references.update(new_references)\n",
|
||||
" return references\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def update_editor(editor, new_editor):\n",
|
||||
" # Can only set at the outset\n",
|
||||
" if not editor:\n",
|
||||
" return new_editor\n",
|
||||
" return editor\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class InterviewState(TypedDict):\n",
|
||||
" messages: Annotated[List[AnyMessage], add_messages]\n",
|
||||
" references: Annotated[Optional[dict], update_references]\n",
|
||||
" editor: Annotated[Optional[Editor], update_editor]"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -298,7 +508,56 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_core.messages import AIMessage, HumanMessage, ToolMessage\nfrom langchain_core.prompts import MessagesPlaceholder\n\ngen_qn_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are an experienced Wikipedia writer and want to edit a specific page. \\\nBesides your identity as a Wikipedia writer, you have a specific focus when researching the topic. \\\nNow, you are chatting with an expert to get information. Ask good questions to get more useful information.\n\nWhen you have no more questions to ask, say \"Thank you so much for your help!\" to end the conversation.\\\nPlease only ask one question at a time and don't ask what you have asked before.\\\nYour questions should be related to the topic you want to write.\nBe comprehensive and curious, gaining as much unique insight from the expert as possible.\\\n\nStay true to your specific perspective:\n\n{persona}\"\"\",\n ),\n MessagesPlaceholder(variable_name=\"messages\", optional=True),\n ]\n)\n\n\ndef tag_with_name(ai_message: AIMessage, name: str):\n ai_message.name = name\n return ai_message\n\n\ndef swap_roles(state: InterviewState, name: str):\n converted = []\n for message in state[\"messages\"]:\n if isinstance(message, AIMessage) and message.name != name:\n message = HumanMessage(**message.dict(exclude={\"type\"}))\n converted.append(message)\n return {\"messages\": converted}\n\n\n@as_runnable\nasync def generate_question(state: InterviewState):\n editor = state[\"editor\"]\n gn_chain = (\n RunnableLambda(swap_roles).bind(name=editor.name)\n | gen_qn_prompt.partial(persona=editor.persona)\n | fast_llm\n | RunnableLambda(tag_with_name).bind(name=editor.name)\n )\n result = await gn_chain.ainvoke(state)\n return {\"messages\": [result]}"
|
||||
"from langchain_core.messages import AIMessage, HumanMessage, ToolMessage\n",
|
||||
"from langchain_core.prompts import MessagesPlaceholder\n",
|
||||
"\n",
|
||||
"gen_qn_prompt = ChatPromptTemplate.from_messages(\n",
|
||||
" [\n",
|
||||
" (\n",
|
||||
" \"system\",\n",
|
||||
" \"\"\"You are an experienced Wikipedia writer and want to edit a specific page. \\\n",
|
||||
"Besides your identity as a Wikipedia writer, you have a specific focus when researching the topic. \\\n",
|
||||
"Now, you are chatting with an expert to get information. Ask good questions to get more useful information.\n",
|
||||
"\n",
|
||||
"When you have no more questions to ask, say \"Thank you so much for your help!\" to end the conversation.\\\n",
|
||||
"Please only ask one question at a time and don't ask what you have asked before.\\\n",
|
||||
"Your questions should be related to the topic you want to write.\n",
|
||||
"Be comprehensive and curious, gaining as much unique insight from the expert as possible.\\\n",
|
||||
"\n",
|
||||
"Stay true to your specific perspective:\n",
|
||||
"\n",
|
||||
"{persona}\"\"\",\n",
|
||||
" ),\n",
|
||||
" MessagesPlaceholder(variable_name=\"messages\", optional=True),\n",
|
||||
" ]\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def tag_with_name(ai_message: AIMessage, name: str):\n",
|
||||
" ai_message.name = name\n",
|
||||
" return ai_message\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def swap_roles(state: InterviewState, name: str):\n",
|
||||
" converted = []\n",
|
||||
" for message in state[\"messages\"]:\n",
|
||||
" if isinstance(message, AIMessage) and message.name != name:\n",
|
||||
" message = HumanMessage(**message.dict(exclude={\"type\"}))\n",
|
||||
" converted.append(message)\n",
|
||||
" return {\"messages\": converted}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@as_runnable\n",
|
||||
"async def generate_question(state: InterviewState):\n",
|
||||
" editor = state[\"editor\"]\n",
|
||||
" gn_chain = (\n",
|
||||
" RunnableLambda(swap_roles).bind(name=editor.name)\n",
|
||||
" | gen_qn_prompt.partial(persona=editor.persona)\n",
|
||||
" | fast_llm\n",
|
||||
" | RunnableLambda(tag_with_name).bind(name=editor.name)\n",
|
||||
" )\n",
|
||||
" result = await gn_chain.ainvoke(state)\n",
|
||||
" return {\"messages\": [result]}"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -318,7 +577,17 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"messages = [\n HumanMessage(f\"So you said you were writing an article on {example_topic}?\")\n]\nquestion = await generate_question.ainvoke(\n {\n \"editor\": perspectives.editors[0],\n \"messages\": messages,\n }\n)\n\nquestion[\"messages\"][0].content"
|
||||
"messages = [\n",
|
||||
" HumanMessage(f\"So you said you were writing an article on {example_topic}?\")\n",
|
||||
"]\n",
|
||||
"question = await generate_question.ainvoke(\n",
|
||||
" {\n",
|
||||
" \"editor\": perspectives.editors[0],\n",
|
||||
" \"messages\": messages,\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"question[\"messages\"][0].content"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -336,7 +605,24 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"class Queries(BaseModel):\n queries: List[str] = Field(\n description=\"Comprehensive list of search engine queries to answer the user's questions.\",\n )\n\n\ngen_queries_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a helpful research assistant. Query the search engine to answer the user's questions.\",\n ),\n MessagesPlaceholder(variable_name=\"messages\", optional=True),\n ]\n)\ngen_queries_chain = gen_queries_prompt | ChatOpenAI(\n model=\"gpt-3.5-turbo\"\n).with_structured_output(Queries, include_raw=True)"
|
||||
"class Queries(BaseModel):\n",
|
||||
" queries: List[str] = Field(\n",
|
||||
" description=\"Comprehensive list of search engine queries to answer the user's questions.\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"gen_queries_prompt = ChatPromptTemplate.from_messages(\n",
|
||||
" [\n",
|
||||
" (\n",
|
||||
" \"system\",\n",
|
||||
" \"You are a helpful research assistant. Query the search engine to answer the user's questions.\",\n",
|
||||
" ),\n",
|
||||
" MessagesPlaceholder(variable_name=\"messages\", optional=True),\n",
|
||||
" ]\n",
|
||||
")\n",
|
||||
"gen_queries_chain = gen_queries_prompt | ChatOpenAI(\n",
|
||||
" model=\"gpt-3.5-turbo\"\n",
|
||||
").with_structured_output(Queries, include_raw=True)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -357,7 +643,10 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"queries = await gen_queries_chain.ainvoke(\n {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n)\nqueries[\"parsed\"].queries"
|
||||
"queries = await gen_queries_chain.ainvoke(\n",
|
||||
" {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n",
|
||||
")\n",
|
||||
"queries[\"parsed\"].queries"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -366,7 +655,38 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"class AnswerWithCitations(BaseModel):\n answer: str = Field(\n description=\"Comprehensive answer to the user's question with citations.\",\n )\n cited_urls: List[str] = Field(\n description=\"List of urls cited in the answer.\",\n )\n\n @property\n def as_str(self) -> str:\n return f\"{self.answer}\\n\\nCitations:\\n\\n\" + \"\\n\".join(\n f\"[{i+1}]: {url}\" for i, url in enumerate(self.cited_urls)\n )\n\n\ngen_answer_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are an expert who can use information effectively. You are chatting with a Wikipedia writer who wants\\\n to write a Wikipedia page on the topic you know. You have gathered the related information and will now use the information to form a response.\n\nMake your response as informative as possible and make sure every sentence is supported by the gathered information.\nEach response must be backed up by a citation from a reliable source, formatted as a footnote, reproducing the URLS after your response.\"\"\",\n ),\n MessagesPlaceholder(variable_name=\"messages\", optional=True),\n ]\n)\n\ngen_answer_chain = gen_answer_prompt | fast_llm.with_structured_output(\n AnswerWithCitations, include_raw=True\n).with_config(run_name=\"GenerateAnswer\")"
|
||||
"class AnswerWithCitations(BaseModel):\n",
|
||||
" answer: str = Field(\n",
|
||||
" description=\"Comprehensive answer to the user's question with citations.\",\n",
|
||||
" )\n",
|
||||
" cited_urls: List[str] = Field(\n",
|
||||
" description=\"List of urls cited in the answer.\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" @property\n",
|
||||
" def as_str(self) -> str:\n",
|
||||
" return f\"{self.answer}\\n\\nCitations:\\n\\n\" + \"\\n\".join(\n",
|
||||
" f\"[{i+1}]: {url}\" for i, url in enumerate(self.cited_urls)\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"gen_answer_prompt = ChatPromptTemplate.from_messages(\n",
|
||||
" [\n",
|
||||
" (\n",
|
||||
" \"system\",\n",
|
||||
" \"\"\"You are an expert who can use information effectively. You are chatting with a Wikipedia writer who wants\\\n",
|
||||
" to write a Wikipedia page on the topic you know. You have gathered the related information and will now use the information to form a response.\n",
|
||||
"\n",
|
||||
"Make your response as informative as possible and make sure every sentence is supported by the gathered information.\n",
|
||||
"Each response must be backed up by a citation from a reliable source, formatted as a footnote, reproducing the URLS after your response.\"\"\",\n",
|
||||
" ),\n",
|
||||
" MessagesPlaceholder(variable_name=\"messages\", optional=True),\n",
|
||||
" ]\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"gen_answer_chain = gen_answer_prompt | fast_llm.with_structured_output(\n",
|
||||
" AnswerWithCitations, include_raw=True\n",
|
||||
").with_config(run_name=\"GenerateAnswer\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -375,7 +695,29 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_community.utilities.duckduckgo_search import DuckDuckGoSearchAPIWrapper\nfrom langchain_core.tools import tool\n\n'''\n# Tavily is typically a better search engine, but your free queries are limited\nsearch_engine = TavilySearchResults(max_results=4)\n\n@tool\nasync def search_engine(query: str):\n \"\"\"Search engine to the internet.\"\"\"\n results = tavily_search.invoke(query)\n return [{\"content\": r[\"content\"], \"url\": r[\"url\"]} for r in results]\n'''\n\n# DDG\nsearch_engine = DuckDuckGoSearchAPIWrapper()\n\n\n@tool\nasync def search_engine(query: str):\n \"\"\"Search engine to the internet.\"\"\"\n results = DuckDuckGoSearchAPIWrapper()._ddgs_text(query)\n return [{\"content\": r[\"body\"], \"url\": r[\"href\"]} for r in results]"
|
||||
"from langchain_community.utilities.duckduckgo_search import DuckDuckGoSearchAPIWrapper\n",
|
||||
"from langchain_core.tools import tool\n",
|
||||
"\n",
|
||||
"'''\n",
|
||||
"# Tavily is typically a better search engine, but your free queries are limited\n",
|
||||
"search_engine = TavilySearchResults(max_results=4)\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"async def search_engine(query: str):\n",
|
||||
" \"\"\"Search engine to the internet.\"\"\"\n",
|
||||
" results = tavily_search.invoke(query)\n",
|
||||
" return [{\"content\": r[\"content\"], \"url\": r[\"url\"]} for r in results]\n",
|
||||
"'''\n",
|
||||
"\n",
|
||||
"# DDG\n",
|
||||
"search_engine = DuckDuckGoSearchAPIWrapper()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"async def search_engine(query: str):\n",
|
||||
" \"\"\"Search engine to the internet.\"\"\"\n",
|
||||
" results = DuckDuckGoSearchAPIWrapper()._ddgs_text(query)\n",
|
||||
" return [{\"content\": r[\"body\"], \"url\": r[\"href\"]} for r in results]"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -384,7 +726,43 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import json\n\nfrom langchain_core.runnables import RunnableConfig\n\n\nasync def gen_answer(\n state: InterviewState,\n config: Optional[RunnableConfig] = None,\n name: str = \"Subject_Matter_Expert\",\n max_str_len: int = 15000,\n):\n swapped_state = swap_roles(state, name) # Convert all other AI messages\n queries = await gen_queries_chain.ainvoke(swapped_state)\n query_results = await search_engine.abatch(\n queries[\"parsed\"].queries, config, return_exceptions=True\n )\n successful_results = [\n res for res in query_results if not isinstance(res, Exception)\n ]\n all_query_results = {\n res[\"url\"]: res[\"content\"] for results in successful_results for res in results\n }\n # We could be more precise about handling max token length if we wanted to here\n dumped = json.dumps(all_query_results)[:max_str_len]\n ai_message: AIMessage = queries[\"raw\"]\n tool_call = queries[\"raw\"].additional_kwargs[\"tool_calls\"][0]\n tool_id = tool_call[\"id\"]\n tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)\n swapped_state[\"messages\"].extend([ai_message, tool_message])\n # Only update the shared state with the final answer to avoid\n # polluting the dialogue history with intermediate messages\n generated = await gen_answer_chain.ainvoke(swapped_state)\n cited_urls = set(generated[\"parsed\"].cited_urls)\n # Save the retrieved information to a the shared state for future reference\n cited_references = {k: v for k, v in all_query_results.items() if k in cited_urls}\n formatted_message = AIMessage(name=name, content=generated[\"parsed\"].as_str)\n return {\"messages\": [formatted_message], \"references\": cited_references}"
|
||||
"import json\n",
|
||||
"\n",
|
||||
"from langchain_core.runnables import RunnableConfig\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async def gen_answer(\n",
|
||||
" state: InterviewState,\n",
|
||||
" config: Optional[RunnableConfig] = None,\n",
|
||||
" name: str = \"Subject_Matter_Expert\",\n",
|
||||
" max_str_len: int = 15000,\n",
|
||||
"):\n",
|
||||
" swapped_state = swap_roles(state, name) # Convert all other AI messages\n",
|
||||
" queries = await gen_queries_chain.ainvoke(swapped_state)\n",
|
||||
" query_results = await search_engine.abatch(\n",
|
||||
" queries[\"parsed\"].queries, config, return_exceptions=True\n",
|
||||
" )\n",
|
||||
" successful_results = [\n",
|
||||
" res for res in query_results if not isinstance(res, Exception)\n",
|
||||
" ]\n",
|
||||
" all_query_results = {\n",
|
||||
" res[\"url\"]: res[\"content\"] for results in successful_results for res in results\n",
|
||||
" }\n",
|
||||
" # We could be more precise about handling max token length if we wanted to here\n",
|
||||
" dumped = json.dumps(all_query_results)[:max_str_len]\n",
|
||||
" ai_message: AIMessage = queries[\"raw\"]\n",
|
||||
" tool_call = queries[\"raw\"].tool_calls[0]\n",
|
||||
" tool_id = tool_call[\"id\"]\n",
|
||||
" tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)\n",
|
||||
" swapped_state[\"messages\"].extend([ai_message, tool_message])\n",
|
||||
" # Only update the shared state with the final answer to avoid\n",
|
||||
" # polluting the dialogue history with intermediate messages\n",
|
||||
" generated = await gen_answer_chain.ainvoke(swapped_state)\n",
|
||||
" cited_urls = set(generated[\"parsed\"].cited_urls)\n",
|
||||
" # Save the retrieved information to a the shared state for future reference\n",
|
||||
" cited_references = {k: v for k, v in all_query_results.items() if k in cited_urls}\n",
|
||||
" formatted_message = AIMessage(name=name, content=generated[\"parsed\"].as_str)\n",
|
||||
" return {\"messages\": [formatted_message], \"references\": cited_references}"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -404,7 +782,10 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"example_answer = await gen_answer(\n {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n)\nexample_answer[\"messages\"][-1].content"
|
||||
"example_answer = await gen_answer(\n",
|
||||
" {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n",
|
||||
")\n",
|
||||
"example_answer[\"messages\"][-1].content"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -423,7 +804,31 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"max_num_turns = 5\n\n\ndef route_messages(state: InterviewState, name: str = \"Subject_Matter_Expert\"):\n messages = state[\"messages\"]\n num_responses = len(\n [m for m in messages if isinstance(m, AIMessage) and m.name == name]\n )\n if num_responses >= max_num_turns:\n return END\n last_question = messages[-2]\n if last_question.content.endswith(\"Thank you so much for your help!\"):\n return END\n return \"ask_question\"\n\n\nbuilder = StateGraph(InterviewState)\n\nbuilder.add_node(\"ask_question\", generate_question)\nbuilder.add_node(\"answer_question\", gen_answer)\nbuilder.add_conditional_edges(\"answer_question\", route_messages)\nbuilder.add_edge(\"ask_question\", \"answer_question\")\n\nbuilder.add_edge(START, \"ask_question\")\ninterview_graph = builder.compile().with_config(run_name=\"Conduct Interviews\")"
|
||||
"max_num_turns = 5\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def route_messages(state: InterviewState, name: str = \"Subject_Matter_Expert\"):\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
" num_responses = len(\n",
|
||||
" [m for m in messages if isinstance(m, AIMessage) and m.name == name]\n",
|
||||
" )\n",
|
||||
" if num_responses >= max_num_turns:\n",
|
||||
" return END\n",
|
||||
" last_question = messages[-2]\n",
|
||||
" if last_question.content.endswith(\"Thank you so much for your help!\"):\n",
|
||||
" return END\n",
|
||||
" return \"ask_question\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"builder = StateGraph(InterviewState)\n",
|
||||
"\n",
|
||||
"builder.add_node(\"ask_question\", generate_question)\n",
|
||||
"builder.add_node(\"answer_question\", gen_answer)\n",
|
||||
"builder.add_conditional_edges(\"answer_question\", route_messages)\n",
|
||||
"builder.add_edge(\"ask_question\", \"answer_question\")\n",
|
||||
"\n",
|
||||
"builder.add_edge(START, \"ask_question\")\n",
|
||||
"interview_graph = builder.compile().with_config(run_name=\"Conduct Interviews\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -444,7 +849,11 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from IPython.display import Image\n\n# Feel free to comment out if you have\n# not installed pygraphviz\nImage(interview_graph.get_graph().draw_png())"
|
||||
"from IPython.display import Image\n",
|
||||
"\n",
|
||||
"# Feel free to comment out if you have\n",
|
||||
"# not installed pygraphviz\n",
|
||||
"Image(interview_graph.get_graph().draw_png())"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -474,7 +883,23 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"final_step = None\n\ninitial_state = {\n \"editor\": perspectives.editors[0],\n \"messages\": [\n AIMessage(\n content=f\"So you said you were writing an article on {example_topic}?\",\n name=\"Subject_Matter_Expert\",\n )\n ],\n}\nasync for step in interview_graph.astream(initial_state):\n name = next(iter(step))\n print(name)\n print(\"-- \", str(step[name][\"messages\"])[:300])\n if END in step:\n final_step = step"
|
||||
"final_step = None\n",
|
||||
"\n",
|
||||
"initial_state = {\n",
|
||||
" \"editor\": perspectives.editors[0],\n",
|
||||
" \"messages\": [\n",
|
||||
" AIMessage(\n",
|
||||
" content=f\"So you said you were writing an article on {example_topic}?\",\n",
|
||||
" name=\"Subject_Matter_Expert\",\n",
|
||||
" )\n",
|
||||
" ],\n",
|
||||
"}\n",
|
||||
"async for step in interview_graph.astream(initial_state):\n",
|
||||
" name = next(iter(step))\n",
|
||||
" print(name)\n",
|
||||
" print(\"-- \", str(step[name][\"messages\"])[:300])\n",
|
||||
" if END in step:\n",
|
||||
" final_step = step"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -501,7 +926,29 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"refine_outline_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are a Wikipedia writer. You have gathered information from experts and search engines. Now, you are refining the outline of the Wikipedia page. \\\nYou need to make sure that the outline is comprehensive and specific. \\\nTopic you are writing about: {topic} \n\nOld outline:\n\n{old_outline}\"\"\",\n ),\n (\n \"user\",\n \"Refine the outline based on your conversations with subject-matter experts:\\n\\nConversations:\\n\\n{conversations}\\n\\nWrite the refined Wikipedia outline:\",\n ),\n ]\n)\n\n# Using turbo preview since the context can get quite long\nrefine_outline_chain = refine_outline_prompt | long_context_llm.with_structured_output(\n Outline\n)"
|
||||
"refine_outline_prompt = ChatPromptTemplate.from_messages(\n",
|
||||
" [\n",
|
||||
" (\n",
|
||||
" \"system\",\n",
|
||||
" \"\"\"You are a Wikipedia writer. You have gathered information from experts and search engines. Now, you are refining the outline of the Wikipedia page. \\\n",
|
||||
"You need to make sure that the outline is comprehensive and specific. \\\n",
|
||||
"Topic you are writing about: {topic} \n",
|
||||
"\n",
|
||||
"Old outline:\n",
|
||||
"\n",
|
||||
"{old_outline}\"\"\",\n",
|
||||
" ),\n",
|
||||
" (\n",
|
||||
" \"user\",\n",
|
||||
" \"Refine the outline based on your conversations with subject-matter experts:\\n\\nConversations:\\n\\n{conversations}\\n\\nWrite the refined Wikipedia outline:\",\n",
|
||||
" ),\n",
|
||||
" ]\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Using turbo preview since the context can get quite long\n",
|
||||
"refine_outline_chain = refine_outline_prompt | long_context_llm.with_structured_output(\n",
|
||||
" Outline\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -510,7 +957,15 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"refined_outline = refine_outline_chain.invoke(\n {\n \"topic\": example_topic,\n \"old_outline\": initial_outline.as_str,\n \"conversations\": \"\\n\\n\".join(\n f\"### {m.name}\\n\\n{m.content}\" for m in final_state[\"messages\"]\n ),\n }\n)"
|
||||
"refined_outline = refine_outline_chain.invoke(\n",
|
||||
" {\n",
|
||||
" \"topic\": example_topic,\n",
|
||||
" \"old_outline\": initial_outline.as_str,\n",
|
||||
" \"conversations\": \"\\n\\n\".join(\n",
|
||||
" f\"### {m.name}\\n\\n{m.content}\" for m in final_state[\"messages\"]\n",
|
||||
" ),\n",
|
||||
" }\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -595,7 +1050,23 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_community.vectorstores import SKLearnVectorStore\nfrom langchain_core.documents import Document\nfrom langchain_openai import OpenAIEmbeddings\n\nembeddings = OpenAIEmbeddings(model=\"text-embedding-3-small\")\nreference_docs = [\n Document(page_content=v, metadata={\"source\": k})\n for k, v in final_state[\"references\"].items()\n]\n# This really doesn't need to be a vectorstore for this size of data.\n# It could just be a numpy matrix. Or you could store documents\n# across requests if you want.\nvectorstore = SKLearnVectorStore.from_documents(\n reference_docs,\n embedding=embeddings,\n)\nretriever = vectorstore.as_retriever(k=10)"
|
||||
"from langchain_community.vectorstores import SKLearnVectorStore\n",
|
||||
"from langchain_core.documents import Document\n",
|
||||
"from langchain_openai import OpenAIEmbeddings\n",
|
||||
"\n",
|
||||
"embeddings = OpenAIEmbeddings(model=\"text-embedding-3-small\")\n",
|
||||
"reference_docs = [\n",
|
||||
" Document(page_content=v, metadata={\"source\": k})\n",
|
||||
" for k, v in final_state[\"references\"].items()\n",
|
||||
"]\n",
|
||||
"# This really doesn't need to be a vectorstore for this size of data.\n",
|
||||
"# It could just be a numpy matrix. Or you could store documents\n",
|
||||
"# across requests if you want.\n",
|
||||
"vectorstore = SKLearnVectorStore.from_documents(\n",
|
||||
" reference_docs,\n",
|
||||
" embedding=embeddings,\n",
|
||||
")\n",
|
||||
"retriever = vectorstore.as_retriever(k=10)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -636,7 +1107,67 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"class SubSection(BaseModel):\n subsection_title: str = Field(..., title=\"Title of the subsection\")\n content: str = Field(\n ...,\n title=\"Full content of the subsection. Include [#] citations to the cited sources where relevant.\",\n )\n\n @property\n def as_str(self) -> str:\n return f\"### {self.subsection_title}\\n\\n{self.content}\".strip()\n\n\nclass WikiSection(BaseModel):\n section_title: str = Field(..., title=\"Title of the section\")\n content: str = Field(..., title=\"Full content of the section\")\n subsections: Optional[List[Subsection]] = Field(\n default=None,\n title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n )\n citations: List[str] = Field(default_factory=list)\n\n @property\n def as_str(self) -> str:\n subsections = \"\\n\\n\".join(\n subsection.as_str for subsection in self.subsections or []\n )\n citations = \"\\n\".join([f\" [{i}] {cit}\" for i, cit in enumerate(self.citations)])\n return (\n f\"## {self.section_title}\\n\\n{self.content}\\n\\n{subsections}\".strip()\n + f\"\\n\\n{citations}\".strip()\n )\n\n\nsection_writer_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are an expert Wikipedia writer. Complete your assigned WikiSection from the following outline:\\n\\n\"\n \"{outline}\\n\\nCite your sources, using the following references:\\n\\n<Documents>\\n{docs}\\n<Documents>\",\n ),\n (\"user\", \"Write the full WikiSection for the {section} section.\"),\n ]\n)\n\n\nasync def retrieve(inputs: dict):\n docs = await retriever.ainvoke(inputs[\"topic\"] + \": \" + inputs[\"section\"])\n formatted = \"\\n\".join(\n [\n f'<Document href=\"{doc.metadata[\"source\"]}\"/>\\n{doc.page_content}\\n</Document>'\n for doc in docs\n ]\n )\n return {\"docs\": formatted, **inputs}\n\n\nsection_writer = (\n retrieve\n | section_writer_prompt\n | long_context_llm.with_structured_output(WikiSection)\n)"
|
||||
"class SubSection(BaseModel):\n",
|
||||
" subsection_title: str = Field(..., title=\"Title of the subsection\")\n",
|
||||
" content: str = Field(\n",
|
||||
" ...,\n",
|
||||
" title=\"Full content of the subsection. Include [#] citations to the cited sources where relevant.\",\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" @property\n",
|
||||
" def as_str(self) -> str:\n",
|
||||
" return f\"### {self.subsection_title}\\n\\n{self.content}\".strip()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class WikiSection(BaseModel):\n",
|
||||
" section_title: str = Field(..., title=\"Title of the section\")\n",
|
||||
" content: str = Field(..., title=\"Full content of the section\")\n",
|
||||
" subsections: Optional[List[Subsection]] = Field(\n",
|
||||
" default=None,\n",
|
||||
" title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n",
|
||||
" )\n",
|
||||
" citations: List[str] = Field(default_factory=list)\n",
|
||||
"\n",
|
||||
" @property\n",
|
||||
" def as_str(self) -> str:\n",
|
||||
" subsections = \"\\n\\n\".join(\n",
|
||||
" subsection.as_str for subsection in self.subsections or []\n",
|
||||
" )\n",
|
||||
" citations = \"\\n\".join([f\" [{i}] {cit}\" for i, cit in enumerate(self.citations)])\n",
|
||||
" return (\n",
|
||||
" f\"## {self.section_title}\\n\\n{self.content}\\n\\n{subsections}\".strip()\n",
|
||||
" + f\"\\n\\n{citations}\".strip()\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"section_writer_prompt = ChatPromptTemplate.from_messages(\n",
|
||||
" [\n",
|
||||
" (\n",
|
||||
" \"system\",\n",
|
||||
" \"You are an expert Wikipedia writer. Complete your assigned WikiSection from the following outline:\\n\\n\"\n",
|
||||
" \"{outline}\\n\\nCite your sources, using the following references:\\n\\n<Documents>\\n{docs}\\n<Documents>\",\n",
|
||||
" ),\n",
|
||||
" (\"user\", \"Write the full WikiSection for the {section} section.\"),\n",
|
||||
" ]\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async def retrieve(inputs: dict):\n",
|
||||
" docs = await retriever.ainvoke(inputs[\"topic\"] + \": \" + inputs[\"section\"])\n",
|
||||
" formatted = \"\\n\".join(\n",
|
||||
" [\n",
|
||||
" f'<Document href=\"{doc.metadata[\"source\"]}\"/>\\n{doc.page_content}\\n</Document>'\n",
|
||||
" for doc in docs\n",
|
||||
" ]\n",
|
||||
" )\n",
|
||||
" return {\"docs\": formatted, **inputs}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"section_writer = (\n",
|
||||
" retrieve\n",
|
||||
" | section_writer_prompt\n",
|
||||
" | long_context_llm.with_structured_output(WikiSection)\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -663,7 +1194,14 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"section = await section_writer.ainvoke(\n {\n \"outline\": refined_outline.as_str,\n \"section\": refined_outline.sections[1].section_title,\n \"topic\": example_topic,\n }\n)\nprint(section.as_str)"
|
||||
"section = await section_writer.ainvoke(\n",
|
||||
" {\n",
|
||||
" \"outline\": refined_outline.as_str,\n",
|
||||
" \"section\": refined_outline.sections[1].section_title,\n",
|
||||
" \"topic\": example_topic,\n",
|
||||
" }\n",
|
||||
")\n",
|
||||
"print(section.as_str)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -681,7 +1219,24 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain_core.output_parsers import StrOutputParser\n\nwriter_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are an expert Wikipedia author. Write the complete wiki article on {topic} using the following section drafts:\\n\\n\"\n \"{draft}\\n\\nStrictly follow Wikipedia format guidelines.\",\n ),\n (\n \"user\",\n 'Write the complete Wiki article using markdown format. Organize citations using footnotes like \"[1]\",'\n \" avoiding duplicates in the footer. Include URLs in the footer.\",\n ),\n ]\n)\n\nwriter = writer_prompt | long_context_llm | StrOutputParser()"
|
||||
"from langchain_core.output_parsers import StrOutputParser\n",
|
||||
"\n",
|
||||
"writer_prompt = ChatPromptTemplate.from_messages(\n",
|
||||
" [\n",
|
||||
" (\n",
|
||||
" \"system\",\n",
|
||||
" \"You are an expert Wikipedia author. Write the complete wiki article on {topic} using the following section drafts:\\n\\n\"\n",
|
||||
" \"{draft}\\n\\nStrictly follow Wikipedia format guidelines.\",\n",
|
||||
" ),\n",
|
||||
" (\n",
|
||||
" \"user\",\n",
|
||||
" 'Write the complete Wiki article using markdown format. Organize citations using footnotes like \"[1]\",'\n",
|
||||
" \" avoiding duplicates in the footer. Include URLs in the footer.\",\n",
|
||||
" ),\n",
|
||||
" ]\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"writer = writer_prompt | long_context_llm | StrOutputParser()"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -774,7 +1329,8 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for tok in writer.stream({\"topic\": example_topic, \"draft\": section.as_str}):\n print(tok, end=\"\")"
|
||||
"for tok in writer.stream({\"topic\": example_topic, \"draft\": section.as_str}):\n",
|
||||
" print(tok, end=\"\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -801,7 +1357,14 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"class ResearchState(TypedDict):\n topic: str\n outline: Outline\n editors: List[Editor]\n interview_results: List[InterviewState]\n # The final sections output\n sections: List[WikiSection]\n article: str"
|
||||
"class ResearchState(TypedDict):\n",
|
||||
" topic: str\n",
|
||||
" outline: Outline\n",
|
||||
" editors: List[Editor]\n",
|
||||
" interview_results: List[InterviewState]\n",
|
||||
" # The final sections output\n",
|
||||
" sections: List[WikiSection]\n",
|
||||
" article: str"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -810,7 +1373,109 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import asyncio\n\n\nasync def initialize_research(state: ResearchState):\n topic = state[\"topic\"]\n coros = (\n generate_outline_direct.ainvoke({\"topic\": topic}),\n survey_subjects.ainvoke(topic),\n )\n results = await asyncio.gather(*coros)\n return {\n **state,\n \"outline\": results[0],\n \"editors\": results[1].editors,\n }\n\n\nasync def conduct_interviews(state: ResearchState):\n topic = state[\"topic\"]\n initial_states = [\n {\n \"editor\": editor,\n \"messages\": [\n AIMessage(\n content=f\"So you said you were writing an article on {topic}?\",\n name=\"Subject_Matter_Expert\",\n )\n ],\n }\n for editor in state[\"editors\"]\n ]\n # We call in to the sub-graph here to parallelize the interviews\n interview_results = await interview_graph.abatch(initial_states)\n\n return {\n **state,\n \"interview_results\": interview_results,\n }\n\n\ndef format_conversation(interview_state):\n messages = interview_state[\"messages\"]\n convo = \"\\n\".join(f\"{m.name}: {m.content}\" for m in messages)\n return f'Conversation with {interview_state[\"editor\"].name}\\n\\n' + convo\n\n\nasync def refine_outline(state: ResearchState):\n convos = \"\\n\\n\".join(\n [\n format_conversation(interview_state)\n for interview_state in state[\"interview_results\"]\n ]\n )\n\n updated_outline = await refine_outline_chain.ainvoke(\n {\n \"topic\": state[\"topic\"],\n \"old_outline\": state[\"outline\"].as_str,\n \"conversations\": convos,\n }\n )\n return {**state, \"outline\": updated_outline}\n\n\nasync def index_references(state: ResearchState):\n all_docs = []\n for interview_state in state[\"interview_results\"]:\n reference_docs = [\n Document(page_content=v, metadata={\"source\": k})\n for k, v in interview_state[\"references\"].items()\n ]\n all_docs.extend(reference_docs)\n await vectorstore.aadd_documents(all_docs)\n return state\n\n\nasync def write_sections(state: ResearchState):\n outline = state[\"outline\"]\n sections = await section_writer.abatch(\n [\n {\n \"outline\": refined_outline.as_str,\n \"section\": section.section_title,\n \"topic\": state[\"topic\"],\n }\n for section in outline.sections\n ]\n )\n return {\n **state,\n \"sections\": sections,\n }\n\n\nasync def write_article(state: ResearchState):\n topic = state[\"topic\"]\n sections = state[\"sections\"]\n draft = \"\\n\\n\".join([section.as_str for section in sections])\n article = await writer.ainvoke({\"topic\": topic, \"draft\": draft})\n return {\n **state,\n \"article\": article,\n }"
|
||||
"import asyncio\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async def initialize_research(state: ResearchState):\n",
|
||||
" topic = state[\"topic\"]\n",
|
||||
" coros = (\n",
|
||||
" generate_outline_direct.ainvoke({\"topic\": topic}),\n",
|
||||
" survey_subjects.ainvoke(topic),\n",
|
||||
" )\n",
|
||||
" results = await asyncio.gather(*coros)\n",
|
||||
" return {\n",
|
||||
" **state,\n",
|
||||
" \"outline\": results[0],\n",
|
||||
" \"editors\": results[1].editors,\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async def conduct_interviews(state: ResearchState):\n",
|
||||
" topic = state[\"topic\"]\n",
|
||||
" initial_states = [\n",
|
||||
" {\n",
|
||||
" \"editor\": editor,\n",
|
||||
" \"messages\": [\n",
|
||||
" AIMessage(\n",
|
||||
" content=f\"So you said you were writing an article on {topic}?\",\n",
|
||||
" name=\"Subject_Matter_Expert\",\n",
|
||||
" )\n",
|
||||
" ],\n",
|
||||
" }\n",
|
||||
" for editor in state[\"editors\"]\n",
|
||||
" ]\n",
|
||||
" # We call in to the sub-graph here to parallelize the interviews\n",
|
||||
" interview_results = await interview_graph.abatch(initial_states)\n",
|
||||
"\n",
|
||||
" return {\n",
|
||||
" **state,\n",
|
||||
" \"interview_results\": interview_results,\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def format_conversation(interview_state):\n",
|
||||
" messages = interview_state[\"messages\"]\n",
|
||||
" convo = \"\\n\".join(f\"{m.name}: {m.content}\" for m in messages)\n",
|
||||
" return f'Conversation with {interview_state[\"editor\"].name}\\n\\n' + convo\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async def refine_outline(state: ResearchState):\n",
|
||||
" convos = \"\\n\\n\".join(\n",
|
||||
" [\n",
|
||||
" format_conversation(interview_state)\n",
|
||||
" for interview_state in state[\"interview_results\"]\n",
|
||||
" ]\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" updated_outline = await refine_outline_chain.ainvoke(\n",
|
||||
" {\n",
|
||||
" \"topic\": state[\"topic\"],\n",
|
||||
" \"old_outline\": state[\"outline\"].as_str,\n",
|
||||
" \"conversations\": convos,\n",
|
||||
" }\n",
|
||||
" )\n",
|
||||
" return {**state, \"outline\": updated_outline}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async def index_references(state: ResearchState):\n",
|
||||
" all_docs = []\n",
|
||||
" for interview_state in state[\"interview_results\"]:\n",
|
||||
" reference_docs = [\n",
|
||||
" Document(page_content=v, metadata={\"source\": k})\n",
|
||||
" for k, v in interview_state[\"references\"].items()\n",
|
||||
" ]\n",
|
||||
" all_docs.extend(reference_docs)\n",
|
||||
" await vectorstore.aadd_documents(all_docs)\n",
|
||||
" return state\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async def write_sections(state: ResearchState):\n",
|
||||
" outline = state[\"outline\"]\n",
|
||||
" sections = await section_writer.abatch(\n",
|
||||
" [\n",
|
||||
" {\n",
|
||||
" \"outline\": refined_outline.as_str,\n",
|
||||
" \"section\": section.section_title,\n",
|
||||
" \"topic\": state[\"topic\"],\n",
|
||||
" }\n",
|
||||
" for section in outline.sections\n",
|
||||
" ]\n",
|
||||
" )\n",
|
||||
" return {\n",
|
||||
" **state,\n",
|
||||
" \"sections\": sections,\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async def write_article(state: ResearchState):\n",
|
||||
" topic = state[\"topic\"]\n",
|
||||
" sections = state[\"sections\"]\n",
|
||||
" draft = \"\\n\\n\".join([section.as_str for section in sections])\n",
|
||||
" article = await writer.ainvoke({\"topic\": topic, \"draft\": draft})\n",
|
||||
" return {\n",
|
||||
" **state,\n",
|
||||
" \"article\": article,\n",
|
||||
" }"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -826,7 +1491,27 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n\nbuilder_of_storm = StateGraph(ResearchState)\n\nnodes = [\n (\"init_research\", initialize_research),\n (\"conduct_interviews\", conduct_interviews),\n (\"refine_outline\", refine_outline),\n (\"index_references\", index_references),\n (\"write_sections\", write_sections),\n (\"write_article\", write_article),\n]\nfor i in range(len(nodes)):\n name, node = nodes[i]\n builder_of_storm.add_node(name, node)\n if i > 0:\n builder_of_storm.add_edge(nodes[i - 1][0], name)\n\nbuilder_of_storm.add_edge(START, nodes[0][0])\nbuilder_of_storm.add_edge(nodes[-1][0], END)\nstorm = builder_of_storm.compile(checkpointer=MemorySaver())"
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"\n",
|
||||
"builder_of_storm = StateGraph(ResearchState)\n",
|
||||
"\n",
|
||||
"nodes = [\n",
|
||||
" (\"init_research\", initialize_research),\n",
|
||||
" (\"conduct_interviews\", conduct_interviews),\n",
|
||||
" (\"refine_outline\", refine_outline),\n",
|
||||
" (\"index_references\", index_references),\n",
|
||||
" (\"write_sections\", write_sections),\n",
|
||||
" (\"write_article\", write_article),\n",
|
||||
"]\n",
|
||||
"for i in range(len(nodes)):\n",
|
||||
" name, node = nodes[i]\n",
|
||||
" builder_of_storm.add_node(name, node)\n",
|
||||
" if i > 0:\n",
|
||||
" builder_of_storm.add_edge(nodes[i - 1][0], name)\n",
|
||||
"\n",
|
||||
"builder_of_storm.add_edge(START, nodes[0][0])\n",
|
||||
"builder_of_storm.add_edge(nodes[-1][0], END)\n",
|
||||
"storm = builder_of_storm.compile(checkpointer=MemorySaver())"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -877,7 +1562,16 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"my-thread\"}}\nasync for step in storm.astream(\n {\n \"topic\": \"Groq, NVIDIA, Llamma.cpp and the future of LLM Inference\",\n },\n config,\n):\n name = next(iter(step))\n print(name)\n print(\"-- \", str(step[name])[:300])"
|
||||
"config = {\"configurable\": {\"thread_id\": \"my-thread\"}}\n",
|
||||
"async for step in storm.astream(\n",
|
||||
" {\n",
|
||||
" \"topic\": \"Groq, NVIDIA, Llamma.cpp and the future of LLM Inference\",\n",
|
||||
" },\n",
|
||||
" config,\n",
|
||||
"):\n",
|
||||
" name = next(iter(step))\n",
|
||||
" print(name)\n",
|
||||
" print(\"-- \", str(step[name])[:300])"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -886,7 +1580,8 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"checkpoint = storm.get_state(config)\narticle = checkpoint.values[\"article\"]"
|
||||
"checkpoint = storm.get_state(config)\n",
|
||||
"article = checkpoint.values[\"article\"]"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -967,7 +1662,10 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from IPython.display import Markdown\n\n# We will down-header the sections to create less confusion in this notebook\nMarkdown(article.replace(\"\\n#\", \"\\n##\"))"
|
||||
"from IPython.display import Markdown\n",
|
||||
"\n",
|
||||
"# We will down-header the sections to create less confusion in this notebook\n",
|
||||
"Markdown(article.replace(\"\\n#\", \"\\n##\"))"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -975,9 +1673,7 @@
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
""
|
||||
]
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -14,9 +14,24 @@
|
||||
"Below is a simple toy example."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "95301021-1db9-426f-807c-ec5b37bd5a9d",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div class=\"admonition warning\">\n",
|
||||
" <p class=\"admonition-title\">ASYNC IN PYTHON<=3.10</p>\n",
|
||||
" <p>\n",
|
||||
"Any Langchain RunnableLambda, a RunnableGenerator, or Tool that invokes other runnables and is running async in python<=3.10, will have to propagate callbacks to child objects manually. This is because LangChain cannot automatically propagate callbacks to child objects in this case.\n",
|
||||
" \n",
|
||||
"This is a common reason why you may fail to see events being emitted from custom runnables or tools.\n",
|
||||
" </p>\n",
|
||||
"</div>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"execution_count": 1,
|
||||
"id": "486a01a0",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -51,7 +66,12 @@
|
||||
" messages = []\n",
|
||||
" # Tagging a node makes it easy to filter out which events to include in your stream\n",
|
||||
" # It's completely optional, but useful if you have many functions with similar names\n",
|
||||
" gen = RunnableGenerator(my_generator).with_config(tags=[\"should_stream\"])\n",
|
||||
" gen = RunnableGenerator(my_generator).with_config(\n",
|
||||
" tags=[\"should_stream\"],\n",
|
||||
" callbacks=config.get(\n",
|
||||
" \"callbacks\", []\n",
|
||||
" ), # <-- Propagate callbacks (Python <= 3.10)\n",
|
||||
" )\n",
|
||||
" async for message in gen.astream(state):\n",
|
||||
" messages.append(message)\n",
|
||||
" return {\"messages\": [AIMessage(content=\" \".join(messages))]}\n",
|
||||
@@ -65,7 +85,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"execution_count": 2,
|
||||
"id": "ce773a40",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -80,7 +100,7 @@
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"/Users/harrisonchase/.pyenv/versions/3.11.1/envs/permchain/lib/python3.11/site-packages/langchain_core/_api/beta_decorator.py:87: LangChainBetaWarning: This API is in beta and may change in the future.\n",
|
||||
"/Users/vadymbarda/.virtualenvs/langgraph/lib/python3.11/site-packages/langchain_core/_api/beta_decorator.py:87: LangChainBetaWarning: This API is in beta and may change in the future.\n",
|
||||
" warn_beta(\n"
|
||||
]
|
||||
}
|
||||
@@ -89,7 +109,7 @@
|
||||
"from langchain_core.messages import HumanMessage\n",
|
||||
"\n",
|
||||
"inputs = [HumanMessage(content=\"What are you thinking about?\")]\n",
|
||||
"async for event in app.astream_events({\"messages\": inputs}, version=\"v1\"):\n",
|
||||
"async for event in app.astream_events({\"messages\": inputs}, version=\"v2\"):\n",
|
||||
" kind = event[\"event\"]\n",
|
||||
" tags = event.get(\"tags\", [])\n",
|
||||
" if kind == \"on_chain_stream\" and \"should_stream\" in tags:\n",
|
||||
@@ -104,7 +124,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "2c7b7902-2d80-4bf9-91c1-737b749e58a3",
|
||||
"id": "615cb9d2-bfa2-4f83-90b0-c6c2d1e6df95",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
@@ -112,9 +132,9 @@
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"display_name": "langgraph",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
"name": "langgraph"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
@@ -126,7 +146,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.1"
|
||||
"version": "3.11.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -30,10 +30,7 @@
|
||||
"id": "47f79af8-58d8-4a48-8d9a-88823d88701f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph openai"
|
||||
]
|
||||
"source": ["%%capture --no-stderr\n%pip install -U langgraph openai"]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -49,18 +46,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"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\")"
|
||||
]
|
||||
"source": ["import getpass\nimport os\n\n\ndef _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",
|
||||
@@ -84,94 +70,7 @@
|
||||
"id": "d59234f9-173e-469d-a725-c13e0979663e",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from openai import AsyncOpenAI\n",
|
||||
"from langchain_core.language_models.chat_models import ChatGenerationChunk\n",
|
||||
"from langchain_core.messages import AIMessageChunk\n",
|
||||
"from langchain_core.runnables.config import (\n",
|
||||
" ensure_config,\n",
|
||||
" get_callback_manager_for_config,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"openai_client = AsyncOpenAI()\n",
|
||||
"# define tool schema for openai tool calling\n",
|
||||
"\n",
|
||||
"tool = {\n",
|
||||
" \"type\": \"function\",\n",
|
||||
" \"function\": {\n",
|
||||
" \"name\": \"get_items\",\n",
|
||||
" \"description\": \"Use this tool to look up which items are in the given place.\",\n",
|
||||
" \"parameters\": {\n",
|
||||
" \"type\": \"object\",\n",
|
||||
" \"properties\": {\"place\": {\"type\": \"string\"}},\n",
|
||||
" \"required\": [\"place\"],\n",
|
||||
" },\n",
|
||||
" },\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async def call_model(state, config=None):\n",
|
||||
" config = ensure_config(config | {\"tags\": [\"agent_llm\"]})\n",
|
||||
" callback_manager = get_callback_manager_for_config(config)\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
"\n",
|
||||
" llm_run_manager = callback_manager.on_chat_model_start({}, [messages])[0]\n",
|
||||
" response = await openai_client.chat.completions.create(\n",
|
||||
" messages=messages, model=\"gpt-3.5-turbo\", tools=[tool], stream=True\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" response_content = \"\"\n",
|
||||
" role = None\n",
|
||||
"\n",
|
||||
" tool_call_id = None\n",
|
||||
" tool_call_function_name = None\n",
|
||||
" tool_call_function_arguments = \"\"\n",
|
||||
" async for chunk in response:\n",
|
||||
" delta = chunk.choices[0].delta\n",
|
||||
" if delta.role is not None:\n",
|
||||
" role = delta.role\n",
|
||||
"\n",
|
||||
" if delta.content:\n",
|
||||
" response_content += delta.content\n",
|
||||
" llm_run_manager.on_llm_new_token(delta.content)\n",
|
||||
"\n",
|
||||
" if delta.tool_calls:\n",
|
||||
" # note: for simplicity we're only handling a single tool call here\n",
|
||||
" if delta.tool_calls[0].function.name is not None:\n",
|
||||
" tool_call_function_name = delta.tool_calls[0].function.name\n",
|
||||
" tool_call_id = delta.tool_calls[0].id\n",
|
||||
"\n",
|
||||
" # note: we're wrapping the tools calls in ChatGenerationChunk so that the events from .astream_events in the graph can render tool calls correctly\n",
|
||||
" tool_call_chunk = ChatGenerationChunk(\n",
|
||||
" message=AIMessageChunk(\n",
|
||||
" content=\"\",\n",
|
||||
" additional_kwargs={\"tool_calls\": [delta.tool_calls[0].dict()]},\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
" llm_run_manager.on_llm_new_token(\"\", chunk=tool_call_chunk)\n",
|
||||
" tool_call_function_arguments += delta.tool_calls[0].function.arguments\n",
|
||||
"\n",
|
||||
" if tool_call_function_name is not None:\n",
|
||||
" tool_calls = [\n",
|
||||
" {\n",
|
||||
" \"id\": tool_call_id,\n",
|
||||
" \"function\": {\n",
|
||||
" \"name\": tool_call_function_name,\n",
|
||||
" \"arguments\": tool_call_function_arguments,\n",
|
||||
" },\n",
|
||||
" \"type\": \"function\",\n",
|
||||
" }\n",
|
||||
" ]\n",
|
||||
" else:\n",
|
||||
" tool_calls = None\n",
|
||||
"\n",
|
||||
" response_message = {\n",
|
||||
" \"role\": role,\n",
|
||||
" \"content\": response_content,\n",
|
||||
" \"tool_calls\": tool_calls,\n",
|
||||
" }\n",
|
||||
" return {\"messages\": [response_message]}"
|
||||
]
|
||||
"source": ["from openai import AsyncOpenAI\nfrom langchain_core.language_models.chat_models import ChatGenerationChunk\nfrom langchain_core.messages import AIMessageChunk\nfrom langchain_core.runnables.config import (\n ensure_config,\n get_callback_manager_for_config,\n)\n\nopenai_client = AsyncOpenAI()\n# define tool schema for openai tool calling\n\ntool = {\n \"type\": \"function\",\n \"function\": {\n \"name\": \"get_items\",\n \"description\": \"Use this tool to look up which items are in the given place.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\"place\": {\"type\": \"string\"}},\n \"required\": [\"place\"],\n },\n },\n}\n\n\nasync def call_model(state, config=None):\n config = ensure_config(config | {\"tags\": [\"agent_llm\"]})\n callback_manager = get_callback_manager_for_config(config)\n messages = state[\"messages\"]\n\n llm_run_manager = callback_manager.on_chat_model_start({}, [messages])[0]\n response = await openai_client.chat.completions.create(\n messages=messages, model=\"gpt-3.5-turbo\", tools=[tool], stream=True\n )\n\n response_content = \"\"\n role = None\n\n tool_call_id = None\n tool_call_function_name = None\n tool_call_function_arguments = \"\"\n async for chunk in response:\n delta = chunk.choices[0].delta\n if delta.role is not None:\n role = delta.role\n\n if delta.content:\n response_content += delta.content\n llm_run_manager.on_llm_new_token(delta.content)\n\n if delta.tool_calls:\n # note: for simplicity we're only handling a single tool call here\n if delta.tool_calls[0].function.name is not None:\n tool_call_function_name = delta.tool_calls[0].function.name\n tool_call_id = delta.tool_calls[0].id\n\n # note: we're wrapping the tools calls in ChatGenerationChunk so that the events from .astream_events in the graph can render tool calls correctly\n tool_call_chunk = ChatGenerationChunk(\n message=AIMessageChunk(\n content=\"\",\n additional_kwargs={\"tool_calls\": [delta.tool_calls[0].dict()]},\n )\n )\n llm_run_manager.on_llm_new_token(\"\", chunk=tool_call_chunk)\n tool_call_function_arguments += delta.tool_calls[0].function.arguments\n\n if tool_call_function_name is not None:\n tool_calls = [\n {\n \"id\": tool_call_id,\n \"function\": {\n \"name\": tool_call_function_name,\n \"arguments\": tool_call_function_arguments,\n },\n \"type\": \"function\",\n }\n ]\n else:\n tool_calls = None\n\n response_message = {\n \"role\": role,\n \"content\": response_content,\n \"tool_calls\": tool_calls,\n }\n return {\"messages\": [response_message]}"]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -187,62 +86,7 @@
|
||||
"id": "b90941d8-afe4-42ec-9262-9c3b87c3b1ec",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import json\n",
|
||||
"from langchain_core.callbacks import adispatch_custom_event\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async def get_items(place: str) -> str:\n",
|
||||
" \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n",
|
||||
"\n",
|
||||
" # this can be replaced with any actual streaming logic that you might have\n",
|
||||
" def stream(place: str):\n",
|
||||
" if \"bed\" in place: # For under the bed\n",
|
||||
" yield from [\"socks\", \"shoes\", \"dust bunnies\"]\n",
|
||||
" elif \"shelf\" in place: # For 'shelf'\n",
|
||||
" yield from [\"books\", \"penciles\", \"pictures\"]\n",
|
||||
" else: # if the agent decides to ask about a different place\n",
|
||||
" yield \"cat snacks\"\n",
|
||||
"\n",
|
||||
" tokens = []\n",
|
||||
" for token in stream(place):\n",
|
||||
" await adispatch_custom_event(\n",
|
||||
" # this will allow you to filter events by name\n",
|
||||
" \"tool_call_token_stream\",\n",
|
||||
" {\n",
|
||||
" \"function_name\": \"get_items\",\n",
|
||||
" \"arguments\": {\"place\": place},\n",
|
||||
" \"tool_output_token\": token,\n",
|
||||
" },\n",
|
||||
" # this will allow you to filter events by tags\n",
|
||||
" config={\"tags\": [\"tool_call\"]},\n",
|
||||
" )\n",
|
||||
" tokens.append(token)\n",
|
||||
"\n",
|
||||
" return \", \".join(tokens)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# define mapping to look up functions when running tools\n",
|
||||
"function_name_to_function = {\"get_items\": get_items}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async def call_tools(state):\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
"\n",
|
||||
" tool_call = messages[-1][\"tool_calls\"][0]\n",
|
||||
" function_name = tool_call[\"function\"][\"name\"]\n",
|
||||
" function_arguments = tool_call[\"function\"][\"arguments\"]\n",
|
||||
" arguments = json.loads(function_arguments)\n",
|
||||
"\n",
|
||||
" function_response = await function_name_to_function[function_name](**arguments)\n",
|
||||
" tool_message = {\n",
|
||||
" \"tool_call_id\": tool_call[\"id\"],\n",
|
||||
" \"role\": \"tool\",\n",
|
||||
" \"name\": function_name,\n",
|
||||
" \"content\": function_response,\n",
|
||||
" }\n",
|
||||
" return {\"messages\": [tool_message]}"
|
||||
]
|
||||
"source": ["import json\nfrom langchain_core.callbacks import adispatch_custom_event\n\n\nasync def get_items(place: str) -> str:\n \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n\n # this can be replaced with any actual streaming logic that you might have\n def stream(place: str):\n if \"bed\" in place: # For under the bed\n yield from [\"socks\", \"shoes\", \"dust bunnies\"]\n elif \"shelf\" in place: # For 'shelf'\n yield from [\"books\", \"penciles\", \"pictures\"]\n else: # if the agent decides to ask about a different place\n yield \"cat snacks\"\n\n tokens = []\n for token in stream(place):\n await adispatch_custom_event(\n # this will allow you to filter events by name\n \"tool_call_token_stream\",\n {\n \"function_name\": \"get_items\",\n \"arguments\": {\"place\": place},\n \"tool_output_token\": token,\n },\n # this will allow you to filter events by tags\n config={\"tags\": [\"tool_call\"]},\n )\n tokens.append(token)\n\n return \", \".join(tokens)\n\n\n# define mapping to look up functions when running tools\nfunction_name_to_function = {\"get_items\": get_items}\n\n\nasync def call_tools(state):\n messages = state[\"messages\"]\n\n tool_call = messages[-1][\"tool_calls\"][0]\n function_name = tool_call[\"function\"][\"name\"]\n function_arguments = tool_call[\"function\"][\"arguments\"]\n arguments = json.loads(function_arguments)\n\n function_response = await function_name_to_function[function_name](**arguments)\n tool_message = {\n \"tool_call_id\": tool_call[\"id\"],\n \"role\": \"tool\",\n \"name\": function_name,\n \"content\": function_response,\n }\n return {\"messages\": [tool_message]}"]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -258,33 +102,7 @@
|
||||
"id": "228260be-1f9a-4195-80e0-9604f8a5dba6",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import operator\n",
|
||||
"from typing import Annotated, TypedDict, Literal\n",
|
||||
"\n",
|
||||
"from langgraph.graph import StateGraph, END\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class State(TypedDict):\n",
|
||||
" messages: Annotated[list, operator.add]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def should_continue(state) -> Literal[\"tools\", END]:\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
" last_message = messages[-1]\n",
|
||||
" if last_message[\"tool_calls\"]:\n",
|
||||
" return \"tools\"\n",
|
||||
" return END\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"workflow = StateGraph(State)\n",
|
||||
"workflow.set_entry_point(\"model\")\n",
|
||||
"workflow.add_node(\"model\", call_model) # i.e. our \"agent\"\n",
|
||||
"workflow.add_node(\"tools\", call_tools)\n",
|
||||
"workflow.add_conditional_edges(\"model\", should_continue)\n",
|
||||
"workflow.add_edge(\"tools\", \"model\")\n",
|
||||
"graph = workflow.compile()"
|
||||
]
|
||||
"source": ["import operator\nfrom typing import Annotated, TypedDict, Literal\n\nfrom langgraph.graph import StateGraph, END, START\n\n\nclass State(TypedDict):\n messages: Annotated[list, operator.add]\n\n\ndef should_continue(state) -> Literal[\"tools\", END]:\n messages = state[\"messages\"]\n last_message = messages[-1]\n if last_message[\"tool_calls\"]:\n return \"tools\"\n return END\n\n\nworkflow = StateGraph(State)\nworkflow.add_edge(START, \"model\")\nworkflow.add_node(\"model\", call_model) # i.e. our \"agent\"\nworkflow.add_node(\"tools\", call_tools)\nworkflow.add_conditional_edges(\"model\", should_continue)\nworkflow.add_edge(\"tools\", \"model\")\ngraph = workflow.compile()"]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -318,14 +136,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"async for event in graph.astream_events(\n",
|
||||
" {\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, version=\"v2\"\n",
|
||||
"):\n",
|
||||
" tags = event.get(\"tags\", [])\n",
|
||||
" if event[\"event\"] == \"on_custom_event\" and \"tool_call\" in tags:\n",
|
||||
" print(\"Tool token\", event[\"data\"][\"tool_output_token\"])"
|
||||
]
|
||||
"source": ["async for event in graph.astream_events(\n {\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, version=\"v2\"\n):\n tags = event.get(\"tags\", [])\n if event[\"event\"] == \"on_custom_event\" and \"tool_call\" in tags:\n print(\"Tool token\", event[\"data\"][\"tool_output_token\"])"]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -30,10 +30,7 @@
|
||||
"id": "47f79af8-58d8-4a48-8d9a-88823d88701f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph openai"
|
||||
]
|
||||
"source": ["%%capture --no-stderr\n%pip install -U langgraph openai"]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -49,18 +46,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"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\")"
|
||||
]
|
||||
"source": ["import getpass\nimport os\n\n\ndef _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",
|
||||
@@ -84,94 +70,7 @@
|
||||
"id": "d59234f9-173e-469d-a725-c13e0979663e",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from openai import AsyncOpenAI\n",
|
||||
"from langchain_core.language_models.chat_models import ChatGenerationChunk\n",
|
||||
"from langchain_core.messages import AIMessageChunk\n",
|
||||
"from langchain_core.runnables.config import (\n",
|
||||
" ensure_config,\n",
|
||||
" get_callback_manager_for_config,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"openai_client = AsyncOpenAI()\n",
|
||||
"# define tool schema for openai tool calling\n",
|
||||
"\n",
|
||||
"tool = {\n",
|
||||
" \"type\": \"function\",\n",
|
||||
" \"function\": {\n",
|
||||
" \"name\": \"get_items\",\n",
|
||||
" \"description\": \"Use this tool to look up which items are in the given place.\",\n",
|
||||
" \"parameters\": {\n",
|
||||
" \"type\": \"object\",\n",
|
||||
" \"properties\": {\"place\": {\"type\": \"string\"}},\n",
|
||||
" \"required\": [\"place\"],\n",
|
||||
" },\n",
|
||||
" },\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async def call_model(state, config=None):\n",
|
||||
" config = ensure_config(config | {\"tags\": [\"agent_llm\"]})\n",
|
||||
" callback_manager = get_callback_manager_for_config(config)\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
"\n",
|
||||
" llm_run_manager = callback_manager.on_chat_model_start({}, [messages])[0]\n",
|
||||
" response = await openai_client.chat.completions.create(\n",
|
||||
" messages=messages, model=\"gpt-3.5-turbo\", tools=[tool], stream=True\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" response_content = \"\"\n",
|
||||
" role = None\n",
|
||||
"\n",
|
||||
" tool_call_id = None\n",
|
||||
" tool_call_function_name = None\n",
|
||||
" tool_call_function_arguments = \"\"\n",
|
||||
" async for chunk in response:\n",
|
||||
" delta = chunk.choices[0].delta\n",
|
||||
" if delta.role is not None:\n",
|
||||
" role = delta.role\n",
|
||||
"\n",
|
||||
" if delta.content:\n",
|
||||
" response_content += delta.content\n",
|
||||
" llm_run_manager.on_llm_new_token(delta.content)\n",
|
||||
"\n",
|
||||
" if delta.tool_calls:\n",
|
||||
" # note: for simplicity we're only handling a single tool call here\n",
|
||||
" if delta.tool_calls[0].function.name is not None:\n",
|
||||
" tool_call_function_name = delta.tool_calls[0].function.name\n",
|
||||
" tool_call_id = delta.tool_calls[0].id\n",
|
||||
"\n",
|
||||
" # note: we're wrapping the tools calls in ChatGenerationChunk so that the events from .astream_events in the graph can render tool calls correctly\n",
|
||||
" tool_call_chunk = ChatGenerationChunk(\n",
|
||||
" message=AIMessageChunk(\n",
|
||||
" content=\"\",\n",
|
||||
" additional_kwargs={\"tool_calls\": [delta.tool_calls[0].dict()]},\n",
|
||||
" )\n",
|
||||
" )\n",
|
||||
" llm_run_manager.on_llm_new_token(\"\", chunk=tool_call_chunk)\n",
|
||||
" tool_call_function_arguments += delta.tool_calls[0].function.arguments\n",
|
||||
"\n",
|
||||
" if tool_call_function_name is not None:\n",
|
||||
" tool_calls = [\n",
|
||||
" {\n",
|
||||
" \"id\": tool_call_id,\n",
|
||||
" \"function\": {\n",
|
||||
" \"name\": tool_call_function_name,\n",
|
||||
" \"arguments\": tool_call_function_arguments,\n",
|
||||
" },\n",
|
||||
" \"type\": \"function\",\n",
|
||||
" }\n",
|
||||
" ]\n",
|
||||
" else:\n",
|
||||
" tool_calls = None\n",
|
||||
"\n",
|
||||
" response_message = {\n",
|
||||
" \"role\": role,\n",
|
||||
" \"content\": response_content,\n",
|
||||
" \"tool_calls\": tool_calls,\n",
|
||||
" }\n",
|
||||
" return {\"messages\": [response_message]}"
|
||||
]
|
||||
"source": ["from openai import AsyncOpenAI\nfrom langchain_core.language_models.chat_models import ChatGenerationChunk\nfrom langchain_core.messages import AIMessageChunk\nfrom langchain_core.runnables.config import (\n ensure_config,\n get_callback_manager_for_config,\n)\n\nopenai_client = AsyncOpenAI()\n# define tool schema for openai tool calling\n\ntool = {\n \"type\": \"function\",\n \"function\": {\n \"name\": \"get_items\",\n \"description\": \"Use this tool to look up which items are in the given place.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\"place\": {\"type\": \"string\"}},\n \"required\": [\"place\"],\n },\n },\n}\n\n\nasync def call_model(state, config=None):\n config = ensure_config(config | {\"tags\": [\"agent_llm\"]})\n callback_manager = get_callback_manager_for_config(config)\n messages = state[\"messages\"]\n\n llm_run_manager = callback_manager.on_chat_model_start({}, [messages])[0]\n response = await openai_client.chat.completions.create(\n messages=messages, model=\"gpt-3.5-turbo\", tools=[tool], stream=True\n )\n\n response_content = \"\"\n role = None\n\n tool_call_id = None\n tool_call_function_name = None\n tool_call_function_arguments = \"\"\n async for chunk in response:\n delta = chunk.choices[0].delta\n if delta.role is not None:\n role = delta.role\n\n if delta.content:\n response_content += delta.content\n llm_run_manager.on_llm_new_token(delta.content)\n\n if delta.tool_calls:\n # note: for simplicity we're only handling a single tool call here\n if delta.tool_calls[0].function.name is not None:\n tool_call_function_name = delta.tool_calls[0].function.name\n tool_call_id = delta.tool_calls[0].id\n\n # note: we're wrapping the tools calls in ChatGenerationChunk so that the events from .astream_events in the graph can render tool calls correctly\n tool_call_chunk = ChatGenerationChunk(\n message=AIMessageChunk(\n content=\"\",\n additional_kwargs={\"tool_calls\": [delta.tool_calls[0].dict()]},\n )\n )\n llm_run_manager.on_llm_new_token(\"\", chunk=tool_call_chunk)\n tool_call_function_arguments += delta.tool_calls[0].function.arguments\n\n if tool_call_function_name is not None:\n tool_calls = [\n {\n \"id\": tool_call_id,\n \"function\": {\n \"name\": tool_call_function_name,\n \"arguments\": tool_call_function_arguments,\n },\n \"type\": \"function\",\n }\n ]\n else:\n tool_calls = None\n\n response_message = {\n \"role\": role,\n \"content\": response_content,\n \"tool_calls\": tool_calls,\n }\n return {\"messages\": [response_message]}"]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -187,41 +86,7 @@
|
||||
"id": "b756ea32",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import json\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async def get_items(place: str) -> str:\n",
|
||||
" \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n",
|
||||
" if \"bed\" in place: # For under the bed\n",
|
||||
" return \"socks, shoes and dust bunnies\"\n",
|
||||
" if \"shelf\" in place: # For 'shelf'\n",
|
||||
" return \"books, penciles and pictures\"\n",
|
||||
" else: # if the agent decides to ask about a different place\n",
|
||||
" return \"cat snacks\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# define mapping to look up functions when running tools\n",
|
||||
"function_name_to_function = {\"get_items\": get_items}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async def call_tools(state):\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
"\n",
|
||||
" tool_call = messages[-1][\"tool_calls\"][0]\n",
|
||||
" function_name = tool_call[\"function\"][\"name\"]\n",
|
||||
" function_arguments = tool_call[\"function\"][\"arguments\"]\n",
|
||||
" arguments = json.loads(function_arguments)\n",
|
||||
"\n",
|
||||
" function_response = await function_name_to_function[function_name](**arguments)\n",
|
||||
" tool_message = {\n",
|
||||
" \"tool_call_id\": tool_call[\"id\"],\n",
|
||||
" \"role\": \"tool\",\n",
|
||||
" \"name\": function_name,\n",
|
||||
" \"content\": function_response,\n",
|
||||
" }\n",
|
||||
" return {\"messages\": [tool_message]}"
|
||||
]
|
||||
"source": ["import json\n\n\nasync def get_items(place: str) -> str:\n \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n if \"bed\" in place: # For under the bed\n return \"socks, shoes and dust bunnies\"\n if \"shelf\" in place: # For 'shelf'\n return \"books, penciles and pictures\"\n else: # if the agent decides to ask about a different place\n return \"cat snacks\"\n\n\n# define mapping to look up functions when running tools\nfunction_name_to_function = {\"get_items\": get_items}\n\n\nasync def call_tools(state):\n messages = state[\"messages\"]\n\n tool_call = messages[-1][\"tool_calls\"][0]\n function_name = tool_call[\"function\"][\"name\"]\n function_arguments = tool_call[\"function\"][\"arguments\"]\n arguments = json.loads(function_arguments)\n\n function_response = await function_name_to_function[function_name](**arguments)\n tool_message = {\n \"tool_call_id\": tool_call[\"id\"],\n \"role\": \"tool\",\n \"name\": function_name,\n \"content\": function_response,\n }\n return {\"messages\": [tool_message]}"]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -237,33 +102,7 @@
|
||||
"id": "228260be-1f9a-4195-80e0-9604f8a5dba6",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import operator\n",
|
||||
"from typing import Annotated, TypedDict, Literal\n",
|
||||
"\n",
|
||||
"from langgraph.graph import StateGraph, END\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class State(TypedDict):\n",
|
||||
" messages: Annotated[list, operator.add]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def should_continue(state) -> Literal[\"tools\", END]:\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
" last_message = messages[-1]\n",
|
||||
" if last_message[\"tool_calls\"]:\n",
|
||||
" return \"tools\"\n",
|
||||
" return END\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"workflow = StateGraph(State)\n",
|
||||
"workflow.set_entry_point(\"model\")\n",
|
||||
"workflow.add_node(\"model\", call_model) # i.e. our \"agent\"\n",
|
||||
"workflow.add_node(\"tools\", call_tools)\n",
|
||||
"workflow.add_conditional_edges(\"model\", should_continue)\n",
|
||||
"workflow.add_edge(\"tools\", \"model\")\n",
|
||||
"graph = workflow.compile()"
|
||||
]
|
||||
"source": ["import operator\nfrom typing import Annotated, TypedDict, Literal\n\nfrom langgraph.graph import StateGraph, END, START\n\n\nclass State(TypedDict):\n messages: Annotated[list, operator.add]\n\n\ndef should_continue(state) -> Literal[\"tools\", END]:\n messages = state[\"messages\"]\n last_message = messages[-1]\n if last_message[\"tool_calls\"]:\n return \"tools\"\n return END\n\n\nworkflow = StateGraph(State)\nworkflow.add_edge(START, \"model\")\nworkflow.add_node(\"model\", call_model) # i.e. our \"agent\"\nworkflow.add_node(\"tools\", call_tools)\nworkflow.add_conditional_edges(\"model\", should_continue)\nworkflow.add_edge(\"tools\", \"model\")\ngraph = workflow.compile()"]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -328,14 +167,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"async for event in graph.astream_events(\n",
|
||||
" {\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, version=\"v2\"\n",
|
||||
"):\n",
|
||||
" tags = event.get(\"tags\", [])\n",
|
||||
" if event[\"event\"] == \"on_chat_model_stream\" and \"agent_llm\" in tags:\n",
|
||||
" print(\"LLM token\", event[\"data\"][\"chunk\"].dict())"
|
||||
]
|
||||
"source": ["async for event in graph.astream_events(\n {\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, version=\"v2\"\n):\n tags = event.get(\"tags\", [])\n if event[\"event\"] == \"on_chat_model_stream\" and \"agent_llm\" in tags:\n print(\"LLM token\", event[\"data\"][\"chunk\"].dict())"]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -343,7 +175,7 @@
|
||||
"id": "adb0f7bc-6e51-478e-bd32-8f72df072d6c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
"source": [""]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -169,9 +169,7 @@
|
||||
"from langchain_core.output_parsers import JsonOutputParser\n",
|
||||
"\n",
|
||||
"# JSON\n",
|
||||
"llm = ChatOllama(model=\"llama3.1\", \n",
|
||||
" format=\"json\", \n",
|
||||
" temperature=0)\n",
|
||||
"llm = ChatOllama(model=\"llama3.1\", format=\"json\", temperature=0)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"prompt = PromptTemplate(\n",
|
||||
@@ -210,6 +208,7 @@
|
||||
"from IPython.display import Image, display\n",
|
||||
"from langgraph.graph import START, END, StateGraph\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class GraphState(TypedDict):\n",
|
||||
" \"\"\"\n",
|
||||
" Represents the state of our graph.\n",
|
||||
@@ -356,7 +355,7 @@
|
||||
"workflow.add_node(\"web_search\", web_search) # web search\n",
|
||||
"\n",
|
||||
"# Build graph\n",
|
||||
"workflow.set_entry_point(\"retrieve\")\n",
|
||||
"workflow.add_edge(START, retrieve)\n",
|
||||
"workflow.add_edge(\"retrieve\", \"grade_documents\")\n",
|
||||
"workflow.add_conditional_edges(\n",
|
||||
" \"grade_documents\",\n",
|
||||
@@ -381,21 +380,22 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import uuid \n",
|
||||
"import uuid\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def predict_custom_agent_answer(example: dict):\n",
|
||||
" \n",
|
||||
" config = {\"configurable\": {\"thread_id\": str(uuid.uuid4())}}\n",
|
||||
" \n",
|
||||
"\n",
|
||||
" state_dict = custom_graph.invoke(\n",
|
||||
" {\"question\": example[\"input\"], \"steps\": []}, config\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
"\n",
|
||||
" return {\"response\": state_dict[\"generation\"], \"steps\": state_dict[\"steps\"]}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"example = {\"input\": \"What are the types of agent memory?\"}\n",
|
||||
"#response = predict_custom_agent_answer(example)\n",
|
||||
"#response"
|
||||
"# response = predict_custom_agent_answer(example)\n",
|
||||
"# response"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -544,6 +544,7 @@
|
||||
" \"generate_answer\",\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def check_trajectory_custom(root_run: Run, example: Example) -> dict:\n",
|
||||
" \"\"\"\n",
|
||||
" Check if all expected tools are called in exact order and without any additional tool calls.\n",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -134,6 +134,7 @@
|
||||
" for d in web_results\n",
|
||||
" ]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Tool list\n",
|
||||
"tools = [retrieve_documents, web_search]"
|
||||
]
|
||||
@@ -152,9 +153,11 @@
|
||||
"from langgraph.graph.message import AnyMessage, add_messages\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class State(TypedDict):\n",
|
||||
" messages: Annotated[list[AnyMessage], add_messages]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Assistant:\n",
|
||||
" def __init__(self, runnable: Runnable):\n",
|
||||
" \"\"\"\n",
|
||||
@@ -291,6 +294,7 @@
|
||||
"source": [
|
||||
"import uuid\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def predict_react_agent_answer(example: dict):\n",
|
||||
" \"\"\"Use this for answer evaluation\"\"\"\n",
|
||||
"\n",
|
||||
|
||||
@@ -457,13 +457,13 @@
|
||||
"source": [
|
||||
"from langchain_core.runnables import RunnableLambda\n",
|
||||
"\n",
|
||||
"from langgraph.graph import END, StateGraph\n",
|
||||
"from langgraph.graph import END, START, StateGraph\n",
|
||||
"\n",
|
||||
"graph_builder = StateGraph(AgentState)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"graph_builder.add_node(\"agent\", agent)\n",
|
||||
"graph_builder.set_entry_point(\"agent\")\n",
|
||||
"graph_builder.add_edge(START, \"agent\")\n",
|
||||
"\n",
|
||||
"graph_builder.add_node(\"update_scratchpad\", update_scratchpad)\n",
|
||||
"graph_builder.add_edge(\"update_scratchpad\", \"agent\")\n",
|
||||
|
||||
@@ -38,11 +38,11 @@ lint_tests: PYTHON_FILES=tests
|
||||
lint_tests: MYPY_CACHE=.mypy_cache_test
|
||||
|
||||
lint lint_diff lint_package lint_tests:
|
||||
poetry run ruff .
|
||||
poetry run ruff check .
|
||||
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff
|
||||
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES)
|
||||
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES)
|
||||
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
|
||||
|
||||
format format_diff:
|
||||
poetry run ruff format $(PYTHON_FILES)
|
||||
poetry run ruff --select I --fix $(PYTHON_FILES)
|
||||
poetry run ruff check --select I --fix $(PYTHON_FILES)
|
||||
|
||||
@@ -2,11 +2,18 @@
|
||||
|
||||
Implementation of LangGraph CheckpointSaver that uses Postgres.
|
||||
|
||||
## Dependencies
|
||||
|
||||
By default `langgraph-checkpoint-postgres` installs `psycopg` (Psycopg 3) without any extras. However, you can choose a specific installation that best suits your needs [here](https://www.psycopg.org/psycopg3/docs/basic/install.html) (for example, `psycopg[binary]`).
|
||||
|
||||
## Usage
|
||||
|
||||
> [!IMPORTANT]
|
||||
> When using Postgres checkpointers for the first time, make sure to call `.setup()` method on them to create required tables. See example below.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> When manually creating Postgres connections and passing them to `PostgresSaver` or `AsyncPostgresSaver`, make sure to include `autocommit=True` and `row_factory=dict_row` (`from psycopg.rows import dict_row`). See a full example in this [how-to guide](https://langchain-ai.github.io/langgraph/how-tos/persistence_postgres/).
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.postgres import PostgresSaver
|
||||
|
||||
@@ -41,7 +48,6 @@ with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
|
||||
}
|
||||
},
|
||||
"pending_sends": [],
|
||||
"current_tasks": {}
|
||||
}
|
||||
|
||||
# store checkpoint
|
||||
@@ -84,7 +90,6 @@ async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
|
||||
}
|
||||
},
|
||||
"pending_sends": [],
|
||||
"current_tasks": {}
|
||||
}
|
||||
|
||||
# store checkpoint
|
||||
@@ -95,4 +100,4 @@ async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
|
||||
|
||||
# list checkpoints
|
||||
[c async for c in checkpointer.alist(read_config)]
|
||||
```
|
||||
```
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Iterator, List, Optional
|
||||
from typing import Any, Iterator, List, Optional, Union
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from psycopg import Connection, Cursor, Pipeline
|
||||
from psycopg.errors import UndefinedTable
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
from psycopg_pool import ConnectionPool
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
ChannelVersions,
|
||||
@@ -21,16 +22,32 @@ from langgraph.checkpoint.postgres.base import (
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _get_connection(conn: Union[Connection, ConnectionPool]) -> Iterator[Connection]:
|
||||
if isinstance(conn, Connection):
|
||||
yield conn
|
||||
elif isinstance(conn, ConnectionPool):
|
||||
with conn.connection() as conn:
|
||||
yield conn
|
||||
else:
|
||||
raise TypeError(f"Invalid connection type: {type(conn)}")
|
||||
|
||||
|
||||
class PostgresSaver(BasePostgresSaver):
|
||||
lock: threading.Lock
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
conn: Connection,
|
||||
conn: Union[Connection, ConnectionPool],
|
||||
pipe: Optional[Pipeline] = None,
|
||||
serde: Optional[SerializerProtocol] = None,
|
||||
) -> None:
|
||||
super().__init__(serde=serde)
|
||||
if isinstance(conn, ConnectionPool) and pipe is not None:
|
||||
raise ValueError(
|
||||
"Pipeline should be used only with a single Connection, not ConnectionPool."
|
||||
)
|
||||
|
||||
self.conn = conn
|
||||
self.pipe = pipe
|
||||
self.lock = threading.Lock()
|
||||
@@ -65,22 +82,21 @@ class PostgresSaver(BasePostgresSaver):
|
||||
already exist and runs database migrations. It MUST be called directly by the user
|
||||
the first time checkpointer is used.
|
||||
"""
|
||||
with self.lock:
|
||||
with self.conn.cursor(binary=True) as cur:
|
||||
try:
|
||||
version = cur.execute(
|
||||
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
|
||||
).fetchone()["v"]
|
||||
except UndefinedTable:
|
||||
version = -1
|
||||
for v, migration in zip(
|
||||
range(version + 1, len(self.MIGRATIONS)),
|
||||
self.MIGRATIONS[version + 1 :],
|
||||
):
|
||||
cur.execute(migration)
|
||||
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
|
||||
if self.pipe:
|
||||
self.pipe.sync()
|
||||
with self._cursor() as cur:
|
||||
try:
|
||||
version = cur.execute(
|
||||
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
|
||||
).fetchone()["v"]
|
||||
except UndefinedTable:
|
||||
version = -1
|
||||
for v, migration in zip(
|
||||
range(version + 1, len(self.MIGRATIONS)),
|
||||
self.MIGRATIONS[version + 1 :],
|
||||
):
|
||||
cur.execute(migration)
|
||||
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
|
||||
if self.pipe:
|
||||
self.pipe.sync()
|
||||
|
||||
def list(
|
||||
self,
|
||||
@@ -127,30 +143,34 @@ class PostgresSaver(BasePostgresSaver):
|
||||
if limit:
|
||||
query += f" LIMIT {limit}"
|
||||
# if we change this to use .stream() we need to make sure to close the cursor
|
||||
for value in self.conn.execute(query, args, binary=True):
|
||||
yield CheckpointTuple(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": value["thread_id"],
|
||||
"checkpoint_ns": value["checkpoint_ns"],
|
||||
"checkpoint_id": value["checkpoint_id"],
|
||||
with self._cursor() as cur:
|
||||
cur.execute(query, args, binary=True)
|
||||
for value in cur:
|
||||
yield CheckpointTuple(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": value["thread_id"],
|
||||
"checkpoint_ns": value["checkpoint_ns"],
|
||||
"checkpoint_id": value["checkpoint_id"],
|
||||
}
|
||||
},
|
||||
self._load_checkpoint(
|
||||
value["checkpoint"],
|
||||
value["channel_values"],
|
||||
value["pending_sends"],
|
||||
),
|
||||
self._load_metadata(value["metadata"]),
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": value["thread_id"],
|
||||
"checkpoint_ns": value["checkpoint_ns"],
|
||||
"checkpoint_id": value["parent_checkpoint_id"],
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
**self._load_checkpoint(value["checkpoint"]),
|
||||
"channel_values": self._load_blobs(value["channel_values"]),
|
||||
},
|
||||
self._load_metadata(value["metadata"]),
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": value["thread_id"],
|
||||
"checkpoint_ns": value["checkpoint_ns"],
|
||||
"checkpoint_id": value["parent_checkpoint_id"],
|
||||
}
|
||||
}
|
||||
if value["parent_checkpoint_id"]
|
||||
else None,
|
||||
)
|
||||
if value["parent_checkpoint_id"]
|
||||
else None,
|
||||
self._load_writes(value["pending_writes"]),
|
||||
)
|
||||
|
||||
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||
"""Get a checkpoint tuple from the database.
|
||||
@@ -198,7 +218,7 @@ class PostgresSaver(BasePostgresSaver):
|
||||
where = "WHERE thread_id = %s AND checkpoint_ns = %s ORDER BY checkpoint_id DESC LIMIT 1"
|
||||
|
||||
with self._cursor() as cur:
|
||||
cur = self.conn.execute(
|
||||
cur.execute(
|
||||
self.SELECT_SQL + where,
|
||||
args,
|
||||
binary=True,
|
||||
@@ -213,10 +233,11 @@ class PostgresSaver(BasePostgresSaver):
|
||||
"checkpoint_id": value["checkpoint_id"],
|
||||
}
|
||||
},
|
||||
{
|
||||
**self._load_checkpoint(value["checkpoint"]),
|
||||
"channel_values": self._load_blobs(value["channel_values"]),
|
||||
},
|
||||
self._load_checkpoint(
|
||||
value["checkpoint"],
|
||||
value["channel_values"],
|
||||
value["pending_sends"],
|
||||
),
|
||||
self._load_metadata(value["metadata"]),
|
||||
{
|
||||
"configurable": {
|
||||
@@ -316,7 +337,7 @@ class PostgresSaver(BasePostgresSaver):
|
||||
writes (List[Tuple[str, Any]]): List of writes to store.
|
||||
task_id (str): Identifier for the task creating the writes.
|
||||
"""
|
||||
with self._cursor() as cur:
|
||||
with self._cursor(pipeline=True) as cur:
|
||||
cur.executemany(
|
||||
self.UPSERT_CHECKPOINT_WRITES_SQL,
|
||||
self._dump_writes(
|
||||
@@ -330,21 +351,24 @@ class PostgresSaver(BasePostgresSaver):
|
||||
|
||||
@contextmanager
|
||||
def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor]:
|
||||
if self.pipe:
|
||||
# a connection in pipeline mode can be used concurrently
|
||||
# in multiple threads/coroutines, but only one cursor can be
|
||||
# used at a time
|
||||
try:
|
||||
with self.conn.cursor(binary=True) as cur:
|
||||
with _get_connection(self.conn) as conn:
|
||||
if self.pipe:
|
||||
# a connection in pipeline mode can be used concurrently
|
||||
# in multiple threads/coroutines, but only one cursor can be
|
||||
# used at a time
|
||||
try:
|
||||
with conn.cursor(binary=True, row_factory=dict_row) as cur:
|
||||
yield cur
|
||||
finally:
|
||||
if pipeline:
|
||||
self.pipe.sync()
|
||||
elif pipeline:
|
||||
# a connection not in pipeline mode can only be used by one
|
||||
# thread/coroutine at a time, so we acquire a lock
|
||||
with self.lock, conn.pipeline(), conn.cursor(
|
||||
binary=True, row_factory=dict_row
|
||||
) as cur:
|
||||
yield cur
|
||||
else:
|
||||
with self.lock, conn.cursor(binary=True, row_factory=dict_row) as cur:
|
||||
yield cur
|
||||
finally:
|
||||
if pipeline:
|
||||
self.pipe.sync()
|
||||
elif pipeline:
|
||||
# a connection not in pipeline mode can only be used by one
|
||||
# thread/coroutine at a time, so we acquire a lock
|
||||
with self.lock, self.conn.pipeline(), self.conn.cursor(binary=True) as cur:
|
||||
yield cur
|
||||
else:
|
||||
with self.lock, self.conn.cursor(binary=True) as cur:
|
||||
yield cur
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any, AsyncIterator, Optional
|
||||
from typing import Any, AsyncIterator, Optional, Union
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline
|
||||
from psycopg.errors import UndefinedTable
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
from psycopg_pool import AsyncConnectionPool
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
ChannelVersions,
|
||||
@@ -19,16 +20,34 @@ from langgraph.checkpoint.postgres.base import BasePostgresSaver
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _get_connection(
|
||||
conn: Union[AsyncConnection, AsyncConnectionPool],
|
||||
) -> AsyncIterator[AsyncConnection]:
|
||||
if isinstance(conn, AsyncConnection):
|
||||
yield conn
|
||||
elif isinstance(conn, AsyncConnectionPool):
|
||||
async with conn.connection() as conn:
|
||||
yield conn
|
||||
else:
|
||||
raise TypeError(f"Invalid connection type: {type(conn)}")
|
||||
|
||||
|
||||
class AsyncPostgresSaver(BasePostgresSaver):
|
||||
lock: asyncio.Lock
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
conn: AsyncConnection,
|
||||
conn: Union[AsyncConnection, AsyncConnectionPool],
|
||||
pipe: Optional[AsyncPipeline] = None,
|
||||
serde: Optional[SerializerProtocol] = None,
|
||||
) -> None:
|
||||
super().__init__(serde=serde)
|
||||
if isinstance(conn, AsyncConnectionPool) and pipe is not None:
|
||||
raise ValueError(
|
||||
"Pipeline should be used only with a single AsyncConnection, not AsyncConnectionPool."
|
||||
)
|
||||
|
||||
self.conn = conn
|
||||
self.pipe = pipe
|
||||
self.lock = asyncio.Lock()
|
||||
@@ -45,7 +64,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
pipeline (bool): whether to use AsyncPipeline
|
||||
|
||||
Returns:
|
||||
PostgresSaver: A new PostgresSaver instance.
|
||||
AsyncPostgresSaver: A new AsyncPostgresSaver instance.
|
||||
"""
|
||||
async with await AsyncConnection.connect(
|
||||
conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
|
||||
@@ -63,26 +82,22 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
already exist and runs database migrations. It MUST be called directly by the user
|
||||
the first time checkpointer is used.
|
||||
"""
|
||||
async with self.lock:
|
||||
async with self.conn.cursor(binary=True) as cur:
|
||||
try:
|
||||
version = (
|
||||
await cur.execute(
|
||||
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
|
||||
)
|
||||
).fetchone()["v"]
|
||||
except UndefinedTable:
|
||||
version = -1
|
||||
for v, migration in zip(
|
||||
range(version + 1, len(self.MIGRATIONS)),
|
||||
self.MIGRATIONS[version + 1 :],
|
||||
):
|
||||
await cur.execute(migration)
|
||||
await cur.execute(
|
||||
f"INSERT INTO checkpoint_migrations (v) VALUES ({v})"
|
||||
)
|
||||
if self.pipe:
|
||||
await self.pipe.sync()
|
||||
async with self._cursor() as cur:
|
||||
try:
|
||||
results = await cur.execute(
|
||||
"SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
|
||||
)
|
||||
version = (await results.fetchone())["v"]
|
||||
except UndefinedTable:
|
||||
version = -1
|
||||
for v, migration in zip(
|
||||
range(version + 1, len(self.MIGRATIONS)),
|
||||
self.MIGRATIONS[version + 1 :],
|
||||
):
|
||||
await cur.execute(migration)
|
||||
await cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
|
||||
if self.pipe:
|
||||
await self.pipe.sync()
|
||||
|
||||
async def alist(
|
||||
self,
|
||||
@@ -111,32 +126,35 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
if limit:
|
||||
query += f" LIMIT {limit}"
|
||||
# if we change this to use .stream() we need to make sure to close the cursor
|
||||
async for value in await self.conn.execute(query, args, binary=True):
|
||||
yield CheckpointTuple(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": value["thread_id"],
|
||||
"checkpoint_ns": value["checkpoint_ns"],
|
||||
"checkpoint_id": value["checkpoint_id"],
|
||||
}
|
||||
},
|
||||
{
|
||||
**self._load_checkpoint(value["checkpoint"]),
|
||||
"channel_values": await asyncio.to_thread(
|
||||
self._load_blobs, value["channel_values"]
|
||||
async with self._cursor() as cur:
|
||||
await cur.execute(query, args, binary=True)
|
||||
async for value in cur:
|
||||
yield CheckpointTuple(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": value["thread_id"],
|
||||
"checkpoint_ns": value["checkpoint_ns"],
|
||||
"checkpoint_id": value["checkpoint_id"],
|
||||
}
|
||||
},
|
||||
await asyncio.to_thread(
|
||||
self._load_checkpoint,
|
||||
value["checkpoint"],
|
||||
value["channel_values"],
|
||||
value["pending_sends"],
|
||||
),
|
||||
},
|
||||
self._load_metadata(value["metadata"]),
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": value["thread_id"],
|
||||
"checkpoint_ns": value["checkpoint_ns"],
|
||||
"checkpoint_id": value["parent_checkpoint_id"],
|
||||
self._load_metadata(value["metadata"]),
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": value["thread_id"],
|
||||
"checkpoint_ns": value["checkpoint_ns"],
|
||||
"checkpoint_id": value["parent_checkpoint_id"],
|
||||
}
|
||||
}
|
||||
}
|
||||
if value["parent_checkpoint_id"]
|
||||
else None,
|
||||
)
|
||||
if value["parent_checkpoint_id"]
|
||||
else None,
|
||||
await asyncio.to_thread(self._load_writes, value["pending_writes"]),
|
||||
)
|
||||
|
||||
async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||
"""Get a checkpoint tuple from the database asynchronously.
|
||||
@@ -163,7 +181,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
where = "WHERE thread_id = %s AND checkpoint_ns = %s ORDER BY checkpoint_id DESC LIMIT 1"
|
||||
|
||||
async with self._cursor() as cur:
|
||||
cur = await self.conn.execute(
|
||||
await cur.execute(
|
||||
self.SELECT_SQL + where,
|
||||
args,
|
||||
binary=True,
|
||||
@@ -178,12 +196,12 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
"checkpoint_id": value["checkpoint_id"],
|
||||
}
|
||||
},
|
||||
{
|
||||
**self._load_checkpoint(value["checkpoint"]),
|
||||
"channel_values": await asyncio.to_thread(
|
||||
self._load_blobs, value["channel_values"]
|
||||
),
|
||||
},
|
||||
await asyncio.to_thread(
|
||||
self._load_checkpoint,
|
||||
value["checkpoint"],
|
||||
value["channel_values"],
|
||||
value["pending_sends"],
|
||||
),
|
||||
self._load_metadata(value["metadata"]),
|
||||
{
|
||||
"configurable": {
|
||||
@@ -273,7 +291,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
|
||||
task_id (str): Identifier for the task creating the writes.
|
||||
"""
|
||||
async with self._cursor() as cur:
|
||||
async with self._cursor(pipeline=True) as cur:
|
||||
await cur.executemany(
|
||||
self.UPSERT_CHECKPOINT_WRITES_SQL,
|
||||
await asyncio.to_thread(
|
||||
@@ -288,23 +306,26 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
|
||||
@asynccontextmanager
|
||||
async def _cursor(self, *, pipeline: bool = False) -> AsyncIterator[AsyncCursor]:
|
||||
if self.pipe:
|
||||
# a connection in pipeline mode can be used concurrently
|
||||
# in multiple threads/coroutines, but only one cursor can be
|
||||
# used at a time
|
||||
try:
|
||||
async with self.conn.cursor(binary=True) as cur:
|
||||
async with _get_connection(self.conn) as conn:
|
||||
if self.pipe:
|
||||
# a connection in pipeline mode can be used concurrently
|
||||
# in multiple threads/coroutines, but only one cursor can be
|
||||
# used at a time
|
||||
try:
|
||||
async with conn.cursor(binary=True, row_factory=dict_row) as cur:
|
||||
yield cur
|
||||
finally:
|
||||
if pipeline:
|
||||
await self.pipe.sync()
|
||||
elif pipeline:
|
||||
# a connection not in pipeline mode can only be used by one
|
||||
# thread/coroutine at a time, so we acquire a lock
|
||||
async with self.lock, conn.pipeline(), conn.cursor(
|
||||
binary=True, row_factory=dict_row
|
||||
) as cur:
|
||||
yield cur
|
||||
else:
|
||||
async with self.lock, conn.cursor(
|
||||
binary=True, row_factory=dict_row
|
||||
) as cur:
|
||||
yield cur
|
||||
finally:
|
||||
if pipeline:
|
||||
await self.pipe.sync()
|
||||
elif pipeline:
|
||||
# a connection not in pipeline mode can only be used by one
|
||||
# thread/coroutine at a time, so we acquire a lock
|
||||
async with self.lock, self.conn.pipeline(), self.conn.cursor(
|
||||
binary=True
|
||||
) as cur:
|
||||
yield cur
|
||||
else:
|
||||
async with self.lock, self.conn.cursor(binary=True) as cur:
|
||||
yield cur
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from base64 import b64decode, b64encode
|
||||
from hashlib import md5
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
@@ -6,13 +5,14 @@ from langchain_core.runnables import RunnableConfig
|
||||
from psycopg.types.json import Jsonb
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
BaseCheckpointSaver,
|
||||
Checkpoint,
|
||||
EmptyChannelError,
|
||||
get_checkpoint_id,
|
||||
)
|
||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||
from langgraph.checkpoint.serde.types import ChannelProtocol
|
||||
from langgraph.checkpoint.serde.types import TASKS, ChannelProtocol
|
||||
|
||||
MetadataInput = Optional[dict[str, Any]]
|
||||
|
||||
@@ -57,7 +57,7 @@ MIGRATIONS = [
|
||||
"ALTER TABLE checkpoint_blobs ALTER COLUMN blob DROP not null;",
|
||||
]
|
||||
|
||||
SELECT_SQL = """
|
||||
SELECT_SQL = f"""
|
||||
select
|
||||
thread_id,
|
||||
checkpoint,
|
||||
@@ -81,7 +81,15 @@ select
|
||||
where cw.thread_id = checkpoints.thread_id
|
||||
and cw.checkpoint_ns = checkpoints.checkpoint_ns
|
||||
and cw.checkpoint_id = checkpoints.checkpoint_id
|
||||
) as pending_writes
|
||||
) as pending_writes,
|
||||
(
|
||||
select array_agg(array[cw.type::bytea, cw.blob])
|
||||
from checkpoint_writes cw
|
||||
where cw.thread_id = checkpoints.thread_id
|
||||
and cw.checkpoint_ns = checkpoints.checkpoint_ns
|
||||
and cw.checkpoint_id = checkpoints.parent_checkpoint_id
|
||||
and cw.channel = '{TASKS}'
|
||||
) as pending_sends
|
||||
from checkpoints """
|
||||
|
||||
UPSERT_CHECKPOINT_BLOBS_SQL = """
|
||||
@@ -112,27 +120,26 @@ class BasePostgresSaver(BaseCheckpointSaver):
|
||||
UPSERT_CHECKPOINT_BLOBS_SQL = UPSERT_CHECKPOINT_BLOBS_SQL
|
||||
UPSERT_CHECKPOINTS_SQL = UPSERT_CHECKPOINTS_SQL
|
||||
UPSERT_CHECKPOINT_WRITES_SQL = UPSERT_CHECKPOINT_WRITES_SQL
|
||||
|
||||
jsonplus_serde = JsonPlusSerializer()
|
||||
|
||||
def _load_checkpoint(self, checkpoint: dict[str, Any]) -> Checkpoint:
|
||||
if len(checkpoint["pending_sends"]) == 2 and all(
|
||||
isinstance(a, str) for a in checkpoint["pending_sends"]
|
||||
):
|
||||
type, bs = checkpoint["pending_sends"]
|
||||
return {
|
||||
**checkpoint,
|
||||
"pending_sends": self.serde.loads_typed((type, b64decode(bs))),
|
||||
}
|
||||
|
||||
return checkpoint
|
||||
|
||||
def _dump_checkpoint(self, checkpoint: Checkpoint) -> dict[str, Any]:
|
||||
type, bs = self.serde.dumps_typed(checkpoint["pending_sends"])
|
||||
def _load_checkpoint(
|
||||
self,
|
||||
checkpoint: dict[str, Any],
|
||||
channel_values: list[tuple[bytes, bytes, bytes]],
|
||||
pending_sends: list[tuple[bytes, bytes]],
|
||||
) -> Checkpoint:
|
||||
return {
|
||||
**checkpoint,
|
||||
"pending_sends": (type, b64encode(bs).decode()),
|
||||
"pending_sends": [
|
||||
self.serde.loads_typed((c.decode(), b)) for c, b in pending_sends or []
|
||||
],
|
||||
"channel_values": self._load_blobs(channel_values),
|
||||
}
|
||||
|
||||
def _dump_checkpoint(self, checkpoint: Checkpoint) -> dict[str, Any]:
|
||||
return {**checkpoint, "pending_sends": []}
|
||||
|
||||
def _load_blobs(
|
||||
self, blob_values: list[tuple[bytes, bytes, bytes]]
|
||||
) -> dict[str, Any]:
|
||||
@@ -199,7 +206,7 @@ class BasePostgresSaver(BaseCheckpointSaver):
|
||||
checkpoint_ns,
|
||||
checkpoint_id,
|
||||
task_id,
|
||||
idx,
|
||||
WRITES_IDX_MAP.get(channel, idx),
|
||||
channel,
|
||||
*self.serde.dumps_typed(value),
|
||||
)
|
||||
@@ -253,9 +260,13 @@ class BasePostgresSaver(BaseCheckpointSaver):
|
||||
if config:
|
||||
wheres.append("thread_id = %s ")
|
||||
param_values.append(config["configurable"]["thread_id"])
|
||||
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
||||
wheres.append("checkpoint_ns = %s")
|
||||
param_values.append(checkpoint_ns)
|
||||
if checkpoint_ns := config["configurable"].get("checkpoint_ns"):
|
||||
wheres.append("checkpoint_ns = %s")
|
||||
param_values.append(checkpoint_ns)
|
||||
|
||||
if checkpoint_id := get_checkpoint_id(config):
|
||||
wheres.append("checkpoint_id = %s ")
|
||||
param_values.append(checkpoint_id)
|
||||
|
||||
# construct predicate for metadata filter
|
||||
if filter:
|
||||
|
||||
Generated
+24
-21
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
@@ -266,7 +266,7 @@ tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "1.0.1"
|
||||
version = "1.0.6"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -394,6 +394,8 @@ files = [
|
||||
{file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"},
|
||||
{file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"},
|
||||
{file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"},
|
||||
{file = "orjson-3.10.6-cp313-none-win32.whl", hash = "sha256:efdf2c5cde290ae6b83095f03119bdc00303d7a03b42b16c54517baa3c4ca3d0"},
|
||||
{file = "orjson-3.10.6-cp313-none-win_amd64.whl", hash = "sha256:8e190fe7888e2e4392f52cafb9626113ba135ef53aacc65cd13109eb9746c43e"},
|
||||
{file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"},
|
||||
{file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"},
|
||||
{file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"},
|
||||
@@ -822,28 +824,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.1.15"
|
||||
version = "0.6.2"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"},
|
||||
{file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"},
|
||||
{file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"},
|
||||
{file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"},
|
||||
{file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"},
|
||||
{file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"},
|
||||
{file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"},
|
||||
{file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"},
|
||||
{file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"},
|
||||
{file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"},
|
||||
{file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"},
|
||||
{file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"},
|
||||
{file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"},
|
||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"},
|
||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"},
|
||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"},
|
||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"},
|
||||
{file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"},
|
||||
{file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"},
|
||||
{file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"},
|
||||
{file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -969,4 +972,4 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
content-hash = "422b6d716b86db072ea3a612287ad20ff5700c18f22d9e9d59cc4e198514519d"
|
||||
content-hash = "b139531e8c6f4e24cea4bdfc29d111d1ea000a6a8a604ba81be4bff0977a2466"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "1.0.2"
|
||||
version = "1.0.4"
|
||||
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -12,10 +12,11 @@ packages = [{ include = "langgraph" }]
|
||||
python = "^3.9.0,<4.0"
|
||||
langgraph-checkpoint = "^1.0.1"
|
||||
orjson = ">=3.10.1"
|
||||
psycopg = {extras = ["binary"], version = ">=3.1.19"}
|
||||
psycopg = "^3.0.0"
|
||||
psycopg-pool = "^3.0.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.1.4"
|
||||
ruff = "^0.6.2"
|
||||
codespell = "^2.2.0"
|
||||
pytest = "^7.2.1"
|
||||
anyio = "^4.4.0"
|
||||
@@ -23,7 +24,7 @@ pytest-asyncio = "^0.21.1"
|
||||
pytest-mock = "^3.11.1"
|
||||
pytest-watch = "^4.2.0"
|
||||
mypy = "^1.10.0"
|
||||
psycopg-pool = "^3.2.2"
|
||||
psycopg = {extras = ["binary"], version = ">=3.0.0"}
|
||||
langgraph-checkpoint = {path = "../checkpoint", develop = true}
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
|
||||
@@ -87,29 +87,15 @@ class TestAsyncPostgresSaver:
|
||||
search_results_4 = [c async for c in saver.alist(None, filter=query_4)]
|
||||
assert len(search_results_4) == 0
|
||||
|
||||
# search by config (defaults to root graph checkpoints)
|
||||
# search by config (defaults to checkpoints across all namespaces)
|
||||
search_results_5 = [
|
||||
c
|
||||
async for c in saver.alist({"configurable": {"thread_id": "thread-2"}})
|
||||
]
|
||||
assert len(search_results_5) == 1
|
||||
assert search_results_5[0].config["configurable"]["checkpoint_ns"] == ""
|
||||
|
||||
# search by config and checkpoint_ns
|
||||
search_results_6 = [
|
||||
c
|
||||
async for c in saver.alist(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": "thread-2",
|
||||
"checkpoint_ns": "inner",
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
assert len(search_results_6) == 1
|
||||
assert (
|
||||
search_results_6[0].config["configurable"]["checkpoint_ns"] == "inner"
|
||||
)
|
||||
assert len(search_results_5) == 2
|
||||
assert {
|
||||
search_results_5[0].config["configurable"]["checkpoint_ns"],
|
||||
search_results_5[1].config["configurable"]["checkpoint_ns"],
|
||||
} == {"", "inner"}
|
||||
|
||||
# TODO: test before and limit params
|
||||
|
||||
@@ -88,27 +88,14 @@ class TestPostgresSaver:
|
||||
search_results_4 = list(saver.list(None, filter=query_4))
|
||||
assert len(search_results_4) == 0
|
||||
|
||||
# search by config (defaults to root graph checkpoints)
|
||||
# search by config (defaults to checkpoints across all namespaces)
|
||||
search_results_5 = list(
|
||||
saver.list({"configurable": {"thread_id": "thread-2"}})
|
||||
)
|
||||
assert len(search_results_5) == 1
|
||||
assert search_results_5[0].config["configurable"]["checkpoint_ns"] == ""
|
||||
|
||||
# search by config and checkpoint_ns
|
||||
search_results_6 = list(
|
||||
saver.list(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": "thread-2",
|
||||
"checkpoint_ns": "inner",
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
assert len(search_results_6) == 1
|
||||
assert (
|
||||
search_results_6[0].config["configurable"]["checkpoint_ns"] == "inner"
|
||||
)
|
||||
assert len(search_results_5) == 2
|
||||
assert {
|
||||
search_results_5[0].config["configurable"]["checkpoint_ns"],
|
||||
search_results_5[1].config["configurable"]["checkpoint_ns"],
|
||||
} == {"", "inner"}
|
||||
|
||||
# TODO: test before and limit params
|
||||
|
||||
@@ -24,11 +24,11 @@ lint_tests: PYTHON_FILES=tests
|
||||
lint_tests: MYPY_CACHE=.mypy_cache_test
|
||||
|
||||
lint lint_diff lint_package lint_tests:
|
||||
poetry run ruff .
|
||||
poetry run ruff check .
|
||||
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff
|
||||
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES)
|
||||
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES)
|
||||
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
|
||||
|
||||
format format_diff:
|
||||
poetry run ruff format $(PYTHON_FILES)
|
||||
poetry run ruff --select I --fix $(PYTHON_FILES)
|
||||
poetry run ruff check --select I --fix $(PYTHON_FILES)
|
||||
|
||||
@@ -35,7 +35,6 @@ with SqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
}
|
||||
},
|
||||
"pending_sends": [],
|
||||
"current_tasks": {}
|
||||
}
|
||||
|
||||
# store checkpoint
|
||||
@@ -78,7 +77,6 @@ async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
}
|
||||
},
|
||||
"pending_sends": [],
|
||||
"current_tasks": {}
|
||||
}
|
||||
|
||||
# store checkpoint
|
||||
@@ -89,4 +87,4 @@ async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
|
||||
# list checkpoints
|
||||
[c async for c in checkpointer.alist(read_config)]
|
||||
```
|
||||
```
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import sqlite3
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
from contextlib import closing, contextmanager
|
||||
from hashlib import md5
|
||||
from typing import Any, AsyncIterator, Dict, Iterator, Optional, Sequence, Tuple
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
BaseCheckpointSaver,
|
||||
ChannelVersions,
|
||||
Checkpoint,
|
||||
@@ -318,7 +319,7 @@ class SqliteSaver(BaseCheckpointSaver):
|
||||
ORDER BY checkpoint_id DESC"""
|
||||
if limit:
|
||||
query += f" LIMIT {limit}"
|
||||
with self.cursor(transaction=False) as cur:
|
||||
with self.cursor(transaction=False) as cur, closing(self.conn.cursor()) as wcur:
|
||||
cur.execute(query, param_values)
|
||||
for (
|
||||
thread_id,
|
||||
@@ -329,6 +330,10 @@ class SqliteSaver(BaseCheckpointSaver):
|
||||
checkpoint,
|
||||
metadata,
|
||||
) in cur:
|
||||
wcur.execute(
|
||||
"SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?",
|
||||
(thread_id, checkpoint_ns, checkpoint_id),
|
||||
)
|
||||
yield CheckpointTuple(
|
||||
{
|
||||
"configurable": {
|
||||
@@ -350,6 +355,10 @@ class SqliteSaver(BaseCheckpointSaver):
|
||||
if parent_checkpoint_id
|
||||
else None
|
||||
),
|
||||
[
|
||||
(task_id, channel, self.serde.loads_typed((type, value)))
|
||||
for task_id, channel, type, value in wcur
|
||||
],
|
||||
)
|
||||
|
||||
def put(
|
||||
@@ -425,14 +434,14 @@ class SqliteSaver(BaseCheckpointSaver):
|
||||
"""
|
||||
with self.lock, self.cursor() as cur:
|
||||
cur.executemany(
|
||||
"INSERT OR REPLACE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[
|
||||
(
|
||||
str(config["configurable"]["thread_id"]),
|
||||
str(config["configurable"]["checkpoint_ns"]),
|
||||
str(config["configurable"]["checkpoint_id"]),
|
||||
task_id,
|
||||
idx,
|
||||
WRITES_IDX_MAP.get(channel, idx),
|
||||
channel,
|
||||
*self.serde.dumps_typed(value),
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@ import aiosqlite
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
BaseCheckpointSaver,
|
||||
ChannelVersions,
|
||||
Checkpoint,
|
||||
@@ -329,14 +330,14 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
AsyncIterator[CheckpointTuple]: An asynchronous iterator of matching checkpoint tuples.
|
||||
"""
|
||||
await self.setup()
|
||||
where, param_values = search_where(config, filter, before)
|
||||
where, params = search_where(config, filter, before)
|
||||
query = f"""SELECT thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata
|
||||
FROM checkpoints
|
||||
{where}
|
||||
ORDER BY checkpoint_id DESC"""
|
||||
if limit:
|
||||
query += f" LIMIT {limit}"
|
||||
async with self.conn.execute(query, param_values) as cursor:
|
||||
async with self.conn.execute(query, params) as cur, self.conn.cursor() as wcur:
|
||||
async for (
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
@@ -345,7 +346,11 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
type,
|
||||
checkpoint,
|
||||
metadata,
|
||||
) in cursor:
|
||||
) in cur:
|
||||
await wcur.execute(
|
||||
"SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?",
|
||||
(thread_id, checkpoint_ns, checkpoint_id),
|
||||
)
|
||||
yield CheckpointTuple(
|
||||
{
|
||||
"configurable": {
|
||||
@@ -367,6 +372,10 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
if parent_checkpoint_id
|
||||
else None
|
||||
),
|
||||
[
|
||||
(task_id, channel, self.serde.loads_typed((type, value)))
|
||||
async for task_id, channel, type, value in wcur
|
||||
],
|
||||
)
|
||||
|
||||
async def aput(
|
||||
@@ -432,19 +441,19 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
task_id (str): Identifier for the task creating the writes.
|
||||
"""
|
||||
await self.setup()
|
||||
async with self.conn.executemany(
|
||||
"INSERT OR REPLACE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[
|
||||
(
|
||||
str(config["configurable"]["thread_id"]),
|
||||
str(config["configurable"]["checkpoint_ns"]),
|
||||
str(config["configurable"]["checkpoint_id"]),
|
||||
task_id,
|
||||
idx,
|
||||
channel,
|
||||
*self.serde.dumps_typed(value),
|
||||
)
|
||||
for idx, (channel, value) in enumerate(writes)
|
||||
],
|
||||
):
|
||||
await self.conn.commit()
|
||||
async with self.conn.cursor() as cur:
|
||||
await cur.executemany(
|
||||
"INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[
|
||||
(
|
||||
str(config["configurable"]["thread_id"]),
|
||||
str(config["configurable"]["checkpoint_ns"]),
|
||||
str(config["configurable"]["checkpoint_id"]),
|
||||
task_id,
|
||||
WRITES_IDX_MAP.get(channel, idx),
|
||||
channel,
|
||||
*self.serde.dumps_typed(value),
|
||||
)
|
||||
for idx, (channel, value) in enumerate(writes)
|
||||
],
|
||||
)
|
||||
|
||||
@@ -70,9 +70,13 @@ def search_where(
|
||||
if config is not None:
|
||||
wheres.append("thread_id = ?")
|
||||
param_values.append(config["configurable"]["thread_id"])
|
||||
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
||||
wheres.append("checkpoint_ns = ?")
|
||||
param_values.append(checkpoint_ns)
|
||||
if checkpoint_ns := config["configurable"].get("checkpoint_ns"):
|
||||
wheres.append("checkpoint_ns = ?")
|
||||
param_values.append(checkpoint_ns)
|
||||
|
||||
if checkpoint_id := get_checkpoint_id(config):
|
||||
wheres.append("checkpoint_id = ?")
|
||||
param_values.append(checkpoint_id)
|
||||
|
||||
# construct predicate for metadata filter
|
||||
if filter:
|
||||
|
||||
Generated
+23
-20
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiosqlite"
|
||||
@@ -380,6 +380,8 @@ files = [
|
||||
{file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"},
|
||||
{file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"},
|
||||
{file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"},
|
||||
{file = "orjson-3.10.6-cp313-none-win32.whl", hash = "sha256:efdf2c5cde290ae6b83095f03119bdc00303d7a03b42b16c54517baa3c4ca3d0"},
|
||||
{file = "orjson-3.10.6-cp313-none-win_amd64.whl", hash = "sha256:8e190fe7888e2e4392f52cafb9626113ba135ef53aacc65cd13109eb9746c43e"},
|
||||
{file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"},
|
||||
{file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"},
|
||||
{file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"},
|
||||
@@ -707,28 +709,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.1.15"
|
||||
version = "0.6.2"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"},
|
||||
{file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"},
|
||||
{file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"},
|
||||
{file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"},
|
||||
{file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"},
|
||||
{file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"},
|
||||
{file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"},
|
||||
{file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"},
|
||||
{file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"},
|
||||
{file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"},
|
||||
{file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"},
|
||||
{file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"},
|
||||
{file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"},
|
||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"},
|
||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"},
|
||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"},
|
||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"},
|
||||
{file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"},
|
||||
{file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"},
|
||||
{file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"},
|
||||
{file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -832,4 +835,4 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.9.0"
|
||||
content-hash = "e073e1a73cdae1fae8ea46499c39e55980cb61c7c8bd6be774c64f80a627eb31"
|
||||
content-hash = "d50ec7c6b55075d19193e080cc95d3b0998b9efa1a0ed407bd6055b5b5e867e0"
|
||||
|
||||
@@ -14,7 +14,7 @@ langgraph-checkpoint = "^1.0.1"
|
||||
aiosqlite = "^0.20.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.1.4"
|
||||
ruff = "^0.6.2"
|
||||
codespell = "^2.2.0"
|
||||
pytest = "^7.2.1"
|
||||
pytest-asyncio = "^0.21.1"
|
||||
|
||||
@@ -84,29 +84,15 @@ class TestAsyncSqliteSaver:
|
||||
search_results_4 = [c async for c in saver.alist(None, filter=query_4)]
|
||||
assert len(search_results_4) == 0
|
||||
|
||||
# search by config (defaults to root graph checkpoints)
|
||||
# search by config (defaults to checkpoints across all namespaces)
|
||||
search_results_5 = [
|
||||
c
|
||||
async for c in saver.alist({"configurable": {"thread_id": "thread-2"}})
|
||||
]
|
||||
assert len(search_results_5) == 1
|
||||
assert search_results_5[0].config["configurable"]["checkpoint_ns"] == ""
|
||||
|
||||
# search by config and checkpoint_ns
|
||||
search_results_6 = [
|
||||
c
|
||||
async for c in saver.alist(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": "thread-2",
|
||||
"checkpoint_ns": "inner",
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
assert len(search_results_6) == 1
|
||||
assert (
|
||||
search_results_6[0].config["configurable"]["checkpoint_ns"] == "inner"
|
||||
)
|
||||
assert len(search_results_5) == 2
|
||||
assert {
|
||||
search_results_5[0].config["configurable"]["checkpoint_ns"],
|
||||
search_results_5[1].config["configurable"]["checkpoint_ns"],
|
||||
} == {"", "inner"}
|
||||
|
||||
# TODO: test before and limit params
|
||||
|
||||
@@ -87,28 +87,15 @@ class TestSqliteSaver:
|
||||
search_results_4 = list(saver.list(None, filter=query_4))
|
||||
assert len(search_results_4) == 0
|
||||
|
||||
# search by config (defaults to root graph checkpoints)
|
||||
# search by config (defaults to checkpoints across all namespaces)
|
||||
search_results_5 = list(
|
||||
saver.list({"configurable": {"thread_id": "thread-2"}})
|
||||
)
|
||||
assert len(search_results_5) == 1
|
||||
assert search_results_5[0].config["configurable"]["checkpoint_ns"] == ""
|
||||
|
||||
# search by config and checkpoint_ns
|
||||
search_results_6 = list(
|
||||
saver.list(
|
||||
{
|
||||
"configurable": {
|
||||
"thread_id": "thread-2",
|
||||
"checkpoint_ns": "inner",
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
assert len(search_results_6) == 1
|
||||
assert (
|
||||
search_results_6[0].config["configurable"]["checkpoint_ns"] == "inner"
|
||||
)
|
||||
assert len(search_results_5) == 2
|
||||
assert {
|
||||
search_results_5[0].config["configurable"]["checkpoint_ns"],
|
||||
search_results_5[1].config["configurable"]["checkpoint_ns"],
|
||||
} == {"", "inner"}
|
||||
|
||||
# TODO: test before and limit params
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ lint_tests: PYTHON_FILES=tests
|
||||
lint_tests: MYPY_CACHE=.mypy_cache_test
|
||||
|
||||
lint lint_diff lint_package lint_tests:
|
||||
poetry run ruff .
|
||||
poetry run ruff check .
|
||||
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff
|
||||
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES)
|
||||
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES)
|
||||
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
|
||||
|
||||
format format_diff:
|
||||
poetry run ruff format $(PYTHON_FILES)
|
||||
poetry run ruff --select I --fix $(PYTHON_FILES)
|
||||
poetry run ruff check --select I --fix $(PYTHON_FILES)
|
||||
|
||||
@@ -74,7 +74,6 @@ checkpoint = {
|
||||
}
|
||||
},
|
||||
"pending_sends": [],
|
||||
"current_tasks": {}
|
||||
}
|
||||
|
||||
# store checkpoint
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from abc import ABC
|
||||
from datetime import datetime, timezone
|
||||
from typing import (
|
||||
Any,
|
||||
@@ -22,6 +21,7 @@ from langgraph.checkpoint.base.id import uuid6
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol, maybe_add_typed_methods
|
||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||
from langgraph.checkpoint.serde.types import (
|
||||
ERROR,
|
||||
ChannelProtocol,
|
||||
SendProtocol,
|
||||
)
|
||||
@@ -96,8 +96,6 @@ class Checkpoint(TypedDict):
|
||||
pending_sends: List[SendProtocol]
|
||||
"""List of packets sent to nodes but not yet processed.
|
||||
Cleared by the next checkpoint."""
|
||||
current_tasks: Dict[str, TaskInfo]
|
||||
"""Map from task ID to task info."""
|
||||
|
||||
|
||||
def empty_checkpoint() -> Checkpoint:
|
||||
@@ -109,7 +107,6 @@ def empty_checkpoint() -> Checkpoint:
|
||||
channel_versions={},
|
||||
versions_seen={},
|
||||
pending_sends=[],
|
||||
current_tasks={},
|
||||
)
|
||||
|
||||
|
||||
@@ -122,7 +119,6 @@ def copy_checkpoint(checkpoint: Checkpoint) -> Checkpoint:
|
||||
channel_versions=checkpoint["channel_versions"].copy(),
|
||||
versions_seen={k: v.copy() for k, v in checkpoint["versions_seen"].items()},
|
||||
pending_sends=checkpoint.get("pending_sends", []).copy(),
|
||||
current_tasks=checkpoint.get("current_tasks", {}).copy(),
|
||||
)
|
||||
|
||||
|
||||
@@ -140,6 +136,8 @@ def create_checkpoint(
|
||||
else:
|
||||
values: dict[str, Any] = {}
|
||||
for k, v in channels.items():
|
||||
if k not in checkpoint["channel_versions"]:
|
||||
continue
|
||||
try:
|
||||
values[k] = v.checkpoint()
|
||||
except EmptyChannelError:
|
||||
@@ -152,7 +150,6 @@ def create_checkpoint(
|
||||
channel_versions=checkpoint["channel_versions"],
|
||||
versions_seen=checkpoint["versions_seen"],
|
||||
pending_sends=checkpoint.get("pending_sends", []),
|
||||
current_tasks={},
|
||||
)
|
||||
|
||||
|
||||
@@ -194,7 +191,7 @@ CheckpointId = ConfigurableFieldSpec(
|
||||
)
|
||||
|
||||
|
||||
class BaseCheckpointSaver(ABC):
|
||||
class BaseCheckpointSaver:
|
||||
"""Base class for creating a graph checkpointer.
|
||||
|
||||
Checkpointers allow LangGraph agents to persist their state
|
||||
@@ -437,3 +434,14 @@ def get_checkpoint_id(config: RunnableConfig) -> Optional[str]:
|
||||
return config["configurable"].get(
|
||||
"checkpoint_id", config["configurable"].get("thread_ts")
|
||||
)
|
||||
|
||||
|
||||
"""
|
||||
Mapping from error type to error index.
|
||||
Regular writes just map to their index in the list of writes being saved.
|
||||
Special writes (e.g. errors) map to negative indices, to avoid those writes from
|
||||
saving regular writes.
|
||||
Each Checkpointer implementation should use this mapping in put_writes.
|
||||
"""
|
||||
WRITES_IDX_MAP = {ERROR: -1}
|
||||
# TODO To store scheduled status of tasks, add a special channel here
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Any, AsyncIterator, Dict, Iterator, List, Optional, Tuple
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
WRITES_IDX_MAP,
|
||||
BaseCheckpointSaver,
|
||||
ChannelVersions,
|
||||
Checkpoint,
|
||||
@@ -16,6 +17,7 @@ from langgraph.checkpoint.base import (
|
||||
SerializerProtocol,
|
||||
get_checkpoint_id,
|
||||
)
|
||||
from langgraph.checkpoint.serde.types import TASKS
|
||||
|
||||
|
||||
class MemorySaver(
|
||||
@@ -52,6 +54,9 @@ class MemorySaver(
|
||||
|
||||
# thread ID -> checkpoint NS -> checkpoint ID -> checkpoint mapping
|
||||
storage: defaultdict[str, dict[str, dict[str, tuple[bytes, bytes, Optional[str]]]]]
|
||||
writes: defaultdict[
|
||||
tuple[str, str, str], dict[tuple[str, int], tuple[str, str, bytes]]
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -60,7 +65,7 @@ class MemorySaver(
|
||||
) -> None:
|
||||
super().__init__(serde=serde)
|
||||
self.storage = defaultdict(lambda: defaultdict(dict))
|
||||
self.writes = defaultdict(list)
|
||||
self.writes = defaultdict(dict)
|
||||
|
||||
def __enter__(self) -> "MemorySaver":
|
||||
return self
|
||||
@@ -103,10 +108,23 @@ class MemorySaver(
|
||||
if checkpoint_id := get_checkpoint_id(config):
|
||||
if saved := self.storage[thread_id][checkpoint_ns].get(checkpoint_id):
|
||||
checkpoint, metadata, parent_checkpoint_id = saved
|
||||
writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)]
|
||||
writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)].values()
|
||||
if parent_checkpoint_id:
|
||||
sends = [
|
||||
w[2]
|
||||
for w in self.writes[
|
||||
(thread_id, checkpoint_ns, parent_checkpoint_id)
|
||||
].values()
|
||||
if w[1] == TASKS
|
||||
]
|
||||
else:
|
||||
sends = []
|
||||
return CheckpointTuple(
|
||||
config=config,
|
||||
checkpoint=self.serde.loads_typed(checkpoint),
|
||||
checkpoint={
|
||||
**self.serde.loads_typed(checkpoint),
|
||||
"pending_sends": [self.serde.loads_typed(s) for s in sends],
|
||||
},
|
||||
metadata=self.serde.loads_typed(metadata),
|
||||
pending_writes=[
|
||||
(id, c, self.serde.loads_typed(v)) for id, c, v in writes
|
||||
@@ -125,7 +143,17 @@ class MemorySaver(
|
||||
if checkpoints := self.storage[thread_id][checkpoint_ns]:
|
||||
checkpoint_id = max(checkpoints.keys())
|
||||
checkpoint, metadata, parent_checkpoint_id = checkpoints[checkpoint_id]
|
||||
writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)]
|
||||
writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)].values()
|
||||
if parent_checkpoint_id:
|
||||
sends = [
|
||||
w[2]
|
||||
for w in self.writes[
|
||||
(thread_id, checkpoint_ns, parent_checkpoint_id)
|
||||
].values()
|
||||
if w[1] == TASKS
|
||||
]
|
||||
else:
|
||||
sends = []
|
||||
return CheckpointTuple(
|
||||
config={
|
||||
"configurable": {
|
||||
@@ -134,7 +162,10 @@ class MemorySaver(
|
||||
"checkpoint_id": checkpoint_id,
|
||||
}
|
||||
},
|
||||
checkpoint=self.serde.loads_typed(checkpoint),
|
||||
checkpoint={
|
||||
**self.serde.loads_typed(checkpoint),
|
||||
"pending_sends": [self.serde.loads_typed(s) for s in sends],
|
||||
},
|
||||
metadata=self.serde.loads_typed(metadata),
|
||||
pending_writes=[
|
||||
(id, c, self.serde.loads_typed(v)) for id, c, v in writes
|
||||
@@ -173,57 +204,91 @@ class MemorySaver(
|
||||
Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
|
||||
"""
|
||||
thread_ids = (config["configurable"]["thread_id"],) if config else self.storage
|
||||
checkpoint_ns = (
|
||||
config["configurable"].get("checkpoint_ns", "") if config else ""
|
||||
config_checkpoint_ns = (
|
||||
config["configurable"].get("checkpoint_ns") if config else None
|
||||
)
|
||||
config_checkpoint_id = get_checkpoint_id(config) if config else None
|
||||
for thread_id in thread_ids:
|
||||
for checkpoint_id, (checkpoint, metadata_b, parent_checkpoint_id) in sorted(
|
||||
self.storage[thread_id][checkpoint_ns].items(),
|
||||
key=lambda x: x[0],
|
||||
reverse=True,
|
||||
):
|
||||
# filter by checkpoint ID
|
||||
if (
|
||||
before
|
||||
and (before_checkpoint_id := get_checkpoint_id(before))
|
||||
and checkpoint_id >= before_checkpoint_id
|
||||
):
|
||||
for checkpoint_ns in self.storage[thread_id].keys():
|
||||
if config_checkpoint_ns and checkpoint_ns != config_checkpoint_ns:
|
||||
continue
|
||||
|
||||
# filter by metadata
|
||||
metadata = self.serde.loads_typed(metadata_b)
|
||||
if filter and not all(
|
||||
query_value == metadata[query_key]
|
||||
for query_key, query_value in filter.items()
|
||||
for checkpoint_id, (
|
||||
checkpoint,
|
||||
metadata_b,
|
||||
parent_checkpoint_id,
|
||||
) in sorted(
|
||||
self.storage[thread_id][checkpoint_ns].items(),
|
||||
key=lambda x: x[0],
|
||||
reverse=True,
|
||||
):
|
||||
continue
|
||||
# filter by checkpoint ID from config
|
||||
if config_checkpoint_id and checkpoint_id != config_checkpoint_id:
|
||||
continue
|
||||
|
||||
# limit search results
|
||||
if limit is not None and limit <= 0:
|
||||
break
|
||||
elif limit is not None:
|
||||
limit -= 1
|
||||
# filter by checkpoint ID from `before` config
|
||||
if (
|
||||
before
|
||||
and (before_checkpoint_id := get_checkpoint_id(before))
|
||||
and checkpoint_id >= before_checkpoint_id
|
||||
):
|
||||
continue
|
||||
|
||||
yield CheckpointTuple(
|
||||
config={
|
||||
"configurable": {
|
||||
"thread_id": thread_id,
|
||||
"checkpoint_ns": checkpoint_ns,
|
||||
"checkpoint_id": checkpoint_id,
|
||||
# filter by metadata
|
||||
metadata = self.serde.loads_typed(metadata_b)
|
||||
if filter and not all(
|
||||
query_value == metadata.get(query_key)
|
||||
for query_key, query_value in filter.items()
|
||||
):
|
||||
continue
|
||||
|
||||
# limit search results
|
||||
if limit is not None and limit <= 0:
|
||||
break
|
||||
elif limit is not None:
|
||||
limit -= 1
|
||||
|
||||
writes = self.writes[
|
||||
(thread_id, checkpoint_ns, checkpoint_id)
|
||||
].values()
|
||||
|
||||
if parent_checkpoint_id:
|
||||
sends = [
|
||||
w[2]
|
||||
for w in self.writes[
|
||||
(thread_id, checkpoint_ns, parent_checkpoint_id)
|
||||
].values()
|
||||
if w[1] == TASKS
|
||||
]
|
||||
else:
|
||||
sends = []
|
||||
|
||||
yield CheckpointTuple(
|
||||
config={
|
||||
"configurable": {
|
||||
"thread_id": thread_id,
|
||||
"checkpoint_ns": checkpoint_ns,
|
||||
"checkpoint_id": checkpoint_id,
|
||||
}
|
||||
},
|
||||
checkpoint={
|
||||
**self.serde.loads_typed(checkpoint),
|
||||
"pending_sends": [self.serde.loads_typed(s) for s in sends],
|
||||
},
|
||||
metadata=metadata,
|
||||
parent_config={
|
||||
"configurable": {
|
||||
"thread_id": thread_id,
|
||||
"checkpoint_ns": checkpoint_ns,
|
||||
"checkpoint_id": parent_checkpoint_id,
|
||||
}
|
||||
}
|
||||
},
|
||||
checkpoint=self.serde.loads_typed(checkpoint),
|
||||
metadata=metadata,
|
||||
parent_config={
|
||||
"configurable": {
|
||||
"thread_id": thread_id,
|
||||
"checkpoint_ns": checkpoint_ns,
|
||||
"checkpoint_id": parent_checkpoint_id,
|
||||
}
|
||||
}
|
||||
if parent_checkpoint_id
|
||||
else None,
|
||||
)
|
||||
if parent_checkpoint_id
|
||||
else None,
|
||||
pending_writes=[
|
||||
(id, c, self.serde.loads_typed(v)) for id, c, v in writes
|
||||
],
|
||||
)
|
||||
|
||||
def put(
|
||||
self,
|
||||
@@ -246,12 +311,14 @@ class MemorySaver(
|
||||
Returns:
|
||||
RunnableConfig: The updated config containing the saved checkpoint's timestamp.
|
||||
"""
|
||||
c = checkpoint.copy()
|
||||
c.pop("pending_sends")
|
||||
thread_id = config["configurable"]["thread_id"]
|
||||
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
||||
self.storage[thread_id][checkpoint_ns].update(
|
||||
{
|
||||
checkpoint["id"]: (
|
||||
self.serde.dumps_typed(checkpoint),
|
||||
self.serde.dumps_typed(c),
|
||||
self.serde.dumps_typed(metadata),
|
||||
config["configurable"].get("checkpoint_id"), # parent
|
||||
)
|
||||
@@ -287,10 +354,10 @@ class MemorySaver(
|
||||
thread_id = config["configurable"]["thread_id"]
|
||||
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
||||
checkpoint_id = config["configurable"]["checkpoint_id"]
|
||||
key = (thread_id, checkpoint_ns, checkpoint_id)
|
||||
self.writes[key].extend(
|
||||
[(task_id, c, self.serde.dumps_typed(v)) for c, v in writes]
|
||||
)
|
||||
outer_key = (thread_id, checkpoint_ns, checkpoint_id)
|
||||
for idx, (c, v) in enumerate(writes):
|
||||
inner_key = (task_id, WRITES_IDX_MAP.get(c, idx))
|
||||
self.writes[outer_key][inner_key] = (task_id, c, self.serde.dumps_typed(v))
|
||||
|
||||
async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||
"""Asynchronous version of get_tuple.
|
||||
@@ -329,7 +396,14 @@ class MemorySaver(
|
||||
"""
|
||||
loop = asyncio.get_running_loop()
|
||||
iter = await loop.run_in_executor(
|
||||
None, partial(self.list, before=before, limit=limit, filter=filter), config
|
||||
None,
|
||||
partial(
|
||||
self.list,
|
||||
before=before,
|
||||
limit=limit,
|
||||
filter=filter,
|
||||
),
|
||||
config,
|
||||
)
|
||||
while True:
|
||||
# handling StopIteration exception inside coroutine won't work
|
||||
|
||||
@@ -12,17 +12,13 @@ class SerializerProtocol(Protocol):
|
||||
Valid implementations include the `pickle`, `json` and `orjson` modules.
|
||||
"""
|
||||
|
||||
def dumps(self, obj: Any) -> bytes:
|
||||
...
|
||||
def dumps(self, obj: Any) -> bytes: ...
|
||||
|
||||
def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
|
||||
...
|
||||
def dumps_typed(self, obj: Any) -> tuple[str, bytes]: ...
|
||||
|
||||
def loads(self, data: bytes) -> Any:
|
||||
...
|
||||
def loads(self, data: bytes) -> Any: ...
|
||||
|
||||
def loads_typed(self, data: tuple[str, bytes]) -> Any:
|
||||
...
|
||||
def loads_typed(self, data: tuple[str, bytes]) -> Any: ...
|
||||
|
||||
|
||||
class SerializerCompat(SerializerProtocol):
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
import dataclasses
|
||||
import decimal
|
||||
import importlib
|
||||
import json
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import pathlib
|
||||
import re
|
||||
from collections import deque
|
||||
from datetime import date, datetime, time, timedelta, timezone
|
||||
from enum import Enum
|
||||
from inspect import isclass
|
||||
from ipaddress import (
|
||||
IPv4Address,
|
||||
IPv4Interface,
|
||||
IPv4Network,
|
||||
IPv6Address,
|
||||
IPv6Interface,
|
||||
IPv6Network,
|
||||
)
|
||||
from typing import Any, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from langchain_core.load.load import Reviver
|
||||
from langchain_core.load.serializable import Serializable
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from langgraph.checkpoint.serde.base import SerializerProtocol
|
||||
from langgraph.checkpoint.serde.types import SendProtocol
|
||||
@@ -37,23 +51,52 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
if isinstance(obj, Serializable):
|
||||
return obj.to_json()
|
||||
elif hasattr(obj, "model_dump") and callable(obj.model_dump):
|
||||
return self._encode_constructor_args(obj.__class__, kwargs=obj.model_dump())
|
||||
return self._encode_constructor_args(
|
||||
obj.__class__, method="model_construct", kwargs=obj.model_dump()
|
||||
)
|
||||
elif hasattr(obj, "dict") and callable(obj.dict):
|
||||
return self._encode_constructor_args(obj.__class__, kwargs=obj.dict())
|
||||
return self._encode_constructor_args(
|
||||
obj.__class__, method="construct", kwargs=obj.dict()
|
||||
)
|
||||
elif isinstance(obj, pathlib.Path):
|
||||
return self._encode_constructor_args(pathlib.Path, args=obj.parts)
|
||||
elif isinstance(obj, re.Pattern):
|
||||
return self._encode_constructor_args(
|
||||
re.compile, args=[obj.pattern, obj.flags]
|
||||
)
|
||||
elif isinstance(obj, UUID):
|
||||
return self._encode_constructor_args(UUID, args=[obj.hex])
|
||||
elif isinstance(obj, (set, frozenset)):
|
||||
elif isinstance(obj, decimal.Decimal):
|
||||
return self._encode_constructor_args(decimal.Decimal, args=[str(obj)])
|
||||
elif isinstance(obj, (set, frozenset, deque)):
|
||||
return self._encode_constructor_args(type(obj), args=[list(obj)])
|
||||
elif isinstance(obj, (IPv4Address, IPv4Interface, IPv4Network)):
|
||||
return self._encode_constructor_args(obj.__class__, args=[str(obj)])
|
||||
elif isinstance(obj, (IPv6Address, IPv6Interface, IPv6Network)):
|
||||
return self._encode_constructor_args(obj.__class__, args=[str(obj)])
|
||||
|
||||
elif isinstance(obj, datetime):
|
||||
return self._encode_constructor_args(
|
||||
datetime, method="fromisoformat", args=[obj.isoformat()]
|
||||
)
|
||||
elif isinstance(obj, timezone):
|
||||
return self._encode_constructor_args(timezone, args=obj.__getinitargs__())
|
||||
elif isinstance(obj, ZoneInfo):
|
||||
return self._encode_constructor_args(ZoneInfo, args=[obj.key])
|
||||
elif isinstance(obj, timedelta):
|
||||
return self._encode_constructor_args(
|
||||
timedelta, args=[obj.days, obj.seconds, obj.microseconds]
|
||||
)
|
||||
elif isinstance(obj, date):
|
||||
return self._encode_constructor_args(
|
||||
date, args=[obj.year, obj.month, obj.day]
|
||||
)
|
||||
elif isinstance(obj, time):
|
||||
return self._encode_constructor_args(
|
||||
time,
|
||||
args=[obj.hour, obj.minute, obj.second, obj.microsecond, obj.tzinfo],
|
||||
kwargs={"fold": obj.fold},
|
||||
)
|
||||
elif dataclasses.is_dataclass(obj):
|
||||
return self._encode_constructor_args(
|
||||
obj.__class__,
|
||||
@@ -66,12 +109,14 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
return self._encode_constructor_args(obj.__class__, args=[obj.value])
|
||||
elif isinstance(obj, SendProtocol):
|
||||
return self._encode_constructor_args(
|
||||
obj.__class__, kwargs={"node": obj.node, "arg": obj.arg}
|
||||
obj.__class__, kwargs={"node": obj.node, "arg": obj.arg, "id": obj.id}
|
||||
)
|
||||
elif isinstance(obj, (bytes, bytearray)):
|
||||
return self._encode_constructor_args(
|
||||
obj.__class__, method="fromhex", args=[obj.hex()]
|
||||
)
|
||||
elif isinstance(obj, BaseException):
|
||||
return repr(obj)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Object of type {obj.__class__.__name__} is not JSON serializable"
|
||||
@@ -83,18 +128,30 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
and value.get("type", None) == "constructor"
|
||||
and value.get("id", None) is not None
|
||||
):
|
||||
# Get module and class name
|
||||
[*module, name] = value["id"]
|
||||
# Import module
|
||||
mod = importlib.import_module(".".join(module))
|
||||
# Import class
|
||||
cls = getattr(mod, name)
|
||||
# Instantiate class
|
||||
if value["method"] is not None:
|
||||
method = getattr(cls, value["method"])
|
||||
return method(*value["args"], **value["kwargs"])
|
||||
else:
|
||||
return cls(*value["args"], **value["kwargs"])
|
||||
try:
|
||||
# Get module and class name
|
||||
[*module, name] = value["id"]
|
||||
# Import module
|
||||
mod = importlib.import_module(".".join(module))
|
||||
# Import class
|
||||
cls = getattr(mod, name)
|
||||
# Instantiate class
|
||||
if value["method"] is not None:
|
||||
method = getattr(cls, value["method"])
|
||||
else:
|
||||
method = cls
|
||||
if isclass(method) and issubclass(method, BaseException):
|
||||
return None
|
||||
if value["args"] and value["kwargs"]:
|
||||
return method(*value["args"], **value["kwargs"])
|
||||
elif value["args"]:
|
||||
return method(*value["args"])
|
||||
elif value["kwargs"]:
|
||||
return method(**value["kwargs"])
|
||||
else:
|
||||
return method()
|
||||
except (ImportError, AttributeError, TypeError):
|
||||
return None
|
||||
|
||||
return LC_REVIVER(value)
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ from typing import (
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from typing_extensions import Self
|
||||
|
||||
ERROR = "__error__"
|
||||
TASKS = "__pregel_tasks"
|
||||
|
||||
Value = TypeVar("Value")
|
||||
Update = TypeVar("Update")
|
||||
C = TypeVar("C")
|
||||
@@ -20,34 +23,26 @@ C = TypeVar("C")
|
||||
class ChannelProtocol(Protocol[Value, Update, C]):
|
||||
# Mirrors langgraph.channels.base.BaseChannel
|
||||
@property
|
||||
def ValueType(self) -> Any:
|
||||
...
|
||||
def ValueType(self) -> Any: ...
|
||||
|
||||
@property
|
||||
def UpdateType(self) -> Any:
|
||||
...
|
||||
def UpdateType(self) -> Any: ...
|
||||
|
||||
def checkpoint(self) -> Optional[C]:
|
||||
...
|
||||
def checkpoint(self) -> Optional[C]: ...
|
||||
|
||||
def from_checkpoint(
|
||||
self, checkpoint: Optional[C], config: RunnableConfig
|
||||
) -> Generator[Self, None, None]:
|
||||
...
|
||||
) -> Generator[Self, None, None]: ...
|
||||
|
||||
async def afrom_checkpoint(
|
||||
self, checkpoint: Optional[C], config: RunnableConfig
|
||||
) -> AsyncGenerator[Self, None]:
|
||||
...
|
||||
) -> AsyncGenerator[Self, None]: ...
|
||||
|
||||
def update(self, values: Sequence[Update]) -> bool:
|
||||
...
|
||||
def update(self, values: Sequence[Update]) -> bool: ...
|
||||
|
||||
def get(self) -> Value:
|
||||
...
|
||||
def get(self) -> Value: ...
|
||||
|
||||
def consume(self) -> bool:
|
||||
...
|
||||
def consume(self) -> bool: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
@@ -55,12 +50,10 @@ class SendProtocol(Protocol):
|
||||
# Mirrors langgraph.constants.Send
|
||||
node: str
|
||||
arg: Any
|
||||
id: str
|
||||
|
||||
def __hash__(self) -> int:
|
||||
...
|
||||
def __hash__(self) -> int: ...
|
||||
|
||||
def __repr__(self) -> str:
|
||||
...
|
||||
def __repr__(self) -> str: ...
|
||||
|
||||
def __eq__(self, value: object) -> bool:
|
||||
...
|
||||
def __eq__(self, value: object) -> bool: ...
|
||||
|
||||
Generated
+23
-20
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
@@ -380,6 +380,8 @@ files = [
|
||||
{file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"},
|
||||
{file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"},
|
||||
{file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"},
|
||||
{file = "orjson-3.10.6-cp313-none-win32.whl", hash = "sha256:efdf2c5cde290ae6b83095f03119bdc00303d7a03b42b16c54517baa3c4ca3d0"},
|
||||
{file = "orjson-3.10.6-cp313-none-win_amd64.whl", hash = "sha256:8e190fe7888e2e4392f52cafb9626113ba135ef53aacc65cd13109eb9746c43e"},
|
||||
{file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"},
|
||||
{file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"},
|
||||
{file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"},
|
||||
@@ -707,28 +709,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.1.15"
|
||||
version = "0.6.2"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"},
|
||||
{file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"},
|
||||
{file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"},
|
||||
{file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"},
|
||||
{file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"},
|
||||
{file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"},
|
||||
{file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"},
|
||||
{file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"},
|
||||
{file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"},
|
||||
{file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"},
|
||||
{file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"},
|
||||
{file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"},
|
||||
{file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"},
|
||||
{file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"},
|
||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"},
|
||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"},
|
||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"},
|
||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"},
|
||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"},
|
||||
{file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"},
|
||||
{file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"},
|
||||
{file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"},
|
||||
{file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -847,4 +850,4 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
content-hash = "3bee25f1adc1349de4358a88037693cec47f6a2b88b809490f9198e6313239f6"
|
||||
content-hash = "de79db3dc7701542739b3417e9d3f02c3b41167719603ee4d08b92e23b7443ee"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "1.0.2"
|
||||
version = "1.0.6"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -13,7 +13,7 @@ python = "^3.9.0,<4.0"
|
||||
langchain-core = ">=0.2.22,<0.3"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.1.4"
|
||||
ruff = "^0.6.2"
|
||||
codespell = "^2.2.0"
|
||||
pytest = "^7.2.1"
|
||||
pytest-asyncio = "^0.21.1"
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import dataclasses
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from collections import deque
|
||||
from datetime import date, datetime, time, timezone
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from ipaddress import IPv4Address
|
||||
|
||||
import dataclasses_json
|
||||
from langchain_core.pydantic_v1 import BaseModel as LcBaseModel
|
||||
from langchain_core.runnables import RunnableMap
|
||||
from pydantic import BaseModel
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||
|
||||
@@ -60,11 +66,24 @@ class Person:
|
||||
|
||||
def test_serde_jsonplus() -> None:
|
||||
uid = uuid.UUID(int=1)
|
||||
current_time = datetime(2024, 4, 19, 23, 4, 57, 51022, timezone.max)
|
||||
deque_instance = deque([1, 2, 3])
|
||||
tzn = ZoneInfo("America/New_York")
|
||||
ip4 = IPv4Address("192.168.0.1")
|
||||
current_date = date(2024, 4, 19)
|
||||
current_time = time(23, 4, 57, 51022, timezone.max)
|
||||
current_timestamp = datetime(2024, 4, 19, 23, 4, 57, 51022, timezone.max)
|
||||
|
||||
to_serialize = {
|
||||
"uid": uid,
|
||||
"path": pathlib.Path("foo", "bar"),
|
||||
"re": re.compile(r"foo", re.DOTALL),
|
||||
"decimal": Decimal("1.10101"),
|
||||
"ip4": ip4,
|
||||
"deque": deque_instance,
|
||||
"tzn": tzn,
|
||||
"date": current_date,
|
||||
"time": current_time,
|
||||
"uid": uid,
|
||||
"timestamp": current_timestamp,
|
||||
"my_slotted_class": MyDataclassWSlots("bar", 2),
|
||||
"my_dataclass": MyDataclass("foo", 1),
|
||||
"my_enum": MyEnum.FOO,
|
||||
@@ -103,7 +122,7 @@ def test_serde_jsonplus() -> None:
|
||||
|
||||
assert dumped == (
|
||||
"json",
|
||||
b"""{"uid": {"lc": 2, "type": "constructor", "id": ["uuid", "UUID"], "method": null, "args": ["00000000000000000000000000000001"], "kwargs": {}}, "time": {"lc": 2, "type": "constructor", "id": ["datetime", "datetime"], "method": "fromisoformat", "args": ["2024-04-19T23:04:57.051022+23:59"], "kwargs": {}}, "my_slotted_class": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclassWSlots"], "method": null, "args": [], "kwargs": {"foo": "bar", "bar": 2}}, "my_dataclass": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclass"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_enum": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyEnum"], "method": null, "args": ["foo"], "kwargs": {}}, "my_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyPydantic"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_funny_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyFunnyPydantic"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "person": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "Person"], "method": null, "args": [], "kwargs": {"name": "foo"}}, "a_bool": true, "a_none": null, "a_str": "foo", "a_str_nuc": "foo\\u0000", "a_str_uc": "foo \xe2\x9b\xb0\xef\xb8\x8f", "a_str_ucuc": "foo \xe2\x9b\xb0\xef\xb8\x8f\\u0000", "a_str_ucucuc": "foo \\\\u26f0\\\\ufe0f", "text": ["Hello", "Python", "Surrogate", "Example", "String", "With", "Surrogates", "Embedded", "In", "The", "Text", "\xe6\x94\xb6\xe8\x8a\xb1\xf0\x9f\x99\x84\xc2\xb7\xe5\x88\xb0"], "an_int": 1, "a_float": 1.1, "runnable_map": {"lc": 1, "type": "constructor", "id": ["langchain", "schema", "runnable", "RunnableParallel"], "kwargs": {"steps__": {}}, "name": "RunnableParallel<>", "graph": {"nodes": [{"id": 0, "type": "schema", "data": "Parallel<>Input"}, {"id": 1, "type": "schema", "data": "Parallel<>Output"}], "edges": []}}}""",
|
||||
b"""{"path": {"lc": 2, "type": "constructor", "id": ["pathlib", "Path"], "method": null, "args": ["foo", "bar"], "kwargs": {}}, "re": {"lc": 2, "type": "constructor", "id": ["re", "compile"], "method": null, "args": ["foo", 48], "kwargs": {}}, "decimal": {"lc": 2, "type": "constructor", "id": ["decimal", "Decimal"], "method": null, "args": ["1.10101"], "kwargs": {}}, "ip4": {"lc": 2, "type": "constructor", "id": ["ipaddress", "IPv4Address"], "method": null, "args": ["192.168.0.1"], "kwargs": {}}, "deque": {"lc": 2, "type": "constructor", "id": ["collections", "deque"], "method": null, "args": [[1, 2, 3]], "kwargs": {}}, "tzn": {"lc": 2, "type": "constructor", "id": ["zoneinfo", "ZoneInfo"], "method": null, "args": ["America/New_York"], "kwargs": {}}, "date": {"lc": 2, "type": "constructor", "id": ["datetime", "date"], "method": null, "args": [2024, 4, 19], "kwargs": {}}, "time": {"lc": 2, "type": "constructor", "id": ["datetime", "time"], "method": null, "args": [23, 4, 57, 51022, {"lc": 2, "type": "constructor", "id": ["datetime", "timezone"], "method": null, "args": [{"lc": 2, "type": "constructor", "id": ["datetime", "timedelta"], "method": null, "args": [0, 86340, 0], "kwargs": {}}], "kwargs": {}}], "kwargs": {"fold": 0}}, "uid": {"lc": 2, "type": "constructor", "id": ["uuid", "UUID"], "method": null, "args": ["00000000000000000000000000000001"], "kwargs": {}}, "timestamp": {"lc": 2, "type": "constructor", "id": ["datetime", "datetime"], "method": "fromisoformat", "args": ["2024-04-19T23:04:57.051022+23:59"], "kwargs": {}}, "my_slotted_class": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclassWSlots"], "method": null, "args": [], "kwargs": {"foo": "bar", "bar": 2}}, "my_dataclass": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclass"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_enum": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyEnum"], "method": null, "args": ["foo"], "kwargs": {}}, "my_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyPydantic"], "method": "model_construct", "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_funny_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyFunnyPydantic"], "method": "construct", "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "person": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "Person"], "method": null, "args": [], "kwargs": {"name": "foo"}}, "a_bool": true, "a_none": null, "a_str": "foo", "a_str_nuc": "foo\\u0000", "a_str_uc": "foo \xe2\x9b\xb0\xef\xb8\x8f", "a_str_ucuc": "foo \xe2\x9b\xb0\xef\xb8\x8f\\u0000", "a_str_ucucuc": "foo \\\\u26f0\\\\ufe0f", "text": ["Hello", "Python", "Surrogate", "Example", "String", "With", "Surrogates", "Embedded", "In", "The", "Text", "\xe6\x94\xb6\xe8\x8a\xb1\xf0\x9f\x99\x84\xc2\xb7\xe5\x88\xb0"], "an_int": 1, "a_float": 1.1, "runnable_map": {"lc": 1, "type": "constructor", "id": ["langchain", "schema", "runnable", "RunnableParallel"], "kwargs": {"steps__": {}}, "name": "RunnableParallel<>", "graph": {"nodes": [{"id": 0, "type": "schema", "data": "Parallel<>Input"}, {"id": 1, "type": "schema", "data": "Parallel<>Output"}], "edges": []}}}""",
|
||||
)
|
||||
|
||||
assert serde.loads_typed(dumped) == {
|
||||
@@ -130,3 +149,21 @@ def test_serde_jsonplus_bytearray() -> None:
|
||||
|
||||
assert dumped == ("bytearray", some_bytearray)
|
||||
assert serde.loads_typed(dumped) == some_bytearray
|
||||
|
||||
|
||||
def test_loads_cannot_find() -> None:
|
||||
serde = JsonPlusSerializer()
|
||||
|
||||
dumped = (
|
||||
"json",
|
||||
b'{"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyPydanticccc"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}',
|
||||
)
|
||||
|
||||
assert serde.loads_typed(dumped) is None, "Should return None if cannot find class"
|
||||
|
||||
dumped = (
|
||||
"json",
|
||||
b'{"lc": 2, "type": "constructor", "id": ["tests", "test_jsonpluss", "MyPydantic"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}',
|
||||
)
|
||||
|
||||
assert serde.loads_typed(dumped) is None, "Should return None if cannot find module"
|
||||
|
||||
@@ -82,26 +82,20 @@ class TestMemorySaver:
|
||||
assert search_results_2[0].metadata == self.metadata_2
|
||||
|
||||
search_results_3 = list(self.memory_saver.list(None, filter=query_3))
|
||||
assert len(search_results_3) == 2
|
||||
assert len(search_results_3) == 3
|
||||
|
||||
search_results_4 = list(self.memory_saver.list(None, filter=query_4))
|
||||
assert len(search_results_4) == 0
|
||||
|
||||
# search by config (defaults to root graph checkpoints)
|
||||
# search by config (defaults to checkpoints across all namespaces)
|
||||
search_results_5 = list(
|
||||
self.memory_saver.list({"configurable": {"thread_id": "thread-2"}})
|
||||
)
|
||||
assert len(search_results_5) == 1
|
||||
assert search_results_5[0].config["configurable"]["checkpoint_ns"] == ""
|
||||
|
||||
# search by config and checkpoint_ns
|
||||
search_results_6 = list(
|
||||
self.memory_saver.list(
|
||||
{"configurable": {"thread_id": "thread-2", "checkpoint_ns": "inner"}}
|
||||
)
|
||||
)
|
||||
assert len(search_results_6) == 1
|
||||
assert search_results_6[0].config["configurable"]["checkpoint_ns"] == "inner"
|
||||
assert len(search_results_5) == 2
|
||||
assert {
|
||||
search_results_5[0].config["configurable"]["checkpoint_ns"],
|
||||
search_results_5[1].config["configurable"]["checkpoint_ns"],
|
||||
} == {"", "inner"}
|
||||
|
||||
# TODO: test before and limit params
|
||||
|
||||
@@ -110,6 +104,7 @@ class TestMemorySaver:
|
||||
# save checkpoints
|
||||
self.memory_saver.put(self.config_1, self.chkpnt_1, self.metadata_1, {})
|
||||
self.memory_saver.put(self.config_2, self.chkpnt_2, self.metadata_2, {})
|
||||
self.memory_saver.put(self.config_3, self.chkpnt_3, self.metadata_3, {})
|
||||
|
||||
# call method / assertions
|
||||
query_1: CheckpointMetadata = {"source": "input"} # search by 1 key
|
||||
@@ -135,7 +130,7 @@ class TestMemorySaver:
|
||||
search_results_3 = [
|
||||
c async for c in self.memory_saver.alist(None, filter=query_3)
|
||||
]
|
||||
assert len(search_results_3) == 2
|
||||
assert len(search_results_3) == 3
|
||||
|
||||
search_results_4 = [
|
||||
c async for c in self.memory_saver.alist(None, filter=query_4)
|
||||
|
||||
+3
-3
@@ -21,11 +21,11 @@ lint_tests: PYTHON_FILES=tests
|
||||
lint_tests: MYPY_CACHE=.mypy_cache_test
|
||||
|
||||
lint lint_diff lint_package lint_tests:
|
||||
poetry run ruff .
|
||||
poetry run ruff check .
|
||||
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff
|
||||
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES)
|
||||
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES)
|
||||
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
|
||||
|
||||
format format_diff:
|
||||
poetry run ruff format $(PYTHON_FILES)
|
||||
poetry run ruff --select I --fix $(PYTHON_FILES)
|
||||
poetry run ruff check --select I --fix $(PYTHON_FILES)
|
||||
|
||||
@@ -339,7 +339,7 @@ async def gen_answer(
|
||||
# We could be more precise about handling max token length if we wanted to here
|
||||
dumped = json.dumps(all_query_results)[:max_str_len]
|
||||
ai_message: AIMessage = queries["raw"]
|
||||
tool_call = queries["raw"].additional_kwargs["tool_calls"][0]
|
||||
tool_call = queries["raw"].tool_calls[0]
|
||||
tool_id = tool_call["id"]
|
||||
tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)
|
||||
swapped_state["messages"].extend([ai_message, tool_message])
|
||||
|
||||
@@ -224,7 +224,7 @@ For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KE
|
||||
def on_stdout(line: str):
|
||||
if "unpacking to docker.io" in line:
|
||||
set("Starting...")
|
||||
elif "GET /ok" in line:
|
||||
elif "Application startup complete" in line:
|
||||
debugger_origin = (
|
||||
f"http://localhost:{debugger_port}"
|
||||
if debugger_port
|
||||
@@ -372,7 +372,11 @@ def _build(
|
||||
pull: bool,
|
||||
tag: str,
|
||||
):
|
||||
base_image = base_image or "langchain/langgraph-api"
|
||||
base_image = base_image or (
|
||||
"langchain/langgraphjs-api"
|
||||
if config_json.get("node_version")
|
||||
else "langchain/langgraph-api"
|
||||
)
|
||||
|
||||
# pull latest images
|
||||
if pull:
|
||||
@@ -380,7 +384,9 @@ def _build(
|
||||
subp_exec(
|
||||
"docker",
|
||||
"pull",
|
||||
f"{base_image}:{config_json['python_version']}",
|
||||
f"{base_image}:{config_json['node_version']}"
|
||||
if config_json.get("node_version")
|
||||
else f"{base_image}:{config_json['python_version']}",
|
||||
verbose=True,
|
||||
)
|
||||
)
|
||||
@@ -462,7 +468,11 @@ def dockerfile(save_path: pathlib.Path, config: pathlib.Path):
|
||||
with open(save_path, "w") as f:
|
||||
f.write(
|
||||
langgraph_cli.config.config_to_docker(
|
||||
config, config_json, "langchain/langgraph-api"
|
||||
config,
|
||||
config_json,
|
||||
"langchain/langgraphjs-api"
|
||||
if config_json.get("node_version")
|
||||
else "langchain/langgraph-api",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -500,7 +510,9 @@ def prepare_args_and_stdin(
|
||||
config_path,
|
||||
config,
|
||||
watch=watch,
|
||||
base_image="langchain/langgraph-api",
|
||||
base_image="langchain/langgraphjs-api"
|
||||
if config.get("node_version")
|
||||
else "langchain/langgraph-api",
|
||||
)
|
||||
return args, stdin
|
||||
|
||||
@@ -527,7 +539,9 @@ def prepare(
|
||||
subp_exec(
|
||||
"docker",
|
||||
"pull",
|
||||
f"langchain/langgraph-api:{config['python_version']}",
|
||||
f"langchain/langgraphjs-api:{config['node_version']}"
|
||||
if config.get("node_version")
|
||||
else f"langchain/langgraph-api:{config['python_version']}",
|
||||
verbose=verbose,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ import click
|
||||
|
||||
class Config(TypedDict):
|
||||
python_version: str
|
||||
node_version: Optional[str]
|
||||
pip_config_file: Optional[str]
|
||||
dockerfile_lines: list[str]
|
||||
dependencies: list[str]
|
||||
@@ -17,27 +18,46 @@ class Config(TypedDict):
|
||||
|
||||
|
||||
def validate_config(config: Config) -> Config:
|
||||
config = {
|
||||
"python_version": config.get("python_version", "3.11"),
|
||||
"pip_config_file": config.get("pip_config_file"),
|
||||
"dockerfile_lines": config.get("dockerfile_lines", []),
|
||||
"dependencies": config.get("dependencies", []),
|
||||
"graphs": config.get("graphs", {}),
|
||||
"env": config.get("env", {}),
|
||||
}
|
||||
if config["python_version"] not in (
|
||||
"3.11",
|
||||
"3.12",
|
||||
):
|
||||
raise click.UsageError(
|
||||
f"Unsupported Python version: {config['python_version']}. "
|
||||
"Supported versions are 3.11 and 3.12."
|
||||
)
|
||||
if not config["dependencies"]:
|
||||
raise click.UsageError(
|
||||
"No dependencies found in config. "
|
||||
"Add at least one dependency to 'dependencies' list."
|
||||
)
|
||||
config = (
|
||||
{
|
||||
"node_version": config.get("node_version"),
|
||||
"dockerfile_lines": config.get("dockerfile_lines", []),
|
||||
"graphs": config.get("graphs", {}),
|
||||
"env": config.get("env", {}),
|
||||
}
|
||||
if config.get("node_version")
|
||||
else {
|
||||
"python_version": config.get("python_version", "3.11"),
|
||||
"pip_config_file": config.get("pip_config_file"),
|
||||
"dockerfile_lines": config.get("dockerfile_lines", []),
|
||||
"dependencies": config.get("dependencies", []),
|
||||
"graphs": config.get("graphs", {}),
|
||||
"env": config.get("env", {}),
|
||||
}
|
||||
)
|
||||
|
||||
if config.get("node_version"):
|
||||
if config["node_version"] not in ("20",):
|
||||
raise click.UsageError(
|
||||
f"Unsupported Node.js version: {config['node_version']}. "
|
||||
"Currently only `node_version: \"20\"` is supported."
|
||||
)
|
||||
|
||||
if config.get("python_version"):
|
||||
if config["python_version"] not in (
|
||||
"3.11",
|
||||
"3.12",
|
||||
):
|
||||
raise click.UsageError(
|
||||
f"Unsupported Python version: {config['python_version']}. "
|
||||
"Supported versions are 3.11 and 3.12."
|
||||
)
|
||||
if not config["dependencies"]:
|
||||
raise click.UsageError(
|
||||
"No dependencies found in config. "
|
||||
"Add at least one dependency to 'dependencies' list."
|
||||
)
|
||||
|
||||
if not config["graphs"]:
|
||||
raise click.UsageError(
|
||||
"No graphs found in config. "
|
||||
@@ -191,7 +211,7 @@ def _update_graph_paths(
|
||||
config["graphs"][graph_id] = f"{module_str}:{attr_str}"
|
||||
|
||||
|
||||
def config_to_docker(config_path: pathlib.Path, config: Config, base_image: str):
|
||||
def python_config_to_docker(config_path: pathlib.Path, config: Config, base_image: str):
|
||||
# configure pip
|
||||
pip_install = (
|
||||
"PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt"
|
||||
@@ -266,6 +286,29 @@ ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
|
||||
{f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else ""}"""
|
||||
|
||||
|
||||
def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image: str):
|
||||
faux_path = f"/deps/{config_path.parent.name}"
|
||||
|
||||
return f"""FROM {base_image}:{config['node_version']}
|
||||
|
||||
{os.linesep.join(config["dockerfile_lines"])}
|
||||
|
||||
ADD . {faux_path}
|
||||
|
||||
RUN cd {faux_path} && yarn install --frozen-lockfile
|
||||
|
||||
ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
|
||||
|
||||
WORKDIR {faux_path}"""
|
||||
|
||||
|
||||
def config_to_docker(config_path: pathlib.Path, config: Config, base_image: str):
|
||||
if config.get("node_version"):
|
||||
return node_config_to_docker(config_path, config, base_image)
|
||||
|
||||
return python_config_to_docker(config_path, config, base_image)
|
||||
|
||||
|
||||
def config_to_compose(
|
||||
config_path: pathlib.Path,
|
||||
config: Config,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user