mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-19 22:25:44 +02:00
Compare commits
79
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46b6cd45e2 | ||
|
|
8c05502e97 | ||
|
|
72e73e47c9 | ||
|
|
7bf99a5d2f | ||
|
|
7461978b8c | ||
|
|
4f89cfc81d | ||
|
|
ea5cb4d100 | ||
|
|
e14c17f6b8 | ||
|
|
b34bfe165a | ||
|
|
ca3715b809 | ||
|
|
7e83cda0b5 | ||
|
|
6727277f8f | ||
|
|
d89fb9eef0 | ||
|
|
1774dbb860 | ||
|
|
82c989feea | ||
|
|
f66493f6db | ||
|
|
eb88dfefa3 | ||
|
|
b8d8a879d8 | ||
|
|
dc10b3aa8e | ||
|
|
af780f97a4 | ||
|
|
3a1b90c882 | ||
|
|
b9361d69b4 | ||
|
|
26b4d9139e | ||
|
|
67a164ca2d | ||
|
|
b68f9211c2 | ||
|
|
4efab93e2f | ||
|
|
cf345ef716 | ||
|
|
535dbd5b06 | ||
|
|
f2e0dc1042 | ||
|
|
6bf367300b | ||
|
|
e42e1a9f6b | ||
|
|
0d51c8614d | ||
|
|
b99a101b2c | ||
|
|
e150193491 | ||
|
|
7304ba683e | ||
|
|
ec51760f57 | ||
|
|
32d07df7a4 | ||
|
|
1ae60dccc5 | ||
|
|
5d1ab66535 | ||
|
|
b4944fa0a0 | ||
|
|
4ddb4ae5a0 | ||
|
|
a99b66ae3f | ||
|
|
4793b3f5e1 | ||
|
|
3c5913aa29 | ||
|
|
43d5dbf174 | ||
|
|
974b6c7bf3 | ||
|
|
d492475c61 | ||
|
|
e5b09b8c20 | ||
|
|
c1d7cbd67c | ||
|
|
8c0b27a85b | ||
|
|
322d6c8dde | ||
|
|
af666a87fb | ||
|
|
6e9f54e5ae | ||
|
|
dd4a15b569 | ||
|
|
1122e5cbbb | ||
|
|
29311f054d | ||
|
|
3b2a237deb | ||
|
|
0cc3799298 | ||
|
|
ddeb9d7314 | ||
|
|
d65d0ec665 | ||
|
|
a937754132 | ||
|
|
8c698fc362 | ||
|
|
3a07fe2efb | ||
|
|
1191dc1b48 | ||
|
|
ee5801511b | ||
|
|
d14477c891 | ||
|
|
01765fbda4 | ||
|
|
54bcd490ef | ||
|
|
85e827ae85 | ||
|
|
55263c640f | ||
|
|
51f58279ba | ||
|
|
7d6a302858 | ||
|
|
d225ef074f | ||
|
|
a07ec9d8c8 | ||
|
|
65b64c1331 | ||
|
|
8e6019a6b8 | ||
|
|
666fb3f31d | ||
|
|
86c047eb31 | ||
|
|
eee91d2129 |
@@ -21,7 +21,11 @@ jobs:
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
name: "test #${{ matrix.python-version }}"
|
||||
core-version:
|
||||
- ">=0.3.0.dev1,<0.4.0"
|
||||
- "latest"
|
||||
|
||||
name: "test #${{ matrix.python-version }} (langchain-core: ${{ matrix.core-version }})"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
|
||||
@@ -35,7 +39,11 @@ jobs:
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: poetry install --with dev
|
||||
run: |
|
||||
poetry install --with dev
|
||||
if [ "${{ matrix.core-version }}" != "latest" ]; then
|
||||
poetry run pip install "langchain-core${{ matrix.core-version }}"
|
||||
fi
|
||||
|
||||
- name: Run core tests
|
||||
shell: bash
|
||||
|
||||
@@ -30,6 +30,8 @@ _MANUAL = {
|
||||
"input_output_schema.ipynb",
|
||||
"pass_private_state.ipynb",
|
||||
"memory/manage-conversation-history.ipynb",
|
||||
"subgraphs-manage-state.ipynb",
|
||||
"subgraph-transform-state.ipynb",
|
||||
"memory/delete-messages.ipynb",
|
||||
"memory/add-summary-conversation-history.ipynb",
|
||||
"persistence_postgres.ipynb",
|
||||
|
||||
@@ -39,7 +39,7 @@ It's often useful to run graphs on some schedule. LangGraph Cloud supports cron
|
||||
- Create a new thread with the specified assistant
|
||||
- Send the specified input to that thread
|
||||
|
||||
Note that this sends the same input to the thread every time. See the [how-to guide](../how-tos/cloud_examples/cron_jobs.ipynb) for creating cron jobs.
|
||||
Note that this sends the same input to the thread every time. See the [how-to guide](../how-tos/cron_jobs.md) for creating cron jobs.
|
||||
|
||||
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.
|
||||
|
||||
@@ -182,13 +182,13 @@ The only difference is in stateless background runs, if the task worker dies hal
|
||||
- whereas a stateful background run would retry from the last successful checkpoint
|
||||
- a stateless background run would retry from the beginning
|
||||
|
||||
See the [how-to guide](../how-tos/cloud_examples/stateless_runs.ipynb) for creating stateless runs.
|
||||
See the [how-to guide](../how-tos/stateless_runs.md) for creating stateless runs.
|
||||
|
||||
### Webhooks
|
||||
|
||||
For all types of runs, langgraph cloud supports completion webhooks. When you create the run you can pass a webhook URL to be called when the completes (successfully or not). This is especially useful for background runs and cron jobs, as the webhook can give you an indication the run has completed and you can perform further actions for your appilcation.
|
||||
|
||||
See this [how-to guide](../how-tos/cloud_examples/webhooks.ipynb) to learn about how to use webhooks with LangGraph Cloud.
|
||||
See this [how-to guide](../how-tos/webhooks.md) to learn about how to use webhooks with LangGraph Cloud.
|
||||
|
||||
## Deployment
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
# How to Self-Host LangGraph Cloud API
|
||||
|
||||
!!! warning "Enterprise License Required"
|
||||
Self-hosting LangGraph Cloud API requires a license key. Please contact sales@langchain.dev for more details.
|
||||
|
||||
LangGraph Cloud APIs can be self-hosted with a valid LangGraph Cloud license key. Self-hosted deployments are built with Docker and deployed with Helm (on Kubernetes) or with Docker Compose. Ensure that the [Docker CLI](https://docs.docker.com/engine/reference/commandline/cli/) is installed.
|
||||
|
||||
LangGraph Cloud license key should be passed to the service as an environment variable named LANGGRAPH_CLOUD_LICENSE_KEY.
|
||||
|
||||
## Build Docker Image
|
||||
|
||||
1. Follow the [How-to Guide](setup.md) for setting up a LangGraph application for deployment. Your LangGraph application will vary from the example in the How-to Guide. However, ensure that the [LangGraph API configuration file](../reference/cli.md#configuration-file) is created.
|
||||
1. Install the [LangGraph CLI](../reference/cli.md#installation).
|
||||
1. Run the following LangGraph CLI `build` command to build a Docker image. Specify the image tag (`-t`) and other desired [options](../reference/cli.md#build).
|
||||
|
||||
langgraph build -t tag_name
|
||||
|
||||
!!! info "Build Platform"
|
||||
When building the Docker image, ensure that the image is built for the platform of the target Kubernetes cluster: `langgraph build -t tag_name --platform linux/amd64,linux/arm64`
|
||||
|
||||
## Self-Host on Kubernetes
|
||||
|
||||
This section is for self-hosting LangGraph Cloud API on Kubernetes via Helm. A Kubernetes cluster must be provisioned before proceeding with these steps. The public Helm chart for LangGraph Cloud is available [here](https://github.com/langchain-ai/helm/tree/main/charts/langgraph-cloud).
|
||||
|
||||
1. Publish the built Docker image to a repository that can be accessed by the target Kubernetes cluster.
|
||||
1. Ensure that the [Helm client](https://github.com/helm/helm?tab=readme-ov-file#install) is installed.
|
||||
1. Make note of all environment variables that are needed for the application. These values will need to be set in the Helm `values` YAML configuration.
|
||||
1. Follow [these instructions](https://github.com/langchain-ai/helm/tree/main/charts/langgraph-cloud#readme) to configure the Helm chart and deploy to Kubernetes.
|
||||
|
||||
## Self-Host with Docker
|
||||
|
||||
!!! warning "Under Construction"
|
||||
This section of the documentation is in progress.
|
||||
|
||||
Docker Compose can be used to deploy LangGraph Cloud to the compute infrastructure of your choice (e.g. VM).
|
||||
@@ -0,0 +1,200 @@
|
||||
# How to Set Up a LangGraph.js Application for Deployment
|
||||
|
||||
A [LangGraph.js](https://langchain-ai.github.io/langgraphjs/) 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.js application for deployment using `package.json` to specify project dependencies.
|
||||
|
||||
This walkthrough is based on [this repository](https://github.com/langchain-ai/langgraphjs-studio-starter), which you can play around with to learn more about how to setup your LangGraph application for deployment.
|
||||
|
||||
The final repo structure will look something like this:
|
||||
|
||||
```bash
|
||||
my-app/
|
||||
├── src # all project code lies within here
|
||||
│ ├── utils # optional utilities for your graph
|
||||
│ │ ├── tools.ts # tools for your graph
|
||||
│ │ ├── nodes.ts # node functions for you graph
|
||||
│ │ └── state.ts # state definition of your graph
|
||||
│ └── agent.ts # code for constructing your graph
|
||||
├── package.json # package dependencies
|
||||
├── .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.
|
||||
|
||||
## Specify Dependencies
|
||||
|
||||
Dependencies can be specified in a `package.json`. If none of these files is created, then dependencies can be specified later in the [LangGraph API configuration file](#create-langgraph-api-config).
|
||||
|
||||
Example `package.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "langgraphjs-studio-starter",
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"dependencies": {
|
||||
"@langchain/community": "^0.2.31",
|
||||
"@langchain/core": "^0.2.31",
|
||||
"@langchain/langgraph": "^0.2.0",
|
||||
"@langchain/openai": "^0.2.8"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example file directory:
|
||||
|
||||
```bash
|
||||
my-app/
|
||||
└── package.json # package dependencies
|
||||
```
|
||||
|
||||
## Specify Environment Variables
|
||||
|
||||
Environment variables can optionally be specified in a file (e.g. `.env`). See the [Environment Variables reference](../reference/env_var.md) to configure additional variables for a deployment.
|
||||
|
||||
Example `.env` file:
|
||||
|
||||
```
|
||||
MY_ENV_VAR_1=foo
|
||||
MY_ENV_VAR_2=bar
|
||||
OPENAI_API_KEY=key
|
||||
TAVILY_API_KEY=key_2
|
||||
```
|
||||
|
||||
Example file directory:
|
||||
|
||||
```bash
|
||||
my-app/
|
||||
├── package.json
|
||||
└── .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 compiled graph 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).
|
||||
|
||||
Here is an example `agent.ts`:
|
||||
|
||||
```ts
|
||||
import type { AIMessage } from "@langchain/core/messages";
|
||||
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
|
||||
import { MessagesAnnotation, StateGraph } from "@langchain/langgraph";
|
||||
import { ToolNode } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
const tools = [
|
||||
new TavilySearchResults({ maxResults: 3, }),
|
||||
];
|
||||
|
||||
// Define the function that calls the model
|
||||
async function callModel(
|
||||
state: typeof MessagesAnnotation.State,
|
||||
) {
|
||||
/**
|
||||
* Call the LLM powering our agent.
|
||||
* Feel free to customize the prompt, model, and other logic!
|
||||
*/
|
||||
const model = new ChatOpenAI({
|
||||
model: "gpt-4o",
|
||||
}).bindTools(tools);
|
||||
|
||||
const response = await model.invoke([
|
||||
{
|
||||
role: "system",
|
||||
content: `You are a helpful assistant. The current date is ${new Date().getTime()}.`
|
||||
},
|
||||
...state.messages
|
||||
]);
|
||||
|
||||
// MessagesAnnotation supports returning a single message or array of messages
|
||||
return { messages: response };
|
||||
}
|
||||
|
||||
// Define the function that determines whether to continue or not
|
||||
function routeModelOutput(state: typeof MessagesAnnotation.State) {
|
||||
const messages = state.messages;
|
||||
const lastMessage: AIMessage = messages[messages.length - 1];
|
||||
// If the LLM is invoking tools, route there.
|
||||
if ((lastMessage?.tool_calls?.length ?? 0) > 0) {
|
||||
return "tools";
|
||||
}
|
||||
// Otherwise end the graph.
|
||||
return "__end__";
|
||||
}
|
||||
|
||||
// Define a new graph.
|
||||
// See https://langchain-ai.github.io/langgraphjs/how-tos/define-state/#getting-started for
|
||||
// more on defining custom graph states.
|
||||
const workflow = new StateGraph(MessagesAnnotation)
|
||||
// Define the two nodes we will cycle between
|
||||
.addNode("callModel", callModel)
|
||||
.addNode("tools", new ToolNode(tools))
|
||||
// Set the entrypoint as `callModel`
|
||||
// This means that this node is the first one called
|
||||
.addEdge("__start__", "callModel")
|
||||
.addConditionalEdges(
|
||||
// First, we define the edges' source node. We use `callModel`.
|
||||
// This means these are the edges taken after the `callModel` node is called.
|
||||
"callModel",
|
||||
// Next, we pass in the function that will determine the sink node(s), which
|
||||
// will be called after the source node is called.
|
||||
routeModelOutput,
|
||||
// List of the possible destinations the conditional edge can route to.
|
||||
// Required for conditional edges to properly render the graph in Studio
|
||||
[
|
||||
"tools",
|
||||
"__end__"
|
||||
],
|
||||
)
|
||||
// This means that after `tools` is called, `callModel` node is called next.
|
||||
.addEdge("tools", "callModel");
|
||||
|
||||
// Finally, we compile it!
|
||||
// This compiles it into a graph you can invoke and deploy.
|
||||
export const graph = workflow.compile();
|
||||
```
|
||||
|
||||
!!! info "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 JavaScript module (alternatively, you can provide [a function that creates a graph](./graph_rebuild.md)).
|
||||
|
||||
Example file directory:
|
||||
|
||||
```bash
|
||||
my-app/
|
||||
├── src # all project code lies within here
|
||||
│ ├── utils # optional utilities for your graph
|
||||
│ │ ├── tools.ts # tools for your graph
|
||||
│ │ ├── nodes.ts # node functions for you graph
|
||||
│ │ └── state.ts # state definition of your graph
|
||||
│ └── agent.ts # code for constructing your graph
|
||||
├── package.json # package dependencies
|
||||
├── .env # environment variables
|
||||
└── langgraph.json # configuration file for LangGraph
|
||||
```
|
||||
|
||||
## Create LangGraph API Config
|
||||
|
||||
Create a [LangGraph API configuration file](../reference/cli.md#configuration-file) called `langgraph.json`. See the [LangGraph CLI reference](../reference/cli.md#configuration-file) for detailed explanations of each key in the JSON object of the configuration file.
|
||||
|
||||
Example `langgraph.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"node_version": "20",
|
||||
"dockerfile_lines": [],
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"agent": "./src/agent.ts: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>`).
|
||||
|
||||
!!! info "Configuration Location"
|
||||
The LangGraph API configuration file must be placed in a directory that is at the same level or higher than the TypeScript files that contain compiled graphs and associated dependencies.
|
||||
|
||||
## Next
|
||||
|
||||
After you setup your project and place it in a github repo, it's time to [deploy your app](./cloud.md).
|
||||
@@ -1,8 +1,8 @@
|
||||
# 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.
|
||||
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.
|
||||
This walkthrough is based on [this repository](https://github.com/langchain-ai/langgraph-example-pyproject), 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).
|
||||
@@ -34,6 +34,7 @@ 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.2.7,<0.3.0
|
||||
langgraph-checkpoint>=1.0.4
|
||||
|
||||
@@ -49,6 +49,7 @@ You can either initialize by passing authentication or by setting an environment
|
||||
|
||||
# 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>)
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
@@ -60,7 +61,8 @@ You can either initialize by passing authentication or by setting an environment
|
||||
|
||||
// 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"
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantId = "agent";
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
@@ -85,6 +87,7 @@ If you have a `LANGCHAIN_API_KEY` set in your environment, you do not need to ex
|
||||
|
||||
# only pass the url argument to get_client() if you changed the default port when calling langgraph up
|
||||
client = get_client()
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
@@ -96,7 +99,8 @@ If you have a `LANGCHAIN_API_KEY` set in your environment, you do not need to ex
|
||||
|
||||
// only set the apiUrl if you changed the default port when calling langgraph up
|
||||
const client = new Client();
|
||||
const assistantId = "agent"
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantId = "agent";
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
|
||||
@@ -43,15 +43,23 @@ If you don't define your conditional edges carefully, you might notice extra edg
|
||||
|
||||
### 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:
|
||||
The first way to solve this is to add path maps to your conditional edges. A path map is just a dictionary or array 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"})
|
||||
```
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"})
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```ts
|
||||
graph.addConditionalEdges("node_a", routingFunction, ["node_b", "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
|
||||
### Solution 2: Update the typing of the router (Python only)
|
||||
|
||||
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:
|
||||
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
# How to kick off background runs
|
||||
|
||||
This guide covers how to kick off background runs for your agent.
|
||||
This can be useful for long running jobs.
|
||||
|
||||
First let's set up our client and thread:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
# create thread
|
||||
thread = await client.threads.create()
|
||||
print(thread)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantID = "agent";
|
||||
// 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': '5cb1e8a1-34b3-4a61-a34e-71a9799bd00d',
|
||||
'created_at': '2024-08-30T20:35:52.062934+00:00',
|
||||
'updated_at': '2024-08-30T20:35:52.062934+00:00',
|
||||
'metadata': {},
|
||||
'status': 'idle',
|
||||
'config': {},
|
||||
'values': None
|
||||
}
|
||||
|
||||
If we list the current runs on this thread, we will see that it's empty:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
runs = await client.runs.list(thread["thread_id"])
|
||||
print(runs)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
let runs = await client.runs.list(thread['thread_id']);
|
||||
console.log(runs);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
[]
|
||||
|
||||
Now let's kick off a run:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
input = {"messages": [{"role": "human", "content": "what's the weather in sf"}]}
|
||||
run = await client.runs.create(thread["thread_id"], assistant_id, input=input)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
let input = {"messages": [{"role": "human", "content": "what's the weather in sf"}]};
|
||||
let run = await client.runs.create(thread["thread_id"], assistantID, { input });
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"assistant_id": <ASSISTANT_ID>
|
||||
}'
|
||||
```
|
||||
|
||||
The first time we poll it, we can see `status=pending`:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
print(await client.runs.get(thread["thread_id"], run["run_id"]))
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
console.log(await client.runs.get(thread["thread_id"], run["run_id"]));
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/<RUN_ID>
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{
|
||||
"run_id": "1ef6a5f8-bd86-6763-bbd6-bff042db7b1b",
|
||||
"thread_id": "7885f0cf-94ad-4040-91d7-73f7ba007c8a",
|
||||
"assistant_id": "fe096781-5601-53d2-b2f6-0d3403f7e9ca",
|
||||
"created_at": "2024-09-04T01:46:47.244887+00:00",
|
||||
"updated_at": "2024-09-04T01:46:47.244887+00:00",
|
||||
"metadata": {},
|
||||
"status": "pending",
|
||||
"kwargs": {
|
||||
"input": {
|
||||
"messages": [
|
||||
{
|
||||
"role": "human",
|
||||
"content": "what's the weather in sf"
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"metadata": {
|
||||
"created_by": "system"
|
||||
},
|
||||
"configurable": {
|
||||
"run_id": "1ef6a5f8-bd86-6763-bbd6-bff042db7b1b",
|
||||
"user_id": "",
|
||||
"graph_id": "agent",
|
||||
"thread_id": "7885f0cf-94ad-4040-91d7-73f7ba007c8a",
|
||||
"assistant_id": "fe096781-5601-53d2-b2f6-0d3403f7e9ca",
|
||||
"checkpoint_id": null
|
||||
}
|
||||
},
|
||||
"webhook": null,
|
||||
"temporary": false,
|
||||
"stream_mode": [
|
||||
"values"
|
||||
],
|
||||
"feedback_keys": null,
|
||||
"interrupt_after": null,
|
||||
"interrupt_before": null
|
||||
},
|
||||
"multitask_strategy": "reject"
|
||||
}
|
||||
|
||||
|
||||
|
||||
Now we can join the run, wait for it to finish and check that status again:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
await client.runs.join(thread["thread_id"], run["run_id"])
|
||||
print(await client.runs.get(thread["thread_id"], run["run_id"]))
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
await client.runs.join(thread["thread_id"], run["run_id"]);
|
||||
console.log(await client.runs.get(thread["thread_id"], run["run_id"]));
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/<RUN_ID>/join &&
|
||||
curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/<RUN_ID>
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{
|
||||
"run_id": "1ef6a5f8-bd86-6763-bbd6-bff042db7b1b",
|
||||
"thread_id": "7885f0cf-94ad-4040-91d7-73f7ba007c8a",
|
||||
"assistant_id": "fe096781-5601-53d2-b2f6-0d3403f7e9ca",
|
||||
"created_at": "2024-09-04T01:46:47.244887+00:00",
|
||||
"updated_at": "2024-09-04T01:46:47.244887+00:00",
|
||||
"metadata": {},
|
||||
"status": "success",
|
||||
"kwargs": {
|
||||
"input": {
|
||||
"messages": [
|
||||
{
|
||||
"role": "human",
|
||||
"content": "what's the weather in sf"
|
||||
}
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"metadata": {
|
||||
"created_by": "system"
|
||||
},
|
||||
"configurable": {
|
||||
"run_id": "1ef6a5f8-bd86-6763-bbd6-bff042db7b1b",
|
||||
"user_id": "",
|
||||
"graph_id": "agent",
|
||||
"thread_id": "7885f0cf-94ad-4040-91d7-73f7ba007c8a",
|
||||
"assistant_id": "fe096781-5601-53d2-b2f6-0d3403f7e9ca",
|
||||
"checkpoint_id": null
|
||||
}
|
||||
},
|
||||
"webhook": null,
|
||||
"temporary": false,
|
||||
"stream_mode": [
|
||||
"values"
|
||||
],
|
||||
"feedback_keys": null,
|
||||
"interrupt_after": null,
|
||||
"interrupt_before": null
|
||||
},
|
||||
"multitask_strategy": "reject"
|
||||
}
|
||||
|
||||
|
||||
Perfect! The run succeeded as we would expect. We can double check that the run worked as expected by printing out the final state:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
final_result = await client.threads.get_state(thread["thread_id"])
|
||||
print(final_result)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
let finalResult = await client.threads.getState(thread["thread_id"]);
|
||||
console.log(finalResult);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{
|
||||
"values": {
|
||||
"messages": [
|
||||
{
|
||||
"content": "what's the weather in sf",
|
||||
"additional_kwargs": {},
|
||||
"response_metadata": {},
|
||||
"type": "human",
|
||||
"name": null,
|
||||
"id": "beba31bf-320d-4125-9c37-cadf526ac47a",
|
||||
"example": false
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"id": "toolu_01AaNPSPzqia21v7aAKwbKYm",
|
||||
"input": {},
|
||||
"name": "tavily_search_results_json",
|
||||
"type": "tool_use",
|
||||
"index": 0,
|
||||
"partial_json": "{\"query\": \"weather in san francisco\"}"
|
||||
}
|
||||
],
|
||||
"additional_kwargs": {},
|
||||
"response_metadata": {
|
||||
"stop_reason": "tool_use",
|
||||
"stop_sequence": null
|
||||
},
|
||||
"type": "ai",
|
||||
"name": null,
|
||||
"id": "run-f220faf8-1d27-4f73-ad91-6bb3f47e8639",
|
||||
"example": false,
|
||||
"tool_calls": [
|
||||
{
|
||||
"name": "tavily_search_results_json",
|
||||
"args": {
|
||||
"query": "weather in san francisco"
|
||||
},
|
||||
"id": "toolu_01AaNPSPzqia21v7aAKwbKYm",
|
||||
"type": "tool_call"
|
||||
}
|
||||
],
|
||||
"invalid_tool_calls": [],
|
||||
"usage_metadata": {
|
||||
"input_tokens": 273,
|
||||
"output_tokens": 61,
|
||||
"total_tokens": 334
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.78, 'lon': -122.42, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1725052131, 'localtime': '2024-08-30 14:08'}, 'current': {'last_updated_epoch': 1725051600, 'last_updated': '2024-08-30 14:00', 'temp_c': 21.1, 'temp_f': 70.0, 'is_day': 1, 'condition': {'text': 'Partly cloudy', 'icon': '//cdn.weatherapi.com/weather/64x64/day/116.png', 'code': 1003}, 'wind_mph': 11.9, 'wind_kph': 19.1, 'wind_degree': 290, 'wind_dir': 'WNW', 'pressure_mb': 1018.0, 'pressure_in': 30.07, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 59, 'cloud': 25, 'feelslike_c': 21.1, 'feelslike_f': 70.0, 'windchill_c': 18.6, 'windchill_f': 65.5, 'heatindex_c': 18.6, 'heatindex_f': 65.5, 'dewpoint_c': 12.2, 'dewpoint_f': 54.0, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 5.0, 'gust_mph': 15.0, 'gust_kph': 24.2}}\"}]",
|
||||
"additional_kwargs": {},
|
||||
"response_metadata": {},
|
||||
"type": "tool",
|
||||
"name": "tavily_search_results_json",
|
||||
"id": "686b2487-f332-4e58-9508-89b3a814cd81",
|
||||
"tool_call_id": "toolu_01AaNPSPzqia21v7aAKwbKYm",
|
||||
"artifact": {
|
||||
"query": "weather in san francisco",
|
||||
"follow_up_questions": null,
|
||||
"answer": null,
|
||||
"images": [],
|
||||
"results": [
|
||||
{
|
||||
"title": "Weather in San Francisco",
|
||||
"url": "https://www.weatherapi.com/",
|
||||
"content": "{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.78, 'lon': -122.42, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1725052131, 'localtime': '2024-08-30 14:08'}, 'current': {'last_updated_epoch': 1725051600, 'last_updated': '2024-08-30 14:00', 'temp_c': 21.1, 'temp_f': 70.0, 'is_day': 1, 'condition': {'text': 'Partly cloudy', 'icon': '//cdn.weatherapi.com/weather/64x64/day/116.png', 'code': 1003}, 'wind_mph': 11.9, 'wind_kph': 19.1, 'wind_degree': 290, 'wind_dir': 'WNW', 'pressure_mb': 1018.0, 'pressure_in': 30.07, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 59, 'cloud': 25, 'feelslike_c': 21.1, 'feelslike_f': 70.0, 'windchill_c': 18.6, 'windchill_f': 65.5, 'heatindex_c': 18.6, 'heatindex_f': 65.5, 'dewpoint_c': 12.2, 'dewpoint_f': 54.0, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 5.0, 'gust_mph': 15.0, 'gust_kph': 24.2}}",
|
||||
"score": 0.976148,
|
||||
"raw_content": null
|
||||
}
|
||||
],
|
||||
"response_time": 3.07
|
||||
},
|
||||
"status": "success"
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "\n\nThe search results provide the current weather conditions in San Francisco. According to the data, as of 2:00 PM on August 30, 2024, the temperature in San Francisco is 70\u00b0F (21.1\u00b0C) with partly cloudy skies. The wind is blowing from the west-northwest at around 12 mph (19 km/h). The humidity is 59% and visibility is 9 miles (16 km). Overall, it looks like a nice late summer day in San Francisco with comfortable temperatures and partly sunny conditions.",
|
||||
"type": "text",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
"additional_kwargs": {},
|
||||
"response_metadata": {
|
||||
"stop_reason": "end_turn",
|
||||
"stop_sequence": null
|
||||
},
|
||||
"type": "ai",
|
||||
"name": null,
|
||||
"id": "run-8fecc61d-3d9f-4e16-8e8a-92f702be498a",
|
||||
"example": false,
|
||||
"tool_calls": [],
|
||||
"invalid_tool_calls": [],
|
||||
"usage_metadata": {
|
||||
"input_tokens": 837,
|
||||
"output_tokens": 124,
|
||||
"total_tokens": 961
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"next": [],
|
||||
"tasks": [],
|
||||
"metadata": {
|
||||
"step": 3,
|
||||
"run_id": "1ef67140-eb23-684b-8253-91d4c90bb05e",
|
||||
"source": "loop",
|
||||
"writes": {
|
||||
"agent": {
|
||||
"messages": [
|
||||
{
|
||||
"id": "run-8fecc61d-3d9f-4e16-8e8a-92f702be498a",
|
||||
"name": null,
|
||||
"type": "ai",
|
||||
"content": [
|
||||
{
|
||||
"text": "\n\nThe search results provide the current weather conditions in San Francisco. According to the data, as of 2:00 PM on August 30, 2024, the temperature in San Francisco is 70\u00b0F (21.1\u00b0C) with partly cloudy skies. The wind is blowing from the west-northwest at around 12 mph (19 km/h). The humidity is 59% and visibility is 9 miles (16 km). Overall, it looks like a nice late summer day in San Francisco with comfortable temperatures and partly sunny conditions.",
|
||||
"type": "text",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
"example": false,
|
||||
"tool_calls": [],
|
||||
"usage_metadata": {
|
||||
"input_tokens": 837,
|
||||
"total_tokens": 961,
|
||||
"output_tokens": 124
|
||||
},
|
||||
"additional_kwargs": {},
|
||||
"response_metadata": {
|
||||
"stop_reason": "end_turn",
|
||||
"stop_sequence": null
|
||||
},
|
||||
"invalid_tool_calls": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"user_id": "",
|
||||
"graph_id": "agent",
|
||||
"thread_id": "5cb1e8a1-34b3-4a61-a34e-71a9799bd00d",
|
||||
"created_by": "system",
|
||||
"assistant_id": "fe096781-5601-53d2-b2f6-0d3403f7e9ca"
|
||||
},
|
||||
"created_at": "2024-08-30T21:09:00.079909+00:00",
|
||||
"checkpoint_id": "1ef67141-3ca2-6fae-8003-fe96832e57d6",
|
||||
"parent_checkpoint_id": "1ef67141-2129-6b37-8002-61fc3bf69cb5"
|
||||
}
|
||||
|
||||
We can also just print the content of the last AIMessage:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
print(final_result['values']['messages'][-1]['content'][0]['text'])
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
console.log(finalResult['values']['messages'][finalResult['values']['messages'].length-1]['content'][0]['text']);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state | jq -r '.values.messages[-1].content.[0].text'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
The search results provide the current weather conditions in San Francisco. According to the data, as of 2:00 PM on August 30, 2024, the temperature in San Francisco is 70°F (21.1°C) with partly cloudy skies. The wind is blowing from the west-northwest at around 12 mph (19 km/h). The humidity is 59% and visibility is 9 miles (16 km). Overall, it looks like a nice late summer day in San Francisco with comfortable temperatures and partly sunny conditions.
|
||||
@@ -13,6 +13,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=<DEPLOYMENT_URL>)
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
@@ -23,7 +24,8 @@ First, we need to setup our client so that we can communicate with our hosted gr
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
const assistantId = agent;
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantId = "agent";
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
@@ -48,7 +50,7 @@ We can use the following commands to find threads that are idle, which means tha
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
console.log(await client.threads.search({status: "idle",limit:1}));
|
||||
console.log(await client.threads.search({ status: "idle", limit: 1 }));
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
@@ -83,7 +85,7 @@ We can use the following commands to find threads that have been interrupted in
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
console.log(await client.threads.search({status: "interrupted",limit:1}));
|
||||
console.log(await client.threads.search({ status: "interrupted", limit: 1 }));
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
@@ -117,7 +119,7 @@ We can use the following commands to find threads that are busy, meaning they ar
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
console.log(await client.threads.search({status: "busy",limit: 1}));
|
||||
console.log(await client.threads.search({ status: "busy", limit: 1 }));
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
@@ -183,7 +185,7 @@ The search endpoint for threads also allows you to filter on metadata, which can
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
console.log((await client.threads.search({metadata: {"foo":"bar"},limit: 1}))[0].status);
|
||||
console.log((await client.threads.search({ metadata: { "foo": "bar" }, limit: 1 }))[0].status);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
# How to create agents with configuration
|
||||
|
||||
One of the benefits of LangGraph API is that it lets you create agents with different configurations.
|
||||
This is useful when you want to:
|
||||
|
||||
- Define a cognitive architecture once as a LangGraph
|
||||
- Let that LangGraph be configurable across some attributes (for example, system message or LLM to use)
|
||||
- Let users create agents with arbitrary configurations, save them, and then use them in the future
|
||||
|
||||
In this guide we will show how to do that for the default agent we have built in.
|
||||
|
||||
If you look at the agent we defined, you can see that inside the `call_model` node we have created the model based on some configuration. That node looks like:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
def call_model(state, config):
|
||||
messages = state["messages"]
|
||||
model_name = config.get('configurable', {}).get("model_name", "anthropic")
|
||||
model = _get_model(model_name)
|
||||
response = model.invoke(messages)
|
||||
# We return a list, because this will get added to the existing list
|
||||
return {"messages": [response]}
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
function callModel(state: State, config: RunnableConfig) {
|
||||
const messages = state.messages;
|
||||
const modelName = config.configurable?.model_name ?? "anthropic";
|
||||
const model = _getModel(modelName);
|
||||
const response = model.invoke(messages);
|
||||
// We return a list, because this will get added to the existing list
|
||||
return { messages: [response] };
|
||||
}
|
||||
```
|
||||
|
||||
We are looking inside the config for a `model_name` parameter (which defaults to `anthropic` if none is found). That means that by default we are using Anthropic as our model provider. In this example we will see an example of how to create an example agent that is configured to use OpenAI.
|
||||
|
||||
First let's set up our client and thread:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# Select an assistant that is not configured
|
||||
assistants = await client.assistants.search()
|
||||
assistant = [a for a in assistants if not a["config"]][0]
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// Select an assistant that is not configured
|
||||
const assistants = await client.assistants.search();
|
||||
const assistant = assistants.find(a => !a.config);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/assistants/search \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"limit": 10,
|
||||
"offset": 0
|
||||
}' | jq -c 'map(select(.config == null or .config == {})) | .[0]'
|
||||
```
|
||||
|
||||
We can now call `.get_schemas` to get schemas associated with this graph:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
schemas = await client.assistants.get_schemas(
|
||||
assistant_id=assistant["assistant_id"]
|
||||
)
|
||||
# There are multiple types of schemas
|
||||
# We can get the `config_schema` to look at the the configurable parameters
|
||||
print(schemas["config_schema"])
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const schemas = await client.assistants.getSchemas(
|
||||
assistant["assistant_id"]
|
||||
);
|
||||
// There are multiple types of schemas
|
||||
// We can get the `config_schema` to look at the the configurable parameters
|
||||
console.log(schemas.config_schema);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/assistants/<ASSISTANT_ID>/schemas | jq -r '.config_schema'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{
|
||||
'model_name':
|
||||
{
|
||||
'title': 'Model Name',
|
||||
'enum': ['anthropic', 'openai'],
|
||||
'type': 'string'
|
||||
}
|
||||
}
|
||||
|
||||
Now we can initialize an assistant with config:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
openai_assistant = await client.assistants.create(
|
||||
# "agent" is the name of a graph we deployed
|
||||
"agent", config={"configurable": {"model_name": "openai"}}
|
||||
)
|
||||
|
||||
print(openai_assistant)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
let openAIAssistant = await client.assistants.create(
|
||||
// "agent" is the name of a graph we deployed
|
||||
"agent", { "configurable": { "model_name": "openai" } }
|
||||
);
|
||||
|
||||
console.log(openAIAssistant);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/assistants \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{"graph_id":"agent","config":{"configurable":{"model_name":"open_ai"}}}'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{
|
||||
"assistant_id": "62e209ca-9154-432a-b9e9-2d75c7a9219b",
|
||||
"graph_id": "agent",
|
||||
"created_at": "2024-08-31T03:09:10.230718+00:00",
|
||||
"updated_at": "2024-08-31T03:09:10.230718+00:00",
|
||||
"config": {
|
||||
"configurable": {
|
||||
"model_name": "open_ai"
|
||||
}
|
||||
},
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
We can verify the config is indeed taking effect:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
thread = await client.threads.create()
|
||||
input = {"messages": [{"role": "user", "content": "who made you?"}]}
|
||||
async for event in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
openai_assistant["assistant_id"],
|
||||
input=input,
|
||||
stream_mode="updates",
|
||||
):
|
||||
print(f"Receiving event of type: {event.event}")
|
||||
print(event.data)
|
||||
print("\n\n")
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const thread = await client.threads.create();
|
||||
let input = { "messages": [{ "role": "user", "content": "who made you?" }] };
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
openAIAssistant["assistant_id"],
|
||||
{
|
||||
input,
|
||||
streamMode: "updates"
|
||||
}
|
||||
);
|
||||
|
||||
for await (const event of streamResponse) {
|
||||
console.log(`Receiving event of type: ${event.event}`);
|
||||
console.log(event.data);
|
||||
console.log("\n\n");
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
thread_id=$(curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{}' | jq -r '.thread_id') && \
|
||||
curl --request POST \
|
||||
--url "<DEPLOYMENT_URL>/threads/${thread_id}/runs/stream" \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"assistant_id": <OPENAI_ASSISTANT_ID>,
|
||||
"input": {
|
||||
"messages": [
|
||||
{
|
||||
"role": "human",
|
||||
"content": "who made you?"
|
||||
}
|
||||
]
|
||||
},
|
||||
"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\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
Receiving event of type: metadata
|
||||
{'run_id': '1ef6746e-5893-67b1-978a-0f1cd4060e16'}
|
||||
|
||||
|
||||
|
||||
Receiving event of type: updates
|
||||
{'agent': {'messages': [{'content': 'I was created by OpenAI, a research organization focused on developing and advancing artificial intelligence technology.', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_157b3831f5'}, 'type': 'ai', 'name': None, 'id': 'run-e1a6b25c-8416-41f2-9981-f9cfe043f414', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
|
||||
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ 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:"<DEPLOYMENT_URL>" });
|
||||
const assistantId = agent;
|
||||
const client = new Client({ apiUrl: "<DEPLOYMENT_URL>" });
|
||||
const assistantId = "agent";
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
@@ -92,21 +92,21 @@ We can verify that the history from the prior thread did indeed copy over correc
|
||||
|
||||
```js
|
||||
function removeThreadId(d) {
|
||||
if (d.metadata && d.metadata.thread_id) {
|
||||
delete d.metadata.thread_id;
|
||||
}
|
||||
return 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);
|
||||
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.");
|
||||
// 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
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
# Cron Jobs
|
||||
|
||||
Sometimes you don't want to run your graph based on user interaction, but rather you would like to schedule your graph to run on a schedule - for example if you wish for your graph to compose and send out a weekly email of to-dos for your team. LangGraph Cloud allows you to do this without having to write your own script by using the `Crons` client. To schedule a graph job, you need to pass a [cron expression](https://crontab.cronhub.io/) to inform the client when you want to run the graph. `Cron` jobs are run in the background and do not interfere with normal invocations of the graph.
|
||||
|
||||
## Setup
|
||||
|
||||
First, let's setup our SDK client, assistant, and thread:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
# create thread
|
||||
thread = await client.threads.create()
|
||||
print(thread)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantId = "agent";
|
||||
// create thread
|
||||
const thread = await client.threads.create();
|
||||
console.log(thread);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/assistants/search \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"limit": 10,
|
||||
"offset": 0
|
||||
}' | jq -c 'map(select(.config == null or .config == {})) | .[0].graph_id' && \
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{}'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{
|
||||
'thread_id': '9dde5490-2b67-47c8-aa14-4bfec88af217',
|
||||
'created_at': '2024-08-30T23:07:38.242730+00:00',
|
||||
'updated_at': '2024-08-30T23:07:38.242730+00:00',
|
||||
'metadata': {},
|
||||
'status': 'idle',
|
||||
'config': {},
|
||||
'values': None
|
||||
}
|
||||
|
||||
## Cron job on a thread
|
||||
|
||||
To create a cron job associated with a specific thread, you can write:
|
||||
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
# This schedules a job to run at 15:27 (3:27PM) every day
|
||||
cron_job = await client.crons.create_for_thread(
|
||||
thread["thread_id"],
|
||||
assistant_id,
|
||||
schedule="27 15 * * *",
|
||||
input={"messages": [{"role": "user", "content": "What time is it?"}]},
|
||||
)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
// This schedules a job to run at 15:27 (3:27PM) every day
|
||||
const cronJob = await client.crons.create_for_thread(
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
schedule: "27 15 * * *",
|
||||
input: { messages: [{ role: "user", content: "What time is it?" }] }
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/crons \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"assistant_id": <ASSISTANT_ID>,
|
||||
}'
|
||||
```
|
||||
|
||||
Note that it is **very** important to delete `Cron` jobs that are no longer useful. Otherwise you could rack up unwanted API charges to the LLM! You can delete a `Cron` job using the following code:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
await client.crons.delete(cron_job["cron_id"])
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
await client.crons.delete(cronJob["cron_id"]);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request DELETE \
|
||||
--url <DEPLOYMENT_URL>/runs/crons/<CRON_ID>
|
||||
```
|
||||
|
||||
## Cron job stateless
|
||||
|
||||
You can also create stateless cron jobs by using the following code:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
# This schedules a job to run at 15:27 (3:27PM) every day
|
||||
cron_job_stateless = await client.crons.create(
|
||||
assistant_id,
|
||||
schedule="27 15 * * *",
|
||||
input={"messages": [{"role": "user", "content": "What time is it?"}]},
|
||||
)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
// This schedules a job to run at 15:27 (3:27PM) every day
|
||||
const cronJobStateless = await client.crons.create(
|
||||
assistantId,
|
||||
{
|
||||
schedule: "27 15 * * *",
|
||||
input: { messages: [{ role: "user", content: "What time is it?" }] }
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/runs/crons \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"assistant_id": <ASSISTANT_ID>,
|
||||
}'
|
||||
```
|
||||
|
||||
Again, remember to delete your job once you are done with it!
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
await client.crons.delete(cron_job_stateless["cron_id"])
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
await client.crons.delete(cronJobStateless["cron_id"]);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request DELETE \
|
||||
--url <DEPLOYMENT_URL>/runs/crons/<CRON_ID>
|
||||
```
|
||||
@@ -5,20 +5,44 @@ This guide assumes knowledge of what double-texting is, which you can learn abou
|
||||
The guide covers the `enqueue` option for double texting, which adds the interruptions to a queue and executes them in the order they are received by the client. Below is a quick example of using the `enqueue` option.
|
||||
|
||||
|
||||
First, we will define a quick helper function for printing out JS model outputs (you can skip this if using Python):
|
||||
First, we will define a quick helper function for printing out JS and CURL model outputs (you can skip this if using Python):
|
||||
|
||||
```js
|
||||
function prettyPrint(m) {
|
||||
const padded = " " + m['type'] + " ";
|
||||
const sepLen = Math.floor((80 - padded.length) / 2);
|
||||
const sep = "=".repeat(sepLen);
|
||||
const secondSep = sep + (padded.length % 2 ? "=" : "");
|
||||
|
||||
console.log(`${sep}${padded}${secondSep}`);
|
||||
console.log("\n\n");
|
||||
console.log(m.content);
|
||||
}
|
||||
```
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
function prettyPrint(m) {
|
||||
const padded = " " + m['type'] + " ";
|
||||
const sepLen = Math.floor((80 - padded.length) / 2);
|
||||
const sep = "=".repeat(sepLen);
|
||||
const secondSep = sep + (padded.length % 2 ? "=" : "");
|
||||
|
||||
console.log(`${sep}${padded}${secondSep}`);
|
||||
console.log("\n\n");
|
||||
console.log(m.content);
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
# PLACE THIS IN A FILE CALLED pretty_print.sh
|
||||
pretty_print() {
|
||||
local type="$1"
|
||||
local content="$2"
|
||||
local padded=" $type "
|
||||
local total_width=80
|
||||
local sep_len=$(( (total_width - ${#padded}) / 2 ))
|
||||
local sep=$(printf '=%.0s' $(eval "echo {1.."${sep_len}"}"))
|
||||
local second_sep=$sep
|
||||
if (( (total_width - ${#padded}) % 2 )); then
|
||||
second_sep="${second_sep}="
|
||||
fi
|
||||
|
||||
echo "${sep}${padded}${second_sep}"
|
||||
echo
|
||||
echo "$content"
|
||||
}
|
||||
```
|
||||
|
||||
Then, let's import our required packages and instantiate our client, assistant, and thread.
|
||||
|
||||
@@ -32,6 +56,7 @@ Then, let's import our required packages and instantiate our client, assistant,
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
@@ -43,9 +68,18 @@ Then, let's import our required packages and instantiate our client, assistant,
|
||||
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantId = "agent";
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
Now let's start two runs, with the second interrupting the first one with a multitask strategy of "enqueue":
|
||||
|
||||
@@ -82,6 +116,25 @@ Now let's start two runs, with the second interrupting the first one with a mult
|
||||
)
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOY<ENT_URL>>/threads/<THREAD_ID>/runs \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in sf?\"}]},
|
||||
}" && curl --request POST \
|
||||
--url <DEPLOY<ENT_URL>>/threads/<THREAD_ID>/runs \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in nyc?\"}]},
|
||||
\"multitask_strategy\": \"enqueue\"
|
||||
}"
|
||||
```
|
||||
|
||||
Verify that the thread has data from both runs:
|
||||
|
||||
=== "Python"
|
||||
@@ -108,12 +161,25 @@ Verify that the thread has data from both runs:
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
source pretty_print.sh && curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/<RUN_ID>/join && \
|
||||
curl --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state | \
|
||||
jq -c '.values.messages[]' | while read -r element; do
|
||||
type=$(echo "$element" | jq -r '.type')
|
||||
content=$(echo "$element" | jq -r '.content | if type == "array" then tostring else . end')
|
||||
pretty_print "$type" "$content"
|
||||
done
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
================================[1m Human Message [0m=================================
|
||||
================================ Human Message =================================
|
||||
|
||||
what's the weather in sf?
|
||||
==================================[1m Ai Message [0m==================================
|
||||
================================== Ai Message ==================================
|
||||
|
||||
[{'id': 'toolu_01Dez1sJre4oA2Y7NsKJV6VT', 'input': {'query': 'weather in san francisco'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]
|
||||
Tool Calls:
|
||||
@@ -121,11 +187,11 @@ Output:
|
||||
Call ID: toolu_01Dez1sJre4oA2Y7NsKJV6VT
|
||||
Args:
|
||||
query: weather in san francisco
|
||||
=================================[1m Tool Message [0m=================================
|
||||
================================= Tool Message =================================
|
||||
Name: tavily_search_results_json
|
||||
|
||||
[{"url": "https://www.accuweather.com/en/us/san-francisco/94103/weather-forecast/347629", "content": "Get the current and future weather conditions for San Francisco, CA, including temperature, precipitation, wind, air quality and more. See the hourly and 10-day outlook, radar maps, alerts and allergy information."}]
|
||||
==================================[1m Ai Message [0m==================================
|
||||
================================== Ai Message ==================================
|
||||
|
||||
According to AccuWeather, the current weather conditions in San Francisco are:
|
||||
|
||||
@@ -145,10 +211,10 @@ Output:
|
||||
Sunday: Partly sunny, high of 61°F (16°C)
|
||||
|
||||
So in summary, expect seasonable spring weather in San Francisco over the next several days, with a mix of sun and clouds and temperatures ranging from the upper 40s at night to the low 60s during the days. Typical dry conditions with no rain in the forecast.
|
||||
================================[1m Human Message [0m=================================
|
||||
================================ Human Message =================================
|
||||
|
||||
what's the weather in nyc?
|
||||
==================================[1m Ai Message [0m==================================
|
||||
================================== Ai Message ==================================
|
||||
|
||||
[{'text': 'Here are the current weather conditions and forecast for New York City:', 'type': 'text'}, {'id': 'toolu_01FFft5Sx9oS6AdVJuRWWcGp', 'input': {'query': 'weather in new york city'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]
|
||||
Tool Calls:
|
||||
@@ -156,11 +222,11 @@ Output:
|
||||
Call ID: toolu_01FFft5Sx9oS6AdVJuRWWcGp
|
||||
Args:
|
||||
query: weather in new york city
|
||||
=================================[1m Tool Message [0m=================================
|
||||
================================= Tool Message =================================
|
||||
Name: tavily_search_results_json
|
||||
|
||||
[{"url": "https://www.weatherapi.com/", "content": "{'location': {'name': 'New York', 'region': 'New York', 'country': 'United States of America', 'lat': 40.71, 'lon': -74.01, 'tz_id': 'America/New_York', 'localtime_epoch': 1718734479, 'localtime': '2024-06-18 14:14'}, 'current': {'last_updated_epoch': 1718733600, 'last_updated': '2024-06-18 14:00', 'temp_c': 29.4, 'temp_f': 84.9, 'is_day': 1, 'condition': {'text': 'Sunny', 'icon': '//cdn.weatherapi.com/weather/64x64/day/113.png', 'code': 1000}, 'wind_mph': 2.2, 'wind_kph': 3.6, 'wind_degree': 158, 'wind_dir': 'SSE', 'pressure_mb': 1025.0, 'pressure_in': 30.26, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 63, 'cloud': 0, 'feelslike_c': 31.3, 'feelslike_f': 88.3, 'windchill_c': 28.3, 'windchill_f': 82.9, 'heatindex_c': 29.6, 'heatindex_f': 85.3, 'dewpoint_c': 18.4, 'dewpoint_f': 65.2, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 7.0, 'gust_mph': 16.5, 'gust_kph': 26.5}}"}]
|
||||
==================================[1m Ai Message [0m==================================
|
||||
================================== Ai Message ==================================
|
||||
|
||||
According to the weather data from WeatherAPI:
|
||||
|
||||
|
||||
@@ -22,6 +22,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=<DEPLOYMENT_URL>)
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
@@ -32,7 +33,8 @@ In this how-to we use a simple ReAct style hosted graph (you can see the full co
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
const assistantId = "agent"
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantId = "agent";
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
@@ -73,7 +75,7 @@ And, now let's compile it with a breakpoint before the tool node:
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const input = { "messages": [{ "role": "human", "content": "what's the weather in sf"}] }
|
||||
const input = { messages: [{ role: "human", content: "what's the weather in sf" }] };
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
@@ -81,9 +83,10 @@ And, now let's compile it with a breakpoint before the tool node:
|
||||
{
|
||||
input: input,
|
||||
streamMode: "updates",
|
||||
interruptBefore: ["action"],
|
||||
interruptBefore: ["action"]
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of streamResponse) {
|
||||
console.log(`Receiving new event of type: ${chunk.event}...`);
|
||||
console.log(chunk.data);
|
||||
|
||||
@@ -18,6 +18,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=<DEPLOYMENT_URL>)
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
@@ -28,6 +29,7 @@ First, we need to setup our client so that we can communicate with our hosted gr
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantId = "agent";
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
@@ -65,7 +67,7 @@ Now let's invoke our graph, making sure to interrupt before the `action` node.
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const input = {"messages": [{ "role": "human", "content": "search for weather in SF"}] }
|
||||
const input = { messages: [{ role: "human", content: "search for weather in SF" }] };
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
@@ -76,6 +78,7 @@ Now let's invoke our graph, making sure to interrupt before the `action` node.
|
||||
interruptBefore: ["action"],
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
@@ -154,15 +157,15 @@ Now, let's assume we actually meant to search for the weather in Sidi Frej (anot
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
// First, lets get the current state
|
||||
const currentState = await client.threads.getState(thread['thread_id']);
|
||||
// First, let's get the current state
|
||||
const currentState = await client.threads.getState(thread["thread_id"]);
|
||||
|
||||
// Let's now get the last message in the state
|
||||
// This is the one with the tool calls that we want to update
|
||||
let lastMessage = currentState['values']['messages'][-1];
|
||||
let lastMessage = currentState.values.messages.slice(-1)[0];
|
||||
|
||||
// Let's now update the args for that tool call
|
||||
lastMessage['tool_calls'][0]['args'] = {'query': 'current weather in Sidi Frej'};
|
||||
lastMessage.tool_calls[0].args = { query: "current weather in Sidi Frej" };
|
||||
|
||||
// Let's now call `update_state` to pass in this message in the `messages` key
|
||||
// This will get treated as any other update to the state
|
||||
@@ -170,7 +173,7 @@ Now, let's assume we actually meant to search for the weather in Sidi Frej (anot
|
||||
// That reducer function will use the ID of the message to update it
|
||||
// It's important that it has the right ID! Otherwise it would get appended
|
||||
// as a new message
|
||||
await client.threads.updateState(thread['thread_id'], {values:{"messages": lastMessage}});
|
||||
await client.threads.updateState(thread["thread_id"], { values: { messages: lastMessage } });
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
@@ -220,6 +223,7 @@ Now we can resume our graph run but with the updated state:
|
||||
streamMode: "updates",
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
|
||||
@@ -29,6 +29,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=<DEPLOYMENT_URL>)
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
@@ -39,10 +40,19 @@ First, we need to setup our client so that we can communicate with our hosted gr
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantId = "agent";
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
## Example with no review
|
||||
|
||||
Let's look at an example when no review is required (because no tools are called)
|
||||
@@ -66,7 +76,7 @@ Let's look at an example when no review is required (because no tools are called
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const input = {"messages": [{ "role": "human", "content": "hi!"}] }
|
||||
const input = { "messages": [{ "role": "human", "content": "hi!" }] };
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
@@ -77,6 +87,7 @@ Let's look at an example when no review is required (because no tools are called
|
||||
interruptBefore: ["action"],
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
@@ -84,6 +95,42 @@ Let's look at an example when no review is required (because no tools are called
|
||||
}
|
||||
```
|
||||
|
||||
=== "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\": \"hi!\"}]},
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
],
|
||||
\"interrupt_before\": [\"action\"]
|
||||
}" | \
|
||||
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:
|
||||
|
||||
{'messages': [{'content': 'hi!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '39c51f14-2d5c-4690-883a-d940854b1845', 'example': False}]}
|
||||
@@ -108,6 +155,13 @@ If we check the state, we can see that it is finished
|
||||
console.log(state.next);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state | jq -c '.next'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
[]
|
||||
@@ -125,7 +179,6 @@ Let's now look at what it looks like to approve a tool call. Note that we don't
|
||||
thread["thread_id"],
|
||||
"agent",
|
||||
input=input,
|
||||
stream_mode="values",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
@@ -134,16 +187,16 @@ Let's now look at what it looks like to approve a tool call. Note that we don't
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]}
|
||||
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);
|
||||
@@ -151,6 +204,38 @@ Let's now look at what it looks like to approve a tool call. Note that we don't
|
||||
}
|
||||
```
|
||||
|
||||
=== "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?\"}]}
|
||||
}" | \
|
||||
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:
|
||||
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '54e19d6e-89fa-44fb-b92c-12e7dd4ddf08', 'example': False}]}
|
||||
@@ -175,6 +260,13 @@ If we now check, we can see that it is waiting on human review:
|
||||
console.log(state.next);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url <DELPOYMENT_URL>/threads/<THREAD_ID>/state | jq -c '.next'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
['human_review_node']
|
||||
@@ -201,10 +293,11 @@ To approve the tool call, we can just continue the thread with no edits. To do t
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
input: undefined,
|
||||
input: null,
|
||||
streamMode: "values",
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
@@ -212,6 +305,37 @@ To approve the tool call, we can just continue the thread with no edits. To do t
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\"
|
||||
}" | \
|
||||
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:
|
||||
|
||||
{'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'}]}
|
||||
@@ -239,7 +363,7 @@ Let's now say we want to edit the tool call. E.g. change some of the parameters
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]}
|
||||
const input = { "messages": [{ "role": "user", "content": "what's the weather in sf?" }] };
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
@@ -249,6 +373,7 @@ Let's now say we want to edit the tool call. E.g. change some of the parameters
|
||||
streamMode: "values",
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
@@ -256,6 +381,38 @@ Let's now say we want to edit the tool call. E.g. change some of the parameters
|
||||
}
|
||||
```
|
||||
|
||||
=== "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?\"}]}
|
||||
}" | \
|
||||
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:
|
||||
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'cec11391-84da-464b-bd2a-bd4f0d93b9ee', 'example': False}]}
|
||||
@@ -310,7 +467,6 @@ To do this, we first need to update the state. We can do this by passing a messa
|
||||
thread["thread_id"],
|
||||
"agent",
|
||||
input=None,
|
||||
stream_mode="values",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
@@ -332,43 +488,93 @@ To do this, we first need to update the state. We can do this by passing a messa
|
||||
|
||||
// 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
|
||||
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
|
||||
{
|
||||
thread.thread_id, // Thread ID
|
||||
{
|
||||
values: { "messages": [newMessage] }, // Updated message
|
||||
asNode: "human_review_node"
|
||||
} // Acting as 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"],
|
||||
}
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
input: null,
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of streamResponseResumed) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"values\": { \"messages\": [$(curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state |
|
||||
jq -c '{
|
||||
role: "assistant",
|
||||
content: .values.messages[-1].content,
|
||||
tool_calls: [
|
||||
{
|
||||
id: .values.messages[-1].tool_calls[0].id,
|
||||
name: "weather_search",
|
||||
args: { city: "San Francisco, USA" }
|
||||
}
|
||||
],
|
||||
id: .values.messages[-1].id
|
||||
}')
|
||||
]},
|
||||
\"as_node\": \"human_review_node\"
|
||||
}" && echo "Resuming Execution" && curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"assistant_id": "agent"
|
||||
}' | \
|
||||
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:
|
||||
@@ -404,7 +610,6 @@ For this example we will just add a single tool call representing the feedback.
|
||||
thread["thread_id"],
|
||||
"agent",
|
||||
input=input,
|
||||
stream_mode="values",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
@@ -413,16 +618,16 @@ For this example we will just add a single tool call representing the feedback.
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]}
|
||||
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);
|
||||
@@ -430,6 +635,38 @@ For this example we will just add a single tool call representing the feedback.
|
||||
}
|
||||
```
|
||||
|
||||
=== "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?\"}]}
|
||||
}" | \
|
||||
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:
|
||||
|
||||
{'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c80f13d0-674d-4233-b6a0-3940509d3cf3', 'example': False}]}
|
||||
@@ -493,38 +730,85 @@ To do this, we first need to update the state. We can do this by passing a messa
|
||||
|
||||
// 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,
|
||||
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
|
||||
{
|
||||
thread.thread_id, // Thread ID
|
||||
{
|
||||
values: { "messages": [newMessage] }, // Updated message
|
||||
asNode: "human_review_node"
|
||||
} // Acting as 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,
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
input: null,
|
||||
streamMode: "values",
|
||||
interruptBefore: ["action"],
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of streamResponseEdited) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"values\": { \"messages\": [$(curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state |
|
||||
jq -c '{
|
||||
role: "tool",
|
||||
content: "User requested changes: pass in the country as well",
|
||||
name: "get_weather",
|
||||
tool_call_id: .values.messages[-1].id.tool_calls[0].id
|
||||
}')
|
||||
]},
|
||||
\"as_node\": \"human_review_node\"
|
||||
}" && echo "Resuming Execution" && curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"assistant_id": "agent"
|
||||
}' | \
|
||||
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:
|
||||
|
||||
Current State:
|
||||
@@ -545,7 +829,6 @@ We can see that we now get to another breakpoint - because it went back to the m
|
||||
thread["thread_id"],
|
||||
"agent",
|
||||
input=None,
|
||||
stream_mode="values",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
@@ -558,10 +841,10 @@ We can see that we now get to another breakpoint - because it went back to the m
|
||||
thread["thread_id"],
|
||||
assistantId,
|
||||
{
|
||||
input: undefined,
|
||||
streamMode: "values",
|
||||
input: null,
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of streamResponseResumed) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
@@ -569,6 +852,37 @@ We can see that we now get to another breakpoint - because it went back to the m
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\"
|
||||
}" | \
|
||||
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:
|
||||
|
||||
{'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'}]}
|
||||
|
||||
@@ -15,6 +15,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=<DEPLOYMENT_URL>)
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
@@ -25,7 +26,8 @@ First, we need to setup our client so that we can communicate with our hosted gr
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
const assistantId = agent;
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantId = "agent";
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
@@ -34,8 +36,7 @@ First, we need to setup our client so that we can communicate with our hosted gr
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data {}
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
## Replay a state
|
||||
@@ -51,7 +52,7 @@ Before replaying a state - we need to create states to replay from! In order to
|
||||
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistant_id, # graph_id
|
||||
assistant_id,
|
||||
input=input,
|
||||
stream_mode="updates",
|
||||
):
|
||||
@@ -308,7 +309,7 @@ Now we can rerun our graph with this new config, starting from the `new_state`,
|
||||
```python
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistant["assistant_id"], # graph_id
|
||||
assistant_id,
|
||||
input=None,
|
||||
stream_mode="updates",
|
||||
checkpoint_id=config['checkpoint_id']
|
||||
@@ -322,7 +323,7 @@ Now we can rerun our graph with this new config, starting from the `new_state`,
|
||||
```js
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistant["assistant_id"],
|
||||
assistantId,
|
||||
{
|
||||
input: null,
|
||||
streamMode: "updates",
|
||||
|
||||
@@ -25,6 +25,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=<DEPLOYMENT_URL>)
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
@@ -35,6 +36,7 @@ First, we need to setup our client so that we can communicate with our hosted gr
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantId = "agent";
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
@@ -56,7 +58,14 @@ Now, let's invoke our graph by interrupting before `ask_human` node:
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
input = { 'messages':[{ "role":"user", "content":"Use the search tool to ask the user where they are, then look up the weather there" }] }
|
||||
input = {
|
||||
"messages": [
|
||||
{
|
||||
"role": "human",
|
||||
"content": "Use the search tool to ask the user where they are, then look up the weather there",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async for chunk in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
@@ -71,7 +80,14 @@ Now, let's invoke our graph by interrupting before `ask_human` node:
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const input = { "messages":[{ "role":"human", "content": "Use the search tool to ask the user where they are, then look up the weather there"}] }
|
||||
const input = {
|
||||
messages: [
|
||||
{
|
||||
role: "human",
|
||||
content: "Use the search tool to ask the user where they are, then look up the weather there"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
@@ -79,9 +95,10 @@ Now, let's invoke our graph by interrupting before `ask_human` node:
|
||||
{
|
||||
input: input,
|
||||
streamMode: "updates",
|
||||
interruptBefore: ["ask_human"],
|
||||
interruptBefore: ["ask_human"]
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
@@ -152,13 +169,23 @@ Because we are treating this as a tool call, we will need to update the state as
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const state = await client.threads.getState(thread['thread_id']);
|
||||
const toolCallId = state['values']['messages'][-1]['tool_calls'][0]['id'];
|
||||
const state = await client.threads.getState(thread["thread_id"]);
|
||||
const toolCallId = state.values.messages[state.values.messages.length - 1].tool_calls[0].id;
|
||||
|
||||
# We now create the tool call with the id and the response we want
|
||||
const toolMessage = [{"tool_call_id": toolCallId, "type": "tool", "content": "san francisco"}];
|
||||
// We now create the tool call with the id and the response we want
|
||||
const toolMessage = [
|
||||
{
|
||||
tool_call_id: toolCallId,
|
||||
type: "tool",
|
||||
content: "san francisco"
|
||||
}
|
||||
];
|
||||
|
||||
await client.threads.updateState(thread['thread_id'], {values: {"messages": toolMessage}, asNode:"ask_human"})
|
||||
await client.threads.updateState(
|
||||
thread["thread_id"],
|
||||
{ values: { messages: toolMessage } },
|
||||
{ asNode: "ask_human" }
|
||||
);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
@@ -212,9 +239,10 @@ We can now tell the agent to continue. We can just pass in None as the input to
|
||||
assistantId,
|
||||
{
|
||||
input: null,
|
||||
streamMode: "updates",
|
||||
streamMode: "updates"
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.data && chunk.event !== "metadata") {
|
||||
console.log(chunk.data);
|
||||
|
||||
@@ -13,9 +13,9 @@ LangGraph Cloud gives you best in class observability, testing, and hosting serv
|
||||
|
||||
- [How to set up app for deployment (requirements.txt)](../deployment/setup.md)
|
||||
- [How to set up app for deployment (pyproject.toml)](../deployment/setup_pyproject.md)
|
||||
- [How to set up app for deployment (JavaScript)](../deployment/setup_javascript.md)
|
||||
- [How to test locally](../deployment/test_locally.md)
|
||||
- [How to deploy to LangGraph cloud](../deployment/cloud.md)
|
||||
- [How to self-host](../deployment/self_hosted.md)
|
||||
|
||||
|
||||
## Streaming
|
||||
@@ -61,17 +61,17 @@ LangGraph Studio is a built-in UI for visualizing, testing, and debugging your a
|
||||
|
||||
LangGraph Cloud supports multiple types of runs besides streaming runs.
|
||||
|
||||
- [How to run an agent in the background](cloud_examples/background_run.ipynb)
|
||||
- [How to run multiple agents in the same thread](cloud_examples/same-thread.ipynb)
|
||||
- [How to create cron jobs](cloud_examples/cron_jobs.ipynb)
|
||||
- [How to create stateless runs](cloud_examples/stateless_runs.ipynb)
|
||||
- [How to run an agent in the background](./background_run.md)
|
||||
- [How to run multiple agents in the same thread](./same-thread.md)
|
||||
- [How to create cron jobs](./cron_jobs.md)
|
||||
- [How to create stateless runs](./stateless_runs.md)
|
||||
|
||||
## Other
|
||||
|
||||
Other guides that may prove helpful!
|
||||
|
||||
- [How to configure agents](cloud_examples/configuration_cloud.ipynb)
|
||||
- [How to configure agents](./configuration_cloud.md)
|
||||
- [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 integrate webhooks](./webhooks.md)
|
||||
- [How to copy threads](./copy_threads.md)
|
||||
- [How to check status of your threads](./check_thread_status.md)
|
||||
|
||||
@@ -4,20 +4,44 @@ This guide assumes knowledge of what double-texting is, which you can learn abou
|
||||
|
||||
The guide covers the `interrupt` option for double texting, which interrupts the prior run of the graph and starts a new one with the double-text. This option does not delete the first run, but rather keeps it in the database but sets its status to `interrupted`. Below is a quick example of using the `interrupt` option.
|
||||
|
||||
First, we will define a quick helper function for printing out JS model outputs (you can skip this if using Python):
|
||||
First, we will define a quick helper function for printing out JS and CURL model outputs (you can skip this if using Python):
|
||||
|
||||
```js
|
||||
function prettyPrint(m) {
|
||||
const padded = " " + m['type'] + " ";
|
||||
const sepLen = Math.floor((80 - padded.length) / 2);
|
||||
const sep = "=".repeat(sepLen);
|
||||
const secondSep = sep + (padded.length % 2 ? "=" : "");
|
||||
|
||||
console.log(`${sep}${padded}${secondSep}`);
|
||||
console.log("\n\n");
|
||||
console.log(m.content);
|
||||
}
|
||||
```
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
function prettyPrint(m) {
|
||||
const padded = " " + m['type'] + " ";
|
||||
const sepLen = Math.floor((80 - padded.length) / 2);
|
||||
const sep = "=".repeat(sepLen);
|
||||
const secondSep = sep + (padded.length % 2 ? "=" : "");
|
||||
|
||||
console.log(`${sep}${padded}${secondSep}`);
|
||||
console.log("\n\n");
|
||||
console.log(m.content);
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
# PLACE THIS IN A FILE CALLED pretty_print.sh
|
||||
pretty_print() {
|
||||
local type="$1"
|
||||
local content="$2"
|
||||
local padded=" $type "
|
||||
local total_width=80
|
||||
local sep_len=$(( (total_width - ${#padded}) / 2 ))
|
||||
local sep=$(printf '=%.0s' $(eval "echo {1.."${sep_len}"}"))
|
||||
local second_sep=$sep
|
||||
if (( (total_width - ${#padded}) % 2 )); then
|
||||
second_sep="${second_sep}="
|
||||
fi
|
||||
|
||||
echo "${sep}${padded}${second_sep}"
|
||||
echo
|
||||
echo "$content"
|
||||
}
|
||||
```
|
||||
|
||||
Now, let's import our required packages and instantiate our client, assistant, and thread.
|
||||
|
||||
@@ -30,6 +54,7 @@ Now, let's import our required packages and instantiate our client, assistant, a
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
@@ -40,10 +65,19 @@ Now, let's import our required packages and instantiate our client, assistant, a
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantId = "agent";
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
Now we can start our two runs and join the second on euntil it has completed:
|
||||
|
||||
=== "Python"
|
||||
@@ -90,6 +124,26 @@ Now we can start our two runs and join the second on euntil it has completed:
|
||||
await client.runs.join(thread["thread_id"], run["run_id"]);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOY<ENT_URL>>/threads/<THREAD_ID>/runs \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in sf?\"}]},
|
||||
}" && sleep 2 && curl --request POST \
|
||||
--url <DEPLOY<ENT_URL>>/threads/<THREAD_ID>/runs \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in nyc?\"}]},
|
||||
\"multitask_strategy\": \"interrupt\"
|
||||
}" && curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/<RUN_ID>/join
|
||||
```
|
||||
|
||||
We can see that the thread has partial data from the first run + data from the second run
|
||||
|
||||
|
||||
@@ -112,12 +166,24 @@ We can see that the thread has partial data from the first run + data from the s
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
source pretty_print.sh && curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state | \
|
||||
jq -c '.values.messages[]' | while read -r element; do
|
||||
type=$(echo "$element" | jq -r '.type')
|
||||
content=$(echo "$element" | jq -r '.content | if type == "array" then tostring else . end')
|
||||
pretty_print "$type" "$content"
|
||||
done
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
================================[1m Human Message [0m=================================
|
||||
================================ Human Message =================================
|
||||
|
||||
what's the weather in sf?
|
||||
==================================[1m Ai Message [0m==================================
|
||||
================================== Ai Message ==================================
|
||||
|
||||
[{'id': 'toolu_01MjNtVJwEcpujRGrf3x6Pih', 'input': {'query': 'weather in san francisco'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]
|
||||
Tool Calls:
|
||||
@@ -125,14 +191,14 @@ Output:
|
||||
Call ID: toolu_01MjNtVJwEcpujRGrf3x6Pih
|
||||
Args:
|
||||
query: weather in san francisco
|
||||
=================================[1m Tool Message [0m=================================
|
||||
================================= Tool Message =================================
|
||||
Name: tavily_search_results_json
|
||||
|
||||
[{"url": "https://www.wunderground.com/hourly/us/ca/san-francisco/KCASANFR2002/date/2024-6-18", "content": "High 64F. Winds W at 10 to 20 mph. A few clouds from time to time. Low 49F. Winds W at 10 to 20 mph. Temp. San Francisco Weather Forecasts. Weather Underground provides local & long-range weather ..."}]
|
||||
================================[1m Human Message [0m=================================
|
||||
================================ Human Message =================================
|
||||
|
||||
what's the weather in nyc?
|
||||
==================================[1m Ai Message [0m==================================
|
||||
================================== Ai Message ==================================
|
||||
|
||||
[{'id': 'toolu_01KtE1m1ifPLQAx4fQLyZL9Q', 'input': {'query': 'weather in new york city'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]
|
||||
Tool Calls:
|
||||
@@ -140,11 +206,11 @@ Output:
|
||||
Call ID: toolu_01KtE1m1ifPLQAx4fQLyZL9Q
|
||||
Args:
|
||||
query: weather in new york city
|
||||
=================================[1m Tool Message [0m=================================
|
||||
================================= Tool Message =================================
|
||||
Name: tavily_search_results_json
|
||||
|
||||
[{"url": "https://www.accuweather.com/en/us/new-york/10021/june-weather/349727", "content": "Get the monthly weather forecast for New York, NY, including daily high/low, historical averages, to help you plan ahead."}]
|
||||
==================================[1m Ai Message [0m==================================
|
||||
================================== Ai Message ==================================
|
||||
|
||||
The search results provide weather forecasts and information for New York City. Based on the top result from AccuWeather, here are some key details about the weather in NYC:
|
||||
|
||||
|
||||
@@ -4,20 +4,44 @@ This guide assumes knowledge of what double-texting is, which you can learn abou
|
||||
|
||||
The guide covers the `reject` option for double texting, which rejects the new run of the graph by throwing an error and continues with the original run until completion. Below is a quick example of using the `reject` option.
|
||||
|
||||
First, we will define a quick helper function for printing out JS model outputs (you can skip this if using Python):
|
||||
First, we will define a quick helper function for printing out JS and CURL model outputs (you can skip this if using Python):
|
||||
|
||||
```js
|
||||
function prettyPrint(m) {
|
||||
const padded = " " + m['type'] + " ";
|
||||
const sepLen = Math.floor((80 - padded.length) / 2);
|
||||
const sep = "=".repeat(sepLen);
|
||||
const secondSep = sep + (padded.length % 2 ? "=" : "");
|
||||
|
||||
console.log(`${sep}${padded}${secondSep}`);
|
||||
console.log("\n\n");
|
||||
console.log(m.content);
|
||||
}
|
||||
```
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
function prettyPrint(m) {
|
||||
const padded = " " + m['type'] + " ";
|
||||
const sepLen = Math.floor((80 - padded.length) / 2);
|
||||
const sep = "=".repeat(sepLen);
|
||||
const secondSep = sep + (padded.length % 2 ? "=" : "");
|
||||
|
||||
console.log(`${sep}${padded}${secondSep}`);
|
||||
console.log("\n\n");
|
||||
console.log(m.content);
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
# PLACE THIS IN A FILE CALLED pretty_print.sh
|
||||
pretty_print() {
|
||||
local type="$1"
|
||||
local content="$2"
|
||||
local padded=" $type "
|
||||
local total_width=80
|
||||
local sep_len=$(( (total_width - ${#padded}) / 2 ))
|
||||
local sep=$(printf '=%.0s' $(eval "echo {1.."${sep_len}"}"))
|
||||
local second_sep=$sep
|
||||
if (( (total_width - ${#padded}) % 2 )); then
|
||||
second_sep="${second_sep}="
|
||||
fi
|
||||
|
||||
echo "${sep}${padded}${second_sep}"
|
||||
echo
|
||||
echo "$content"
|
||||
}
|
||||
```
|
||||
|
||||
Now, let's import our required packages and instantiate our client, assistant, and thread.
|
||||
|
||||
@@ -29,6 +53,7 @@ Now, let's import our required packages and instantiate our client, assistant, a
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
@@ -39,10 +64,19 @@ Now, let's import our required packages and instantiate our client, assistant, a
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantId = "agent";
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
Now we can run a thread and try to run a second one with the "reject" option, which should fail since we have already started a run:
|
||||
|
||||
|
||||
@@ -90,6 +124,27 @@ Now we can run a thread and try to run a second one with the "reject" option, wh
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOY<ENT_URL>>/threads/<THREAD_ID>/runs \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in sf?\"}]},
|
||||
}" && curl --request POST \
|
||||
--url <DEPLOY<ENT_URL>>/threads/<THREAD_ID>/runs \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in nyc?\"}]},
|
||||
\"multitask_strategy\": \"reject\"
|
||||
}" || { echo "Failed to start concurrent run"; echo "Error: $?" >&2; }
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
Failed to start concurrent run Client error '409 Conflict' for url 'http://localhost:8123/threads/f9e7088b-8028-4e5c-88d2-9cc9a2870e50/runs'
|
||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409
|
||||
|
||||
@@ -120,12 +175,25 @@ We can verify that the original thread finished executing:
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
source pretty_print.sh && curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/<RUN_ID>/join && \
|
||||
curl --request GET --url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state | \
|
||||
jq -c '.values.messages[]' | while read -r element; do
|
||||
type=$(echo "$element" | jq -r '.type')
|
||||
content=$(echo "$element" | jq -r '.content | if type == "array" then tostring else . end')
|
||||
pretty_print "$type" "$content"
|
||||
done
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
================================[1m Human Message [0m=================================
|
||||
================================ Human Message =================================
|
||||
|
||||
what's the weather in sf?
|
||||
==================================[1m Ai Message [0m==================================
|
||||
================================== Ai Message ==================================
|
||||
|
||||
[{'id': 'toolu_01CyewEifV2Kmi7EFKHbMDr1', 'input': {'query': 'weather in san francisco'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]
|
||||
Tool Calls:
|
||||
@@ -133,11 +201,11 @@ Output:
|
||||
Call ID: toolu_01CyewEifV2Kmi7EFKHbMDr1
|
||||
Args:
|
||||
query: weather in san francisco
|
||||
=================================[1m Tool Message [0m=================================
|
||||
================================= Tool Message =================================
|
||||
Name: tavily_search_results_json
|
||||
|
||||
[{"url": "https://www.accuweather.com/en/us/san-francisco/94103/june-weather/347629", "content": "Get the monthly weather forecast for San Francisco, CA, including daily high/low, historical averages, to help you plan ahead."}]
|
||||
==================================[1m Ai Message [0m==================================
|
||||
================================== Ai Message ==================================
|
||||
|
||||
According to the search results from Tavily, the current weather in San Francisco is:
|
||||
|
||||
|
||||
@@ -4,20 +4,44 @@ This guide assumes knowledge of what double-texting is, which you can learn abou
|
||||
|
||||
The guide covers the `rollback` option for double texting, which interrupts the prior run of the graph and starts a new one with the double-text. This option is very similar to the `interrupt` option, but in this case the first run is completely deleted from the database and cannot be restarted. Below is a quick example of using the `rollback` option.
|
||||
|
||||
First, we will define a quick helper function for printing out JS model outputs (you can skip this if using Python):
|
||||
First, we will define a quick helper function for printing out JS and CURL model outputs (you can skip this if using Python):
|
||||
|
||||
```js
|
||||
function prettyPrint(m) {
|
||||
const padded = " " + m['type'] + " ";
|
||||
const sepLen = Math.floor((80 - padded.length) / 2);
|
||||
const sep = "=".repeat(sepLen);
|
||||
const secondSep = sep + (padded.length % 2 ? "=" : "");
|
||||
|
||||
console.log(`${sep}${padded}${secondSep}`);
|
||||
console.log("\n\n");
|
||||
console.log(m.content);
|
||||
}
|
||||
```
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
function prettyPrint(m) {
|
||||
const padded = " " + m['type'] + " ";
|
||||
const sepLen = Math.floor((80 - padded.length) / 2);
|
||||
const sep = "=".repeat(sepLen);
|
||||
const secondSep = sep + (padded.length % 2 ? "=" : "");
|
||||
|
||||
console.log(`${sep}${padded}${secondSep}`);
|
||||
console.log("\n\n");
|
||||
console.log(m.content);
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
# PLACE THIS IN A FILE CALLED pretty_print.sh
|
||||
pretty_print() {
|
||||
local type="$1"
|
||||
local content="$2"
|
||||
local padded=" $type "
|
||||
local total_width=80
|
||||
local sep_len=$(( (total_width - ${#padded}) / 2 ))
|
||||
local sep=$(printf '=%.0s' $(eval "echo {1.."${sep_len}"}"))
|
||||
local second_sep=$sep
|
||||
if (( (total_width - ${#padded}) % 2 )); then
|
||||
second_sep="${second_sep}="
|
||||
fi
|
||||
|
||||
echo "${sep}${padded}${second_sep}"
|
||||
echo
|
||||
echo "$content"
|
||||
}
|
||||
```
|
||||
|
||||
Now, let's import our required packages and instantiate our client, assistant, and thread.
|
||||
|
||||
@@ -31,6 +55,7 @@ Now, let's import our required packages and instantiate our client, assistant, a
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
@@ -41,10 +66,19 @@ Now, let's import our required packages and instantiate our client, assistant, a
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantId = "agent";
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
Now let's run a thread with the multitask parameter set to "rollback":
|
||||
|
||||
=== "Python"
|
||||
@@ -91,6 +125,26 @@ Now let's run a thread with the multitask parameter set to "rollback":
|
||||
await client.runs.join(thread["thread_id"], run["run_id"]);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOY<ENT_URL>>/threads/<THREAD_ID>/runs \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in sf?\"}]},
|
||||
}" && sleep 2 && curl --request POST \
|
||||
--url <DEPLOY<ENT_URL>>/threads/<THREAD_ID>/runs \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in nyc?\"}]},
|
||||
\"multitask_strategy\": \"rollback\"
|
||||
}" && curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/<RUN_ID>/join
|
||||
```
|
||||
|
||||
We can see that the thread has data only from the second run
|
||||
|
||||
=== "Python"
|
||||
@@ -112,12 +166,24 @@ We can see that the thread has data only from the second run
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
source pretty_print.sh && curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/state | \
|
||||
jq -c '.values.messages[]' | while read -r element; do
|
||||
type=$(echo "$element" | jq -r '.type')
|
||||
content=$(echo "$element" | jq -r '.content | if type == "array" then tostring else . end')
|
||||
pretty_print "$type" "$content"
|
||||
done
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
================================[1m Human Message [0m=================================
|
||||
================================ Human Message =================================
|
||||
|
||||
what's the weather in nyc?
|
||||
==================================[1m Ai Message [0m==================================
|
||||
================================== Ai Message ==================================
|
||||
|
||||
[{'id': 'toolu_01JzPqefao1gxwajHQ3Yh3JD', 'input': {'query': 'weather in nyc'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]
|
||||
Tool Calls:
|
||||
@@ -125,11 +191,11 @@ Output:
|
||||
Call ID: toolu_01JzPqefao1gxwajHQ3Yh3JD
|
||||
Args:
|
||||
query: weather in nyc
|
||||
=================================[1m Tool Message [0m=================================
|
||||
================================= Tool Message =================================
|
||||
Name: tavily_search_results_json
|
||||
|
||||
[{"url": "https://www.weatherapi.com/", "content": "{'location': {'name': 'New York', 'region': 'New York', 'country': 'United States of America', 'lat': 40.71, 'lon': -74.01, 'tz_id': 'America/New_York', 'localtime_epoch': 1718734479, 'localtime': '2024-06-18 14:14'}, 'current': {'last_updated_epoch': 1718733600, 'last_updated': '2024-06-18 14:00', 'temp_c': 29.4, 'temp_f': 84.9, 'is_day': 1, 'condition': {'text': 'Sunny', 'icon': '//cdn.weatherapi.com/weather/64x64/day/113.png', 'code': 1000}, 'wind_mph': 2.2, 'wind_kph': 3.6, 'wind_degree': 158, 'wind_dir': 'SSE', 'pressure_mb': 1025.0, 'pressure_in': 30.26, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 63, 'cloud': 0, 'feelslike_c': 31.3, 'feelslike_f': 88.3, 'windchill_c': 28.3, 'windchill_f': 82.9, 'heatindex_c': 29.6, 'heatindex_f': 85.3, 'dewpoint_c': 18.4, 'dewpoint_f': 65.2, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 7.0, 'gust_mph': 16.5, 'gust_kph': 26.5}}"}]
|
||||
==================================[1m Ai Message [0m==================================
|
||||
================================== Ai Message ==================================
|
||||
|
||||
The weather API results show that the current weather in New York City is sunny with a temperature of around 85°F (29°C). The wind is light at around 2-3 mph from the south-southeast. Overall it looks like a nice sunny summer day in NYC.
|
||||
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
# How to run multiple agents on the same thread
|
||||
|
||||
In LangGraph Cloud, a thread is not explicitly associated with a particular agent.
|
||||
This means that you can run multiple agents on the same thread, which allows a different agent to continue from an initial agent's progress.
|
||||
|
||||
In this example, we will create two agents and then call them both on the same thread.
|
||||
You'll see that the second agent will respond using information from the [checkpoint](https://langchain-ai.github.io/langgraph/concepts/low_level/#checkpointer-state) generated in the thread by the first agent as context.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
|
||||
openai_assistant = await client.assistants.create(
|
||||
graph_id="agent", config={"configurable": {"model_name": "openai"}}
|
||||
)
|
||||
|
||||
# There should always be a default assistant with no configuration
|
||||
assistants = await client.assistants.search()
|
||||
default_assistant = [a for a in assistants if not a["config"]][0]
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
|
||||
const openAIAssistant = await client.assistants.create(
|
||||
{ graphId: "agent", config: {"configurable": {"model_name": "openai"}}}
|
||||
);
|
||||
|
||||
const assistants = await client.assistants.search();
|
||||
const defaultAssistant = assistants.find(a => !a.config);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/assistants \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"graph_id": "agent",
|
||||
"config": { "configurable": { "model_name": "openai" } }
|
||||
}' && \
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/assistants/search \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"limit": 10,
|
||||
"offset": 0
|
||||
}' | jq -c 'map(select(.config == null or .config == {})) | .[0]'
|
||||
```
|
||||
|
||||
We can see that these agents are different:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
print(openai_assistant)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
console.log(openAIAssistant);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/assistants/<OPENAI_ASSISTANT_ID>
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{
|
||||
"assistant_id": "db87f39d-b2b1-4da8-ac65-cf81beb3c766",
|
||||
"graph_id": "agent",
|
||||
"created_at": "2024-08-30T21:18:51.850581+00:00",
|
||||
"updated_at": "2024-08-30T21:18:51.850581+00:00",
|
||||
"config": {
|
||||
"configurable": {
|
||||
"model_name": "openai"
|
||||
}
|
||||
},
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
print(default_assistant)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
console.log(defaultAssistant);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request GET \
|
||||
--url <DEPLOYMENT_URL>/assistants/<DEFAULT_ASSISTANT_ID>
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{
|
||||
"assistant_id": "fe096781-5601-53d2-b2f6-0d3403f7e9ca",
|
||||
"graph_id": "agent",
|
||||
"created_at": "2024-08-08T22:45:24.562906+00:00",
|
||||
"updated_at": "2024-08-08T22:45:24.562906+00:00",
|
||||
"config": {},
|
||||
"metadata": {
|
||||
"created_by": "system"
|
||||
}
|
||||
}
|
||||
|
||||
We can now run the OpenAI assistant on the thread first.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
thread = await client.threads.create()
|
||||
input = {"messages": [{"role": "user", "content": "who made you?"}]}
|
||||
async for event in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
openai_assistant["assistant_id"],
|
||||
input=input,
|
||||
stream_mode="updates",
|
||||
):
|
||||
print(f"Receiving event of type: {event.event}")
|
||||
print(event.data)
|
||||
print("\n\n")
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const thread = await client.threads.create();
|
||||
let input = {"messages": [{"role": "user", "content": "who made you?"}]}
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
openAIAssistant["assistant_id"],
|
||||
{
|
||||
input,
|
||||
streamMode: "updates"
|
||||
}
|
||||
);
|
||||
for await (const event of streamResponse) {
|
||||
console.log(`Receiving event of type: ${event.event}`);
|
||||
console.log(event.data);
|
||||
console.log("\n\n");
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
thread_id=$(curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{}' | jq -r '.thread_id') && \
|
||||
curl --request POST \
|
||||
--url "<DEPLOYMENT_URL>/threads/${thread_id}/runs/stream" \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"assistant_id": <OPENAI_ASSISTANT_ID>,
|
||||
"input": {
|
||||
"messages": [
|
||||
{
|
||||
"role": "human",
|
||||
"content": "who made you?"
|
||||
}
|
||||
]
|
||||
},
|
||||
"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\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
Receiving event of type: metadata
|
||||
{'run_id': '1ef671c5-fb83-6e70-b698-44dba2d9213e'}
|
||||
|
||||
|
||||
Receiving event of type: updates
|
||||
{'agent': {'messages': [{'content': 'I was created by OpenAI, a research organization focused on developing and advancing artificial intelligence technology.', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_157b3831f5'}, 'type': 'ai', 'name': None, 'id': 'run-f5735b86-b80d-4c71-8dc3-4782b5a9c7c8', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
|
||||
|
||||
Now, we can run it on the default assistant and see that this second assistant is aware of the initial question, and can answer the question, "and you?":
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
input = {"messages": [{"role": "user", "content": "and you?"}]}
|
||||
async for event in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
default_assistant["assistant_id"],
|
||||
input=input,
|
||||
stream_mode="updates",
|
||||
):
|
||||
print(f"Receiving event of type: {event.event}")
|
||||
print(event.data)
|
||||
print("\n\n")
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
let input = {"messages": [{"role": "user", "content": "and you?"}]}
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
defaultAssistant["assistant_id"],
|
||||
{
|
||||
input,
|
||||
streamMode: "updates"
|
||||
}
|
||||
);
|
||||
for await (const event of streamResponse) {
|
||||
console.log(`Receiving event of type: ${event.event}`);
|
||||
console.log(event.data);
|
||||
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": <DEFAULT_ASSISTANT_ID>,
|
||||
"input": {
|
||||
"messages": [
|
||||
{
|
||||
"role": "human",
|
||||
"content": "and you?"
|
||||
}
|
||||
]
|
||||
},
|
||||
"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\n"
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
Receiving event of type: metadata
|
||||
{'run_id': '1ef6722d-80b3-6fbb-9324-253796b1cd13'}
|
||||
|
||||
|
||||
Receiving event of type: updates
|
||||
{'agent': {'messages': [{'content': [{'text': 'I am an artificial intelligence created by Anthropic, not by OpenAI. I should not have stated that OpenAI created me, as that is incorrect. Anthropic is the company that developed and trained me using advanced language models and AI technology. I will be more careful about providing accurate information regarding my origins in the future.', 'type': 'text', 'index': 0}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-ebaacf62-9dd9-4165-9535-db432e4793ec', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 302, 'output_tokens': 72, 'total_tokens': 374}}]}}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
# Stateless Runs
|
||||
|
||||
Most of the time, you provide a `thread_id` to your client when you run your graph in order to keep track of prior runs through the persistent state implemented in LangGraph Cloud. However, if you don't need to persist the runs you don't need to use the built in persistent state and can create stateless runs.
|
||||
|
||||
## Setup
|
||||
|
||||
First, let's setup our client:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
# create thread
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantId = "agent";
|
||||
// create thread
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/assistants/search \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"limit": 10,
|
||||
"offset": 0
|
||||
}' | jq -c 'map(select(.config == null or .config == {})) | .[0].graph_id' && \
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{}'
|
||||
```
|
||||
|
||||
## Stateless streaming
|
||||
|
||||
We can stream the results of a stateless run in an almost identical fashion to how we stream from a run with the state attribute, but instead of passing a value to the `thread_id` parameter, we pass `None`:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
input = {
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello! My name is Bagatur and I am 26 years old."}
|
||||
]
|
||||
}
|
||||
|
||||
async for chunk in client.runs.stream(
|
||||
# Don't pass in a thread_id and the stream will be stateless
|
||||
None,
|
||||
assistant_id,
|
||||
input=input,
|
||||
stream_mode="updates",
|
||||
):
|
||||
if chunk.data and "run_id" not in chunk.data:
|
||||
print(chunk.data)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
let input = {
|
||||
messages: [
|
||||
{ role: "user", content: "Hello! My name is Bagatur and I am 26 years old." }
|
||||
]
|
||||
};
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
// Don't pass in a thread_id and the stream will be stateless
|
||||
null,
|
||||
assistantId,
|
||||
{
|
||||
input,
|
||||
streamMode: "updates"
|
||||
}
|
||||
);
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.data && !("run_id" in chunk.data)) {
|
||||
console.log(chunk.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "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\": \"Hello! My name is Bagatur and I am 26 years old.\"}]},
|
||||
\"stream_mode\": [
|
||||
\"updates\"
|
||||
]
|
||||
}" | jq -c 'select(.data and (.data | has("run_id") | not)) | .data'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{'agent': {'messages': [{'content': "Hello Bagatur! It's nice to meet you. Thank you for introducing yourself and sharing your age. Is there anything specific you'd like to know or discuss? I'm here to help with any questions or topics you're interested in.", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-489ec573-1645-4ce2-a3b8-91b391d50a71', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
|
||||
|
||||
## Waiting for stateless results
|
||||
|
||||
In addition to streaming, you can also wait for a stateless result by using the `.wait` function like follows:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
stateless_run_result = await client.runs.wait(
|
||||
None,
|
||||
assistant_id,
|
||||
input=input,
|
||||
)
|
||||
print(stateless_run_result)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
let statelessRunResult = await client.runs.wait(
|
||||
null,
|
||||
assistantId,
|
||||
{ input: input }
|
||||
);
|
||||
console.log(statelessRunResult);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/runs/runs/wait \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"assistant_id": <ASSISTANT_IDD>,
|
||||
}'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{
|
||||
'messages': [
|
||||
{
|
||||
'content': 'Hello! My name is Bagatur and I am 26 years old.',
|
||||
'additional_kwargs': {},
|
||||
'response_metadata': {},
|
||||
'type': 'human',
|
||||
'name': None,
|
||||
'id': '5e088543-62c2-43de-9d95-6086ad7f8b48',
|
||||
'example': False}
|
||||
,
|
||||
{
|
||||
'content': "Hello Bagatur! It's nice to meet you. Thank you for introducing yourself and sharing your age. Is there anything specific you'd like to know or discuss? I'm here to help with any questions or topics you'd like to explore.",
|
||||
'additional_kwargs': {},
|
||||
'response_metadata': {},
|
||||
'type': 'ai',
|
||||
'name': None,
|
||||
'id': 'run-d6361e8d-4d4c-45bd-ba47-39520257f773',
|
||||
'example': False,
|
||||
'tool_calls': [],
|
||||
'invalid_tool_calls': [],
|
||||
'usage_metadata': None
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -14,6 +14,8 @@ First let's set up our client and thread:
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
# create thread
|
||||
thread = await client.threads.create()
|
||||
print(thread)
|
||||
@@ -25,18 +27,33 @@ First let's set up our client and thread:
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantID = "agent";
|
||||
// create thread
|
||||
const thread = await client.threads.create();
|
||||
console.log(thread)
|
||||
console.log(thread);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
|
||||
Output:
|
||||
|
||||
{'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3',
|
||||
'created_at': '2024-06-21T22:10:27.696862+00:00',
|
||||
'updated_at': '2024-06-21T22:10:27.696862+00:00',
|
||||
'metadata': {}}
|
||||
{
|
||||
'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3',
|
||||
'created_at': '2024-06-21T22:10:27.696862+00:00',
|
||||
'updated_at': '2024-06-21T22:10:27.696862+00:00',
|
||||
'metadata': {},
|
||||
'status': 'idle',
|
||||
'config': {},
|
||||
'values': None
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -56,7 +73,7 @@ Output:
|
||||
# stream debug
|
||||
async for chunk in client.runs.stream(
|
||||
thread_id=thread["thread_id"],
|
||||
assistant_id="agent",
|
||||
assistant_id=assistant_id,
|
||||
input=input,
|
||||
stream_mode="debug",
|
||||
):
|
||||
@@ -70,30 +87,67 @@ Output:
|
||||
```js
|
||||
// create input
|
||||
const input = {
|
||||
"messages": [
|
||||
{
|
||||
"role": "human",
|
||||
"content": "What's the weather in SF?",
|
||||
}
|
||||
]
|
||||
}
|
||||
messages: [
|
||||
{
|
||||
role: "human",
|
||||
content: "What's the weather in SF?",
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// stream debug
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
"agent",
|
||||
assistantID,
|
||||
{
|
||||
input,
|
||||
streamMode: "debug"
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of streamResponse) {
|
||||
console.log(f"Receiving new event of type: {chunk.event}...")
|
||||
console.log(chunk.data)
|
||||
console.log("\n\n")
|
||||
console.log(`Receiving new event of type: ${chunk.event}...`);
|
||||
console.log(chunk.data);
|
||||
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?\"}]},
|
||||
\"stream_mode\": [
|
||||
\"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...
|
||||
|
||||
@@ -8,6 +8,8 @@ This guide covers how to stream events from your graph (`stream_mode="events"`).
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
# create thread
|
||||
thread = await client.threads.create()
|
||||
print(thread)
|
||||
@@ -19,9 +21,11 @@ This guide covers how to stream events from your graph (`stream_mode="events"`).
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantID = "agent";
|
||||
// create thread
|
||||
const thread = await client.threads.create();
|
||||
console.log(thread)
|
||||
console.log(thread);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
@@ -35,12 +39,15 @@ This guide covers how to stream events from your graph (`stream_mode="events"`).
|
||||
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': {},
|
||||
'status': 'idle',
|
||||
'config': {}}
|
||||
{
|
||||
'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': {},
|
||||
'status': 'idle',
|
||||
'config': {},
|
||||
'values': None
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -63,7 +70,7 @@ Streaming events produces responses containing an `event` key (in addition to ot
|
||||
# stream events
|
||||
async for chunk in client.runs.stream(
|
||||
thread_id=thread["thread_id"],
|
||||
assistant_id="agent",
|
||||
assistant_id=assistant_id,
|
||||
input=input,
|
||||
stream_mode="events",
|
||||
):
|
||||
@@ -77,27 +84,27 @@ Streaming events produces responses containing an `event` key (in addition to ot
|
||||
```js
|
||||
// create input
|
||||
const input = {
|
||||
"messages": [
|
||||
{
|
||||
"role": "human",
|
||||
"content": "What's the weather in SF?",
|
||||
}
|
||||
]
|
||||
"messages": [
|
||||
{
|
||||
"role": "human",
|
||||
"content": "What's the weather in SF?",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// stream events
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
"agent",
|
||||
assistantID,
|
||||
{
|
||||
input,
|
||||
streamMode: "events"
|
||||
}
|
||||
);
|
||||
for await (const chunk of streamResponse) {
|
||||
console.log(f"Receiving new event of type: {chunk.event}...")
|
||||
console.log(chunk.data)
|
||||
console.log("\n\n")
|
||||
console.log(`Receiving new event of type: ${chunk.event}...`);
|
||||
console.log(chunk.data);
|
||||
console.log("\n\n");
|
||||
}
|
||||
```
|
||||
|
||||
@@ -280,9 +287,6 @@ Output:
|
||||
|
||||
Receiving new event of type: end...
|
||||
None
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Token-by-Token Streaming
|
||||
@@ -297,7 +301,7 @@ Token-by-token streaming can be implemented with the `events` streaming mode. Th
|
||||
# stream token-by-token
|
||||
async for chunk in client.runs.stream(
|
||||
thread_id=thread["thread_id"],
|
||||
assistant_id="agent",
|
||||
assistant_id=assistant_id,
|
||||
input=input,
|
||||
stream_mode="events",
|
||||
):
|
||||
@@ -318,7 +322,7 @@ Token-by-token streaming can be implemented with the `events` streaming mode. Th
|
||||
// stream events
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
"agent",
|
||||
assistantID,
|
||||
{
|
||||
input,
|
||||
streamMode: "events"
|
||||
|
||||
@@ -22,10 +22,10 @@ E.g., the state should look something like:
|
||||
import { Annotation, messagesStateReducer } from "@langchain/langgraph";
|
||||
|
||||
export const StateAnnotation = Annotation.Root({
|
||||
messages: Annotation<BaseMessage[]>({
|
||||
messages: Annotation<BaseMessage[]>({
|
||||
reducer: messagesStateReducer,
|
||||
default: () => [],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
@@ -46,6 +46,8 @@ First let's set up our client and thread:
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
# create thread
|
||||
thread = await client.threads.create()
|
||||
print(thread)
|
||||
@@ -57,9 +59,11 @@ First let's set up our client and thread:
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantID = "agent";
|
||||
// create thread
|
||||
const thread = await client.threads.create();
|
||||
console.log(thread)
|
||||
console.log(thread);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
@@ -72,12 +76,15 @@ First let's set up our client and thread:
|
||||
|
||||
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': {},
|
||||
'status': 'idle',
|
||||
'config': {}}
|
||||
{
|
||||
'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': {},
|
||||
'status': 'idle',
|
||||
'config': {},
|
||||
'values': None
|
||||
}
|
||||
|
||||
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`)
|
||||
|
||||
@@ -182,7 +189,7 @@ Now we can stream by messages, which will return complete messages (at the end o
|
||||
|
||||
async for event in client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistant_id="agent",
|
||||
assistant_id=assistant_id,
|
||||
input=input,
|
||||
config=config,
|
||||
stream_mode="messages",
|
||||
@@ -221,24 +228,25 @@ Now we can stream by messages, which will return complete messages (at the end o
|
||||
|
||||
```js
|
||||
const input = {
|
||||
"messages": [
|
||||
messages: [
|
||||
{
|
||||
"role": "human",
|
||||
"content": "What's the weather in sf",
|
||||
role: "human",
|
||||
content: "What's the weather in sf",
|
||||
}
|
||||
]
|
||||
}
|
||||
const config = {"configurable": {"model_name": "openai"}}
|
||||
};
|
||||
const config = { configurable: { model_name: "openai" } };
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
"agent",
|
||||
assistantID,
|
||||
{
|
||||
input,
|
||||
config,
|
||||
streamMode: "messages"
|
||||
}
|
||||
);
|
||||
|
||||
for await (const event of streamResponse) {
|
||||
if (event.event === "metadata") {
|
||||
console.log(`Metadata: Run ID - ${event.data.run_id}`);
|
||||
|
||||
@@ -10,6 +10,8 @@ First let's set up our client and thread:
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
# create thread
|
||||
thread = await client.threads.create()
|
||||
print(thread)
|
||||
@@ -21,9 +23,11 @@ First let's set up our client and thread:
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantID = "agent";
|
||||
// create thread
|
||||
const thread = await client.threads.create();
|
||||
console.log(thread)
|
||||
console.log(thread);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
@@ -36,12 +40,15 @@ First let's set up our client and thread:
|
||||
|
||||
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': {},
|
||||
'status': 'idle',
|
||||
'config': {}}
|
||||
{
|
||||
'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': {},
|
||||
'status': 'idle',
|
||||
'config': {},
|
||||
'values': None
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
@@ -61,7 +68,7 @@ When configuring multiple streaming modes for a run, responses for each respecti
|
||||
# stream events with multiple streaming modes
|
||||
async for chunk in client.runs.stream(
|
||||
thread_id=thread["thread_id"],
|
||||
assistant_id="agent",
|
||||
assistant_id=assistant_id,
|
||||
input=input,
|
||||
stream_mode=["messages", "events", "debug"],
|
||||
):
|
||||
@@ -75,27 +82,27 @@ When configuring multiple streaming modes for a run, responses for each respecti
|
||||
```js
|
||||
// create input
|
||||
const input = {
|
||||
"messages": [
|
||||
messages: [
|
||||
{
|
||||
"role": "human",
|
||||
"content": "What's the weather in SF?",
|
||||
role: "human",
|
||||
content: "What's the weather in SF?",
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// stream events with multiple streaming modes
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
"agent",
|
||||
assistantID,
|
||||
{
|
||||
input,
|
||||
streamMode: ["messages", "events", "debug"]
|
||||
}
|
||||
);
|
||||
for await (const chunk of streamResponse) {
|
||||
console.log(f"Receiving new event of type: {chunk.event}...")
|
||||
console.log(chunk.data)
|
||||
console.log("\n\n")
|
||||
console.log(`Receiving new event of type: ${chunk.event}...`);
|
||||
console.log(chunk.data);
|
||||
console.log("\n\n");
|
||||
}
|
||||
```
|
||||
|
||||
@@ -482,5 +489,4 @@ Output:
|
||||
None
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# How to stream state updates of your graph
|
||||
|
||||
This guide covers how to use `stream_mode="updates"` for your graph, which will stream the updates to the graph state that are made after each node is executed. This differs from using `stream_mode="values"`: instead of streaming the entire value of the state at each superstep, it only streams the updates from each of the nodes that made an update to the state at that superstep. Read [this conceptual guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#stream-and-astream) to learn more.```
|
||||
This guide covers how to use `stream_mode="updates"` for your graph, which will stream the updates to the graph state that are made after each node is executed. This differs from using `stream_mode="values"`: instead of streaming the entire value of the state at each superstep, it only streams the updates from each of the nodes that made an update to the state at that superstep. Read [this conceptual guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#stream-and-astream) to learn more.
|
||||
|
||||
First let's set up our client and thread:
|
||||
|
||||
@@ -23,7 +23,7 @@ First let's set up our client and thread:
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// create thread
|
||||
const thread = await client.threads.create();
|
||||
console.log(thread)
|
||||
console.log(thread);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
@@ -36,12 +36,15 @@ First let's set up our client and thread:
|
||||
|
||||
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': {},
|
||||
'status': 'idle',
|
||||
'config': {}}
|
||||
{
|
||||
'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': {},
|
||||
'status': 'idle',
|
||||
'config': {},
|
||||
'values': None
|
||||
}
|
||||
|
||||
Now we can stream by updates, which outputs updates made to the state by each node after it has executed:
|
||||
|
||||
@@ -72,13 +75,13 @@ Now we can stream by updates, which outputs updates made to the state by each no
|
||||
|
||||
```js
|
||||
const input = {
|
||||
"messages": [
|
||||
messages: [
|
||||
{
|
||||
"role": "human",
|
||||
"content": "What's the weather in la",
|
||||
role: "human",
|
||||
content: "What's the weather in la"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
@@ -88,10 +91,11 @@ Now we can stream by updates, which outputs updates made to the state by each no
|
||||
streamMode: "updates"
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of streamResponse) {
|
||||
console.log(f"Receiving new event of type: {chunk.event}...")
|
||||
console.log(chunk.data)
|
||||
console.log("\n\n")
|
||||
console.log(`Receiving new event of type: ${chunk.event}...`);
|
||||
console.log(chunk.data);
|
||||
console.log("\n\n");
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# How to stream full state of your graph
|
||||
|
||||
This guide covers how to use `stream_mode="values"`, which streams the value of the state at each superstep. This differs from using `stream_mode="updates"`: instead of streaming just the updates to the state from each node, it streams the entire graph state at that superstep. Read [this conceptual guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#stream-and-astream) to learn more.```
|
||||
This guide covers how to use `stream_mode="values"`, which streams the value of the state at each superstep. This differs from using `stream_mode="updates"`: instead of streaming just the updates to the state from each node, it streams the entire graph state at that superstep. Read [this conceptual guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#stream-and-astream) to learn more.
|
||||
|
||||
First let's set up our client and thread:
|
||||
|
||||
@@ -23,7 +23,7 @@ First let's set up our client and thread:
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// create thread
|
||||
const thread = await client.threads.create();
|
||||
console.log(thread)
|
||||
console.log(thread);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
@@ -36,12 +36,15 @@ First let's set up our client and thread:
|
||||
|
||||
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': {},
|
||||
'status': 'idle',
|
||||
'config': {}}
|
||||
{
|
||||
'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': {},
|
||||
'status': 'idle',
|
||||
'config': {},
|
||||
'values': None
|
||||
}
|
||||
|
||||
Now we can stream by values, which streams the full state of the graph after each node has finished executing:
|
||||
|
||||
@@ -76,9 +79,9 @@ Now we can stream by values, which streams the full state of the graph after eac
|
||||
}
|
||||
);
|
||||
for await (const chunk of streamResponse) {
|
||||
console.log(f"Receiving new event of type: {chunk.event}...")
|
||||
console.log(chunk.data)
|
||||
console.log("\n\n")
|
||||
console.log(`Receiving new event of type: ${chunk.event}...`);
|
||||
console.log(chunk.data);
|
||||
console.log("\n\n");
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# Use Webhooks
|
||||
|
||||
You may wish to use webhooks in your client, especially when using async streams in case you want to update something in your service once the API call to LangGraph Cloud has finished running. To do so, you will need to expose an endpoint that can accept POST requests, and then pass it to your API request in the "webhook" parameter.
|
||||
|
||||
Currently, the SDK has not exposed this endpoint but you can access it through curl commands as follows.
|
||||
|
||||
The following endpoints accept `webhook` as a parameter:
|
||||
|
||||
- Create Run -> POST /thread/{thread_id}/runs
|
||||
- Create Thread Cron -> POST /thread/{thread_id}/runs/crons
|
||||
- Stream Run -> POST /thread/{thread_id}/runs/stream
|
||||
- Wait Run -> POST /thread/{thread_id}/runs/wait
|
||||
- Create Cron -> POST /runs/crons
|
||||
- Stream Run Stateless -> POST /runs/stream
|
||||
- Wait Run Stateless -> POST /runs/wait
|
||||
|
||||
In this example, we will show calling a webhook after streaming a run. First, let's setup our assistant and thread:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# Using the graph deployed with the name "agent"
|
||||
assistant_id = "agent"
|
||||
# create thread
|
||||
thread = await client.threads.create()
|
||||
print(thread)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// Using the graph deployed with the name "agent"
|
||||
const assistantID = "agent";
|
||||
// create thread
|
||||
const thread = await client.threads.create();
|
||||
console.log(thread);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/assistants/search \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"limit": 10,
|
||||
"offset": 0
|
||||
}' | jq -c 'map(select(.config == null or .config == {})) | .[0]' && \
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{}'
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
{
|
||||
'thread_id': '9dde5490-2b67-47c8-aa14-4bfec88af217',
|
||||
'created_at': '2024-08-30T23:07:38.242730+00:00',
|
||||
'updated_at': '2024-08-30T23:07:38.242730+00:00',
|
||||
'metadata': {},
|
||||
'status': 'idle',
|
||||
'config': {},
|
||||
'values': None
|
||||
}
|
||||
|
||||
Now we can invoke a run with a webhook:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
# create input
|
||||
input = { "messages": [{ "role": "human", "content": "Hello!" }] }
|
||||
|
||||
async for chunk in client.runs.stream(
|
||||
thread_id=thread["thread_id"],
|
||||
assistant_id=assistant_id,
|
||||
input=input,
|
||||
stream_mode="events",
|
||||
webhook="your-webhook"
|
||||
):
|
||||
# Do something with the stream output
|
||||
pass
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
// create input
|
||||
const input = { messages: [{ role: "human", content: "Hello!" }] };
|
||||
|
||||
// stream events
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistantID,
|
||||
{
|
||||
input: input,
|
||||
webhook: "your-webhook"
|
||||
}
|
||||
);
|
||||
for await (const chunk of streamResponse) {
|
||||
// Do something with the stream output
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"assistant_id": <ASSISTANT_ID>,
|
||||
"input" : {"messages":[{"role": "user", "content": "Hello!"}]},
|
||||
"webhook": <YOUR_WEBHOOK_URL>
|
||||
}'
|
||||
```
|
||||
|
||||
And that's it! Now you can trigger your custom webhooks whenever you want in your LangGraph applications!
|
||||
+168
-31
@@ -14,13 +14,25 @@ This tutorial will use:
|
||||
|
||||
1. Create a new application with the following directory and files:
|
||||
|
||||
=== "Python"
|
||||
|
||||
<my-app>/
|
||||
|-- agent.py # code for your LangGraph agent
|
||||
|-- requirements.txt # Python packages required for your graph
|
||||
|-- langgraph.json # configuration file for LangGraph
|
||||
|-- .env # environment files with API keys
|
||||
|
||||
2. The `agent.py` file should contain Python code for defining your graph. The following code is a simple example, the important thing is that at some point in your file you compile your graph and assign the compiled graph to a variable (in this case the `graph` variable). This example code uses `create_react_agent`, a prebuilt agent, read more about it [here](..//concepts/agentic_concepts.md#react-agent).
|
||||
=== "Javascript"
|
||||
|
||||
<my-app>/
|
||||
|-- agent.ts # code for your LangGraph agent
|
||||
|-- package.json # Javascript packages required for your graph
|
||||
|-- langgraph.json # configuration file for LangGraph
|
||||
|-- .env # environment files with API keys
|
||||
|
||||
2. The `agent.py`/`agent.ts` file should contain code for defining your graph. The following code is a simple example, the important thing is that at some point in your file you compile your graph and assign the compiled graph to a variable (in this case the `graph` variable). This example code uses `create_react_agent`, a prebuilt agent. You can read more about it [here](../concepts/agentic_concepts.md#react-agent).
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
@@ -34,14 +46,53 @@ This tutorial will use:
|
||||
graph = create_react_agent(model, tools)
|
||||
```
|
||||
|
||||
3. The `requirements.txt` file should contain any dependencies for your graph(s). In this case we only require four packages for our graph to run:
|
||||
=== "Javascript"
|
||||
|
||||
langgraph
|
||||
langchain_anthropic
|
||||
tavily-python
|
||||
langchain_community
|
||||
```ts
|
||||
import { ChatAnthropic } from "@langchain/anthropic";
|
||||
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
4. The [`langgraph.json`][langgraph.json] file is a configuration file that describes what graph(s) you are going to host. In this case we only have one graph to host: the compiled `graph` object from `agent.py`.
|
||||
const model = new ChatAnthropic({
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
});
|
||||
|
||||
const tools = [
|
||||
new TavilySearchResults({ maxResults: 3, }),
|
||||
];
|
||||
|
||||
export const graph = createReactAgent({ llm: model, tools });
|
||||
```
|
||||
|
||||
3. The `requirements.txt`/`package.json` file should contain any dependencies for your graph(s). In this case we only require four packages for our graph to run:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
langgraph
|
||||
langchain_anthropic
|
||||
tavily-python
|
||||
langchain_community
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
{
|
||||
"name": "my-app",
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"dependencies": {
|
||||
"@langchain/community": "^0.2.31",
|
||||
"@langchain/core": "^0.2.31",
|
||||
"@langchain/langgraph": "0.2.0",
|
||||
"@langchain/openai": "^0.2.8"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. The [`langgraph.json`][langgraph.json] file is a configuration file that describes what graph(s) you are going to host. In this case we only have one graph to host: the compiled `graph` object from `agent.py`/`agent.ts`.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -53,7 +104,21 @@ This tutorial will use:
|
||||
}
|
||||
```
|
||||
|
||||
Learn more about the LangGraph CLI configuration file [here](./reference/cli.md#configuration-file).
|
||||
=== "Javascript"
|
||||
|
||||
```json
|
||||
{
|
||||
"node_version": "20",
|
||||
"dockerfile_lines": [],
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"agent": "./src/agent.ts:graph"
|
||||
},
|
||||
"env": ".env"
|
||||
}
|
||||
```
|
||||
|
||||
Learn more about the LangGraph CLI configuration file [here](./reference/cli.md#configuration-file).
|
||||
|
||||
5. The `.env` file should have any environment variables needed to run your graph. This will only be used for local testing, so if you are not testing locally you can skip this step. NOTE: if you do add this, you should NOT check this into git. For this graph, we need two environment variables:
|
||||
|
||||
@@ -206,36 +271,108 @@ export LANGSMITH_API_KEY=...
|
||||
|
||||
The first thing to do when using the SDK is to setup our client, access our assistant, and create a thread to execute a run on:
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
=== "Python"
|
||||
|
||||
# Replace this with the URL of your own deployed graph
|
||||
URL = "https://chatbot-23a570f3210f52a7b167f09f6158e3b3-ffoprvkqsa-uc.a.run.app"
|
||||
client = get_client(url=URL)
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
# Search all hosted graphs
|
||||
assistants = await client.assistants.search()
|
||||
# In this example we select the first assistant since we are only hosting a single graph
|
||||
assistant = assistants[0]
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# get default assistant
|
||||
assistants = await client.assistants.search()
|
||||
assistant = [a for a in assistants if not a["config"]][0]
|
||||
# create thread
|
||||
thread = await client.threads.create()
|
||||
print(thread)
|
||||
```
|
||||
|
||||
# We create a thread for tracking the state of our run
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// get default assistant
|
||||
const assistants = await client.assistants.search();
|
||||
const assistant = assistants.find(a => !a.config);
|
||||
// create thread
|
||||
const thread = await client.threads.create();
|
||||
console.log(thread)
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/assistants/search \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"limit": 10,
|
||||
"offset": 0
|
||||
}' | jq -c 'map(select(.config == null or .config == {})) | .[0]' && \
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{}'
|
||||
```
|
||||
|
||||
We can then execute a run on the thread:
|
||||
|
||||
```python
|
||||
input = {"messages":[{"role": "user", "content": "Hello! My name is Bagatur and I am 26 years old."}]}
|
||||
=== "Python"
|
||||
|
||||
async for chunk in client.runs.stream(
|
||||
thread['thread_id'],
|
||||
assistant["assistant_id"],
|
||||
input=input,
|
||||
stream_mode="updates",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
```
|
||||
```python
|
||||
input = {"messages":[{"role": "user", "content": "Hello! My name is Bagatur and I am 26 years old."}]}
|
||||
|
||||
async for chunk in client.runs.stream(
|
||||
thread['thread_id'],
|
||||
assistant["assistant_id"],
|
||||
input=input,
|
||||
stream_mode="updates",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const input = { "messages":[{ "role": "user", "content": "Hello! My name is Bagatur and I am 26 years old." }] };
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistant["assistant_id"],
|
||||
{
|
||||
input,
|
||||
}
|
||||
);
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.data && chunk.event !== "metadata" ) {
|
||||
console.log(chunk.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": <ASSISTANT_ID>,
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"Hello! My name is Bagatur and I am 26 years old.\"}]},
|
||||
}" | sed 's/\r$//' | awk '
|
||||
/^event:/ { event = $2 }
|
||||
/^data:/ {
|
||||
json_data = substr($0, index($0, $2))
|
||||
|
||||
if (event != "metadata") {
|
||||
print json_data
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
|
||||
Output:
|
||||
|
||||
{'agent': {'messages': [{'content': "Hi Bagatur! It's nice to meet you. How can I assist you today?", 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_9cb5d38cf7'}, 'type': 'ai', 'name': None, 'id': 'run-c89118b7-1b1e-42b9-a85d-c43fe99881cd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ Welcome to the LangGraph how-to guides! These guides provide practical, step-by-
|
||||
LangGraph is known for being a highly controllable agent framework.
|
||||
These how-to guides show how to achieve that controllability.
|
||||
|
||||
- [How to create subgraphs](subgraph.ipynb)
|
||||
- [How to create branches for parallel execution](branching.ipynb)
|
||||
- [How to create map-reduce branches for parallel execution](map-reduce.ipynb)
|
||||
- [How to control graph recursion limit](recursion-limit.ipynb)
|
||||
@@ -64,6 +63,12 @@ These guides show how to use different streaming modes.
|
||||
- [How to pass config to tools](pass-config-to-tools.ipynb)
|
||||
- [How to handle large numbers of tools](many-tools.ipynb)
|
||||
|
||||
## Subgraphs
|
||||
|
||||
- [How to create subgraphs](subgraph.ipynb)
|
||||
- [How to manage state in subgraphs](subgraphs-manage-state.ipynb)
|
||||
- [How to transform inputs and outputs of a subgraph](subgraph-transform-state.ipynb)
|
||||
|
||||
## State Management
|
||||
|
||||
- [Use Pydantic model as state](state-model.ipynb)
|
||||
|
||||
+11
-8
@@ -126,7 +126,6 @@ nav:
|
||||
- "How-to Guides":
|
||||
- "how-tos/index.md"
|
||||
- Controllability:
|
||||
- Create subgraphs: how-tos/subgraph.ipynb
|
||||
- Create branches for parallel execution: how-tos/branching.ipynb
|
||||
- Create map-reduce branches for parallel execution: how-tos/map-reduce.ipynb
|
||||
- Control graph recursion limit: how-tos/recursion-limit.ipynb
|
||||
@@ -161,6 +160,10 @@ nav:
|
||||
- Pass graph state to tools: how-tos/pass-run-time-values-to-tools.ipynb
|
||||
- Pass config to tools: how-tos/pass-config-to-tools.ipynb
|
||||
- Handle many tools: how-tos/many-tools.ipynb
|
||||
- Subgraphs:
|
||||
- Create subgraphs: how-tos/subgraph.ipynb
|
||||
- Manage state in subgraphs: how-tos/subgraphs-manage-state.ipynb
|
||||
- Transform inputs and outputs of a subgraph: how-tos/subgraph-transform-state.ipynb
|
||||
- State Management:
|
||||
- Use Pydantic model as state: how-tos/state-model.ipynb
|
||||
- Use a context object in state: how-tos/state-context-key.ipynb
|
||||
@@ -197,11 +200,11 @@ nav:
|
||||
- Setup:
|
||||
- Setup App: "cloud/deployment/setup.md"
|
||||
- Setup App (pyproject.toml): "cloud/deployment/setup_pyproject.md"
|
||||
- Setup App (JavaScript): "cloud/deployment/setup_javascript.md"
|
||||
- Rebuild Graph at Runtime: "cloud/deployment/graph_rebuild.md"
|
||||
- Test App Locally: "cloud/deployment/test_locally.md"
|
||||
- Deployment:
|
||||
- Deploy to Cloud: "cloud/deployment/cloud.md"
|
||||
- Self-Host: "cloud/deployment/self_hosted.md"
|
||||
- Streaming:
|
||||
- Stream Values: "cloud/how-tos/stream_values.md"
|
||||
- Stream Updates: "cloud/how-tos/stream_updates.md"
|
||||
@@ -226,14 +229,14 @@ nav:
|
||||
- Invoke graph in LangGraph Studio: "cloud/how-tos/invoke_studio.md"
|
||||
- Interact with threads in LangGraph Studio: "cloud/how-tos/threads_studio.md"
|
||||
- Different Types of Runs:
|
||||
- Run an Agent in the Background: "cloud/how-tos/cloud_examples/background_run.ipynb"
|
||||
- Run Multiple Agents in Same Thread: "cloud/how-tos/cloud_examples/same-thread.ipynb"
|
||||
- Create Cron Jobs: "cloud/how-tos/cloud_examples/cron_jobs.ipynb"
|
||||
- Create Stateless Runs: "cloud/how-tos/cloud_examples/stateless_runs.ipynb"
|
||||
- Run an Agent in the Background: "cloud/how-tos/background_run.md"
|
||||
- Run Multiple Agents in Same Thread: "cloud/how-tos/same-thread.md"
|
||||
- Create Cron Jobs: "cloud/how-tos/cron_jobs.md"
|
||||
- Create Stateless Runs: "cloud/how-tos/stateless_runs.md"
|
||||
- Other:
|
||||
- Configure Agents: "cloud/how-tos/cloud_examples/configuration_cloud.ipynb"
|
||||
- Configure Agents: "cloud/how-tos/configuration_cloud.md"
|
||||
- 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'
|
||||
- Integrate Webhooks: 'cloud/how-tos/webhooks.md'
|
||||
- Copy Threads: 'cloud/how-tos/copy_threads.md'
|
||||
- Check Status of Threads: "cloud/how-tos/check_thread_status.md"
|
||||
- Conceptual Guides:
|
||||
|
||||
@@ -103,14 +103,6 @@
|
||||
" print(list(s.values())[0])\n",
|
||||
" print(\"----\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "87f147e3-f96f-4b96-a3cc-ec7affd7a57f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -1,388 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to kick off background runs\n",
|
||||
"\n",
|
||||
"This guide covers how to kick off background runs for your agent.\n",
|
||||
"This can be useful for long running jobs."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "b8e6408a-b37e-428f-9567-077fa55d58e8",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Initialize the client\n",
|
||||
"from langgraph_sdk import get_client\n",
|
||||
"\n",
|
||||
"client = get_client()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "4947e9bc-111f-4991-8c41-1041da9bf0ba",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'assistant_id': 'e90fee30-be91-43aa-a33c-d54bd219072e',\n",
|
||||
" 'graph_id': 'agent',\n",
|
||||
" 'created_at': '2024-06-18T18:06:55.102231+00:00',\n",
|
||||
" 'updated_at': '2024-06-18T18:06:55.102231+00:00',\n",
|
||||
" 'config': {'configurable': {'model_name': 'anthropic'}},\n",
|
||||
" 'metadata': {}}"
|
||||
]
|
||||
},
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# List available assistants\n",
|
||||
"assistants = await client.assistants.search()\n",
|
||||
"assistants[0]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "230c0464-a6e5-420f-9e38-ca514e5634ce",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# NOTE: we can use `assistant_id` UUID from the above response, or just pass graph ID instead when creating runs. we'll use graph ID here\n",
|
||||
"assistant_id = \"agent\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "56aa5159-5583-4134-9210-709b969bda6f",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n",
|
||||
" 'created_at': '2024-06-21T14:58:02.079462+00:00',\n",
|
||||
" 'updated_at': '2024-06-21T14:58:02.079462+00:00',\n",
|
||||
" 'metadata': {}}"
|
||||
]
|
||||
},
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Create a new thread\n",
|
||||
"thread = await client.threads.create()\n",
|
||||
"thread"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "147c3f98-f889-4f05-a090-6b31f2a0b291",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[]"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# If we list runs on this thread, we can see it is empty\n",
|
||||
"runs = await client.runs.list(thread[\"thread_id\"])\n",
|
||||
"runs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "8c7b44ef-4816-496d-88a1-2f7327cf576d",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Let's kick off a run\n",
|
||||
"input = {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf\"}]}\n",
|
||||
"run = await client.runs.create(thread[\"thread_id\"], assistant_id, input=input)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "d84b4d80-b0aa-4d9f-a05d-0744b2fe8f72",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'run_id': '1ef2fdea-814c-6165-8b2a-a40e2a028198',\n",
|
||||
" 'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n",
|
||||
" 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n",
|
||||
" 'created_at': '2024-06-21T14:58:02.095911+00:00',\n",
|
||||
" 'updated_at': '2024-06-21T14:58:02.095911+00:00',\n",
|
||||
" 'metadata': {},\n",
|
||||
" 'status': 'pending',\n",
|
||||
" 'kwargs': {'input': {'messages': [{'role': 'human',\n",
|
||||
" 'content': 'what's the weather in sf'}]},\n",
|
||||
" 'config': {'metadata': {'created_by': 'system'},\n",
|
||||
" 'configurable': {'run_id': '1ef2fdea-814c-6165-8b2a-a40e2a028198',\n",
|
||||
" 'user_id': '',\n",
|
||||
" 'graph_id': 'agent',\n",
|
||||
" 'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n",
|
||||
" 'thread_ts': None,\n",
|
||||
" 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}},\n",
|
||||
" 'webhook': None,\n",
|
||||
" 'temporary': False,\n",
|
||||
" 'stream_mode': ['events'],\n",
|
||||
" 'feedback_keys': None,\n",
|
||||
" 'interrupt_after': None,\n",
|
||||
" 'interrupt_before': None},\n",
|
||||
" 'multitask_strategy': 'reject'}"
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# The first time we poll it, we can see `status=pending`\n",
|
||||
"await client.runs.get(thread[\"thread_id\"], run[\"run_id\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "3639da3c-bfe5-454c-ab1e-8ed7af394dfe",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Wait until the run finishes\n",
|
||||
"await client.runs.join(thread[\"thread_id\"], run[\"run_id\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"id": "8fa206ed-515e-4607-9a80-bebafe76cc24",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'run_id': '1ef2fdea-814c-6165-8b2a-a40e2a028198',\n",
|
||||
" 'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n",
|
||||
" 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n",
|
||||
" 'created_at': '2024-06-21T14:58:02.095911+00:00',\n",
|
||||
" 'updated_at': '2024-06-21T14:58:02.095911+00:00',\n",
|
||||
" 'metadata': {},\n",
|
||||
" 'status': 'success',\n",
|
||||
" 'kwargs': {'input': {'messages': [{'role': 'human',\n",
|
||||
" 'content': 'what's the weather in sf'}]},\n",
|
||||
" 'config': {'metadata': {'created_by': 'system'},\n",
|
||||
" 'configurable': {'run_id': '1ef2fdea-814c-6165-8b2a-a40e2a028198',\n",
|
||||
" 'user_id': '',\n",
|
||||
" 'graph_id': 'agent',\n",
|
||||
" 'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n",
|
||||
" 'thread_ts': None,\n",
|
||||
" 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}},\n",
|
||||
" 'webhook': None,\n",
|
||||
" 'temporary': False,\n",
|
||||
" 'stream_mode': ['events'],\n",
|
||||
" 'feedback_keys': None,\n",
|
||||
" 'interrupt_after': None,\n",
|
||||
" 'interrupt_before': None},\n",
|
||||
" 'multitask_strategy': 'reject'}"
|
||||
]
|
||||
},
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Eventually, it should finish and we should see `status=success`\n",
|
||||
"await client.runs.get(thread[\"thread_id\"], run[\"run_id\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"id": "8de4495f-7873-487c-b1a8-ad2a78a1ff35",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# We can get the final results\n",
|
||||
"final_result = await client.threads.get_state(thread[\"thread_id\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"id": "9da76fce-66e4-4f1b-8c24-09759889e50e",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'values': {'messages': [{'content': 'what's the weather in sf',\n",
|
||||
" 'additional_kwargs': {},\n",
|
||||
" 'response_metadata': {},\n",
|
||||
" 'type': 'human',\n",
|
||||
" 'name': None,\n",
|
||||
" 'id': 'bfe07fff-cb40-40be-84d5-a061d2c40006',\n",
|
||||
" 'example': False},\n",
|
||||
" {'content': [{'id': 'toolu_01QUzhhfDQkpbPSediUrXvQb',\n",
|
||||
" 'input': {'query': 'weather in san francisco'},\n",
|
||||
" 'name': 'tavily_search_results_json',\n",
|
||||
" 'type': 'tool_use'}],\n",
|
||||
" 'additional_kwargs': {},\n",
|
||||
" 'response_metadata': {},\n",
|
||||
" 'type': 'ai',\n",
|
||||
" 'name': None,\n",
|
||||
" 'id': 'run-6d8665ca-a77d-4b44-9a7b-4e975b155fb1',\n",
|
||||
" 'example': False,\n",
|
||||
" 'tool_calls': [{'name': 'tavily_search_results_json',\n",
|
||||
" 'args': {'query': 'weather in san francisco'},\n",
|
||||
" 'id': 'toolu_01QUzhhfDQkpbPSediUrXvQb'}],\n",
|
||||
" 'invalid_tool_calls': [],\n",
|
||||
" 'usage_metadata': None},\n",
|
||||
" {'content': '[{\"url\": \"https://www.timeanddate.com/weather/usa/san-francisco/historic\", \"content\": \"San Francisco Weather History for the Previous 24 Hours Show weather for: Previous 24 hours June 17, 2024 June 16, 2024 June 15, 2024 June 14, 2024 June 13, 2024 June 12, 2024 June 11, 2024 June 10, 2024 June 9, 2024 June 8, 2024 June 7, 2024 June 6, 2024 June 5, 2024 June 4, 2024 June 3, 2024 June 2, 2024\"}]',\n",
|
||||
" 'additional_kwargs': {},\n",
|
||||
" 'response_metadata': {},\n",
|
||||
" 'type': 'tool',\n",
|
||||
" 'name': 'tavily_search_results_json',\n",
|
||||
" 'id': '257a1f29-2f66-4f9e-b35d-c8818dbbaa3f',\n",
|
||||
" 'tool_call_id': 'toolu_01QUzhhfDQkpbPSediUrXvQb'},\n",
|
||||
" {'content': [{'text': 'The search results provide historic weather data for San Francisco, but do not give the current weather conditions. To get the current weather forecast for San Francisco, I would need to refine my search query. Here is an updated search:',\n",
|
||||
" 'type': 'text'},\n",
|
||||
" {'id': 'toolu_01RLJEcWYRvRoBhiHdrhoRZx',\n",
|
||||
" 'input': {'query': 'san francisco weather forecast today'},\n",
|
||||
" 'name': 'tavily_search_results_json',\n",
|
||||
" 'type': 'tool_use'}],\n",
|
||||
" 'additional_kwargs': {},\n",
|
||||
" 'response_metadata': {},\n",
|
||||
" 'type': 'ai',\n",
|
||||
" 'name': None,\n",
|
||||
" 'id': 'run-ca41dbf8-7e89-4ff2-a245-87098d7928ba',\n",
|
||||
" 'example': False,\n",
|
||||
" 'tool_calls': [{'name': 'tavily_search_results_json',\n",
|
||||
" 'args': {'query': 'san francisco weather forecast today'},\n",
|
||||
" 'id': 'toolu_01RLJEcWYRvRoBhiHdrhoRZx'}],\n",
|
||||
" 'invalid_tool_calls': [],\n",
|
||||
" 'usage_metadata': None},\n",
|
||||
" {'content': '[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{\\'location\\': {\\'name\\': \\'San Francisco\\', \\'region\\': \\'California\\', \\'country\\': \\'United States of America\\', \\'lat\\': 37.78, \\'lon\\': -122.42, \\'tz_id\\': \\'America/Los_Angeles\\', \\'localtime_epoch\\': 1718981382, \\'localtime\\': \\'2024-06-21 7:49\\'}, \\'current\\': {\\'last_updated_epoch\\': 1718981100, \\'last_updated\\': \\'2024-06-21 07:45\\', \\'temp_c\\': 12.8, \\'temp_f\\': 55.0, \\'is_day\\': 1, \\'condition\\': {\\'text\\': \\'Overcast\\', \\'icon\\': \\'//cdn.weatherapi.com/weather/64x64/day/122.png\\', \\'code\\': 1009}, \\'wind_mph\\': 6.9, \\'wind_kph\\': 11.2, \\'wind_degree\\': 200, \\'wind_dir\\': \\'SSW\\', \\'pressure_mb\\': 1011.0, \\'pressure_in\\': 29.84, \\'precip_mm\\': 0.01, \\'precip_in\\': 0.0, \\'humidity\\': 86, \\'cloud\\': 100, \\'feelslike_c\\': 12.2, \\'feelslike_f\\': 53.9, \\'windchill_c\\': 11.2, \\'windchill_f\\': 52.1, \\'heatindex_c\\': 12.0, \\'heatindex_f\\': 53.5, \\'dewpoint_c\\': 9.4, \\'dewpoint_f\\': 48.8, \\'vis_km\\': 16.0, \\'vis_miles\\': 9.0, \\'uv\\': 3.0, \\'gust_mph\\': 7.6, \\'gust_kph\\': 12.2}}\"}]',\n",
|
||||
" 'additional_kwargs': {},\n",
|
||||
" 'response_metadata': {},\n",
|
||||
" 'type': 'tool',\n",
|
||||
" 'name': 'tavily_search_results_json',\n",
|
||||
" 'id': 'c80a3720-6a9f-4ff0-9ce2-6112e66a6f81',\n",
|
||||
" 'tool_call_id': 'toolu_01RLJEcWYRvRoBhiHdrhoRZx'},\n",
|
||||
" {'content': 'The updated search provides the current weather forecast for San Francisco. According to the results, as of 7:49am on June 21, 2024 in San Francisco, the temperature is 55°F (12.8°C), it is overcast with 100% cloud cover, and there are light winds from the south-southwest around 7 mph (11 km/h). The forecast also shows low precipitation of 0.01 mm, high humidity of 86%, and visibility of 9 miles (16 km).\\n\\nIn summary, the current weather in San Francisco is cool, overcast, and breezy based on this weather forecast data. Let me know if you need any other details!',\n",
|
||||
" 'additional_kwargs': {},\n",
|
||||
" 'response_metadata': {},\n",
|
||||
" 'type': 'ai',\n",
|
||||
" 'name': None,\n",
|
||||
" 'id': 'run-4f23b53d-a8ec-4038-b3ed-08b2560bf81c',\n",
|
||||
" 'example': False,\n",
|
||||
" 'tool_calls': [],\n",
|
||||
" 'invalid_tool_calls': [],\n",
|
||||
" 'usage_metadata': None}]},\n",
|
||||
" 'next': [],\n",
|
||||
" 'config': {'configurable': {'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n",
|
||||
" 'thread_ts': '1ef2fdea-f879-65a5-8005-443b6a4039aa'}},\n",
|
||||
" 'metadata': {'step': 5,\n",
|
||||
" 'run_id': '1ef2fdea-814c-6165-8b2a-a40e2a028198',\n",
|
||||
" 'source': 'loop',\n",
|
||||
" 'writes': {'agent': {'messages': [{'id': 'run-4f23b53d-a8ec-4038-b3ed-08b2560bf81c',\n",
|
||||
" 'name': None,\n",
|
||||
" 'type': 'ai',\n",
|
||||
" 'content': 'The updated search provides the current weather forecast for San Francisco. According to the results, as of 7:49am on June 21, 2024 in San Francisco, the temperature is 55°F (12.8°C), it is overcast with 100% cloud cover, and there are light winds from the south-southwest around 7 mph (11 km/h). The forecast also shows low precipitation of 0.01 mm, high humidity of 86%, and visibility of 9 miles (16 km).\\n\\nIn summary, the current weather in San Francisco is cool, overcast, and breezy based on this weather forecast data. Let me know if you need any other details!',\n",
|
||||
" 'example': False,\n",
|
||||
" 'tool_calls': [],\n",
|
||||
" 'usage_metadata': None,\n",
|
||||
" 'additional_kwargs': {},\n",
|
||||
" 'response_metadata': {},\n",
|
||||
" 'invalid_tool_calls': []}]}},\n",
|
||||
" 'user_id': '',\n",
|
||||
" 'graph_id': 'agent',\n",
|
||||
" 'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n",
|
||||
" 'created_by': 'system',\n",
|
||||
" 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'},\n",
|
||||
" 'created_at': '2024-06-21T14:58:14.591805+00:00',\n",
|
||||
" 'parent_config': {'configurable': {'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n",
|
||||
" 'thread_ts': '1ef2fdea-d44c-6fc4-8004-d2713436777d'}}}"
|
||||
]
|
||||
},
|
||||
"execution_count": 11,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"final_result"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"id": "ddd6e698-4609-4389-b84a-bb8939fff08b",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"'The updated search provides the current weather forecast for San Francisco. According to the results, as of 7:49am on June 21, 2024 in San Francisco, the temperature is 55°F (12.8°C), it is overcast with 100% cloud cover, and there are light winds from the south-southwest around 7 mph (11 km/h). The forecast also shows low precipitation of 0.01 mm, high humidity of 86%, and visibility of 9 miles (16 km).\\n\\nIn summary, the current weather in San Francisco is cool, overcast, and breezy based on this weather forecast data. Let me know if you need any other details!'"
|
||||
]
|
||||
},
|
||||
"execution_count": 12,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# We can get the content of the final message\n",
|
||||
"final_result[\"values\"][\"messages\"][-1][\"content\"]"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "langgraph-example-dev",
|
||||
"language": "python",
|
||||
"name": "langgraph-example-dev"
|
||||
},
|
||||
"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": 5
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "68c0837d-c40a-4209-9f88-5d08c00c31b0",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to create agents with configuration\n",
|
||||
"\n",
|
||||
"One of the benefits of LangGraph API is that it lets you create agents with different configurations.\n",
|
||||
"This is useful when you want to:\n",
|
||||
"\n",
|
||||
"- Define a cognitive architecture once as a LangGraph\n",
|
||||
"- Let that LangGraph be configurable across some attributes (for example, system message or LLM to use)\n",
|
||||
"- Let users create agents with arbitrary configurations, save them, and then use them in the future\n",
|
||||
"\n",
|
||||
"In this guide we will show how to do that for the default agent we have built in.\n",
|
||||
"\n",
|
||||
"If you look at the agent we defined, you can see that inside the `call_model` node we have created the model based on some configuration. That node looks like:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"def call_model(state, config):\n",
|
||||
" messages = state[\"messages\"]\n",
|
||||
" model_name = config.get('configurable', {}).get(\"model_name\", \"anthropic\")\n",
|
||||
" model = _get_model(model_name)\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",
|
||||
"We are looking inside the config for a `model_name` parameter (which defaults to `anthropic` if none is found).\n",
|
||||
"That means that by default we are using Anthropic as our model provider.\n",
|
||||
"In this example we will see an example of how to create an example agent that is configured to use OpenAI.\n",
|
||||
"\n",
|
||||
"We've also communicated to the graph that it should expect configuration with this key. \n",
|
||||
"We've done this by passing `config_schema` when constructing the graph, eg:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"class GraphConfig(TypedDict):\n",
|
||||
" model_name: Literal[\"anthropic\", \"openai\"]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Define a new graph\n",
|
||||
"workflow = StateGraph(AgentState, config_schema=GraphConfig)\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 16,
|
||||
"id": "f69c9a4f-2ef9-4998-827b-fe86d12bfd76",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph_sdk import get_client\n",
|
||||
"\n",
|
||||
"client = get_client()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 14,
|
||||
"id": "9a37bfb5-7331-4004-8054-508838e54f18",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# First, let's check what valid configuration can be\n",
|
||||
"# We can do this by getting the default assistant\n",
|
||||
"# There should always be a default assistant with no configuration\n",
|
||||
"assistants = await client.assistants.search()\n",
|
||||
"assistants = [a for a in assistants if not a[\"config\"]]\n",
|
||||
"base_assistant = assistants[0]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 17,
|
||||
"id": "70193a08-127c-44b3-a102-10db260d7e3b",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'model_name': {'title': 'Model Name',\n",
|
||||
" 'enum': ['anthropic', 'openai'],\n",
|
||||
" 'type': 'string'}}"
|
||||
]
|
||||
},
|
||||
"execution_count": 17,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# We can now call `.get_schemas` to get schemas associated with this graph\n",
|
||||
"schemas = await client.assistants.get_schemas(\n",
|
||||
" assistant_id=base_assistant[\"assistant_id\"]\n",
|
||||
")\n",
|
||||
"# There are multiple types of schemas\n",
|
||||
"# We can get the `config_schema` to look at the the configurable parameters\n",
|
||||
"schemas[\"config_schema\"][\"definitions\"][\"Configurable\"][\"properties\"]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 18,
|
||||
"id": "99be5aee-9a6b-4515-b72f-ba135a893c65",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"assistant = await client.assistants.create(\n",
|
||||
" graph_id=\"agent\", config={\"configurable\": {\"model_name\": \"openai\"}}\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "4f10d346-69e6-44f4-8ff0-ef539ba938df",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We can see that this assistant has saved the config"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 20,
|
||||
"id": "3898ca35-eb2c-4b12-97ea-e0cc6a7c6a2e",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'assistant_id': '40a3a2bf-5319-4fae-a2ac-05e075615cdc',\n",
|
||||
" 'graph_id': 'agent',\n",
|
||||
" 'config': {'configurable': {'model_name': 'openai'}},\n",
|
||||
" 'created_at': '2024-06-05T23:12:30.519458+00:00',\n",
|
||||
" 'updated_at': '2024-06-05T23:12:30.519458+00:00',\n",
|
||||
" 'metadata': {}}"
|
||||
]
|
||||
},
|
||||
"execution_count": 20,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"assistant"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 21,
|
||||
"id": "68ed7a1b-74be-4560-8c55-c76d49d3d348",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"StreamPart(event='metadata', data={'run_id': '1ef23911-c23b-6d8c-b1dc-94bb982ca7b1'})\n",
|
||||
"StreamPart(event='values', data={'messages': [{'role': 'user', 'content': 'who made you?'}]})\n",
|
||||
"StreamPart(event='values', data={'messages': [{'content': 'who made you?', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'ed93c1c9-80d6-4f2b-a048-ef859ea533f9', 'example': False}, {'content': 'I was created by OpenAI, a research organization focused on developing and advancing artificial intelligence technology.', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-6560cd65-5c9c-434b-8835-0baadc684760', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]})\n",
|
||||
"StreamPart(event='end', data=None)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"thread = await client.threads.create()\n",
|
||||
"input = {\"messages\": [{\"role\": \"user\", \"content\": \"who made you?\"}]}\n",
|
||||
"async for event in client.runs.stream(\n",
|
||||
" thread[\"thread_id\"], assistant[\"assistant_id\"], input=input\n",
|
||||
"):\n",
|
||||
" print(event)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.1"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Cron Jobs\n",
|
||||
"\n",
|
||||
"Sometimes you don't want to run your graph based on user interaction, but rather you would like to schedule your graph to run on a schedule - for example if you wish for your graph to compose and send out a weekly email of to-dos for your team. LangGraph Cloud allows you to do this without having to write your own script by using the `Crons` client. To schedule a graph job, you need to pass a [cron expression](https://crontab.cronhub.io/) to inform the client when you want to run the graph. `Cron` jobs are run in the background and do not interfere with normal invocations of the graph.\n",
|
||||
"\n",
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"First, let's setup our SDK client, assistant, and thread:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 110,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph_sdk import get_client\n",
|
||||
"\n",
|
||||
"client = get_client()\n",
|
||||
"assistants = await client.assistants.search()\n",
|
||||
"assistants = [a for a in assistants if not a[\"config\"]]\n",
|
||||
"assistant = assistants[0]\n",
|
||||
"thread = await client.threads.create()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Cron job on a thread \n",
|
||||
"\n",
|
||||
"To create a cron job associated with a specific thread, you can write:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# This schedules a job to run at 15:27 (3:27PM) every day\n",
|
||||
"cron_1 = await client.crons.create_for_thread(\n",
|
||||
" thread[\"thread_id\"],\n",
|
||||
" assistant[\"assistant_id\"],\n",
|
||||
" schedule=\"27 15 * * *\",\n",
|
||||
" input={\"messages\": [{\"role\": \"user\", \"content\": \"What time is it?\"}]},\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Note that it is **very** important to delete `Cron` jobs that are no longer useful. Otherwise you could rack up unwanted API charges to the LLM! You can delete a `Cron` job using the following code:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"await client.crons.delete(cron_1[\"cron_id\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Cron job stateless\n",
|
||||
"\n",
|
||||
"You can also create stateless cron jobs by using the following code:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# This schedules a job to run at 15:27 (3:27PM) every day\n",
|
||||
"cron_2 = await client.crons.create(\n",
|
||||
" assistant[\"assistant_id\"],\n",
|
||||
" schedule=\"27 15 * * *\",\n",
|
||||
" input={\"messages\": [{\"role\": \"user\", \"content\": \"What time is it?\"}]},\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Again, remember to delete your job once you are done with it!"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"await client.crons.delete(cron_2[\"cron_id\"])"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 432 KiB |
@@ -1,192 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "68c0837d-c40a-4209-9f88-5d08c00c31b0",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to run multiple agents on the same thread\n",
|
||||
"\n",
|
||||
"In LangGraph Cloud, a thread is not explicitly associated with a particular agent.\n",
|
||||
"This means that you can run multiple agents on the same thread, which allows a different\n",
|
||||
"agent to continue from an initial agent's progress.\n",
|
||||
"\n",
|
||||
"In this example, we will create two agents and then call them both on the same thread.\n",
|
||||
"You'll see that the second agent will respond using information from the [checkpoint](https://langchain-ai.github.io/langgraph/concepts/low_level/#checkpointer-state) generated in the thread\n",
|
||||
"by the first agent as context."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "e06be1f6-07a5-4e93-8497-02473fc65d4f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph_sdk import get_client\n",
|
||||
"\n",
|
||||
"client = get_client()\n",
|
||||
"\n",
|
||||
"openai_assistant = await client.assistants.create(\n",
|
||||
" graph_id=\"agent\", config={\"configurable\": {\"model_name\": \"openai\"}}\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# There should always be a default assistant with no configuration\n",
|
||||
"assistants = await client.assistants.search()\n",
|
||||
"default_assistant = [a for a in assistants if not a[\"config\"]][0]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "4f10d346-69e6-44f4-8ff0-ef539ba938df",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We can see that these agents are different:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "3898ca35-eb2c-4b12-97ea-e0cc6a7c6a2e",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'assistant_id': '13ecc353-a9a9-474b-a824-b6a343cd74b1',\n",
|
||||
" 'graph_id': 'agent',\n",
|
||||
" 'config': {'configurable': {'model_name': 'openai'}},\n",
|
||||
" 'created_at': '2024-05-21T16:22:59.258447+00:00',\n",
|
||||
" 'updated_at': '2024-05-21T16:22:59.258447+00:00',\n",
|
||||
" 'metadata': {}}"
|
||||
]
|
||||
},
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"openai_assistant"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"id": "a8fa67b2-cb4f-43d3-a1fc-f8b3936c16b6",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n",
|
||||
" 'graph_id': 'agent',\n",
|
||||
" 'config': {},\n",
|
||||
" 'created_at': '2024-05-18T00:19:39.688822+00:00',\n",
|
||||
" 'updated_at': '2024-05-18T00:19:39.688822+00:00',\n",
|
||||
" 'metadata': {'created_by': 'system'}}"
|
||||
]
|
||||
},
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"default_assistant"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "5e655e61-c2ee-488a-90f6-6189c84841da",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We can now run the OpenAI assistant on the thread first."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 14,
|
||||
"id": "68ed7a1b-74be-4560-8c55-c76d49d3d348",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"StreamPart(event='metadata', data={'run_id': 'f90b3029-8669-4d70-976c-b70368e355d8'})\n",
|
||||
"StreamPart(event='updates', data={'agent': {'messages': [{'content': 'I was created by OpenAI, a research organization focused on developing and advancing artificial intelligence technology.', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-9801a5ba-2f3c-43de-89cf-c740debf36fc', 'example': False, 'tool_calls': [], 'invalid_tool_calls': []}]}})\n",
|
||||
"StreamPart(event='end', data=None)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"thread = await client.threads.create()\n",
|
||||
"input = {\"messages\": [{\"role\": \"user\", \"content\": \"who made you?\"}]}\n",
|
||||
"async for event in client.runs.stream(\n",
|
||||
" thread[\"thread_id\"],\n",
|
||||
" openai_assistant[\"assistant_id\"],\n",
|
||||
" input=input,\n",
|
||||
" stream_mode=\"updates\",\n",
|
||||
"):\n",
|
||||
" print(event)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c53709e9-ddb2-4429-9042-456eb6c91244",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Now, we can run it on a second Anthropic-based assistant and see that this second assistant is aware of the initial question, and can answer the question, `and you?`:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 15,
|
||||
"id": "666d78f1-019a-433e-839e-52d2ebb3d9c8",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"StreamPart(event='metadata', data={'run_id': 'c3521302-48ae-4c29-a0f2-5eb865cbc6d7'})\n",
|
||||
"StreamPart(event='updates', data={'agent': {'messages': [{'content': \"I am an AI assistant created by Anthropic to be helpful, harmless, and honest. I don't actually have a physical form or visual representation - I exist as a language model trained to have natural conversations.\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-4d05ffd7-0505-43e1-a068-0207c56b7665', 'example': False, 'tool_calls': [], 'invalid_tool_calls': []}]}})\n",
|
||||
"StreamPart(event='end', data=None)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"input = {\"messages\": [{\"role\": \"user\", \"content\": \"and you?\"}]}\n",
|
||||
"async for event in client.runs.stream(\n",
|
||||
" thread[\"thread_id\"],\n",
|
||||
" default_assistant[\"assistant_id\"],\n",
|
||||
" input=input,\n",
|
||||
" stream_mode=\"updates\",\n",
|
||||
"):\n",
|
||||
" print(event)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.1"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Stateless Runs\n",
|
||||
"\n",
|
||||
"Most of the time, you provide a `thread_id` to your client when you run your graph in order to keep track of prior runs through the persistent state implemented in LangGraph Cloud. However, if you have your own database to save runs and don't need to use the built in persistent state, you can create stateless runs.\n",
|
||||
"\n",
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"First, let's setup our client"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 106,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph_sdk import get_client\n",
|
||||
"\n",
|
||||
"client = get_client()\n",
|
||||
"assistants = await client.assistants.search()\n",
|
||||
"assistants = [a for a in assistants if not a[\"config\"]]\n",
|
||||
"assistant = assistants[0]\n",
|
||||
"thread = await client.threads.create()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Stateless streaming\n",
|
||||
"\n",
|
||||
"We can stream the results of a stateless run in an almost identical fashion to how we stream from a run with the state attribute, but instead of passing a value to the `thread_id` parameter, we pass `None`:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 107,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'agent': {'messages': [{'content': \"Hello Bagatur! It's nice to meet you. Thank you for introducing yourself and sharing your age. Is there anything specific you'd like to know or discuss? I'm here to help with any questions or topics you're interested in.\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-489ec573-1645-4ce2-a3b8-91b391d50a71', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"input = {\n",
|
||||
" \"messages\": [\n",
|
||||
" {\"role\": \"user\", \"content\": \"Hello! My name is Bagatur and I am 26 years old.\"}\n",
|
||||
" ]\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"async for chunk in client.runs.stream(\n",
|
||||
" # Don't pass in a thread_id and the stream will be stateless\n",
|
||||
" None,\n",
|
||||
" assistant[\"assistant_id\"], # graph_id\n",
|
||||
" input=input,\n",
|
||||
" stream_mode=\"updates\",\n",
|
||||
"):\n",
|
||||
" if chunk.data and \"run_id\" not in chunk.data:\n",
|
||||
" print(chunk.data)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Waiting for stateless results\n",
|
||||
"\n",
|
||||
"In addition to streaming, you can also wait for a stateless result by using the `.wait` function like follows:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 108,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"stateless_run_result = await client.runs.wait(\n",
|
||||
" None,\n",
|
||||
" assistant[\"assistant_id\"], # graph_id\n",
|
||||
" input=input,\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 109,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'messages': [{'content': 'Hello! My name is Bagatur and I am 26 years old.',\n",
|
||||
" 'additional_kwargs': {},\n",
|
||||
" 'response_metadata': {},\n",
|
||||
" 'type': 'human',\n",
|
||||
" 'name': None,\n",
|
||||
" 'id': '5e088543-62c2-43de-9d95-6086ad7f8b48',\n",
|
||||
" 'example': False},\n",
|
||||
" {'content': \"Hello Bagatur! It's nice to meet you. Thank you for introducing yourself and sharing your age. Is there anything specific you'd like to know or discuss? I'm here to help with any questions or topics you'd like to explore.\",\n",
|
||||
" 'additional_kwargs': {},\n",
|
||||
" 'response_metadata': {},\n",
|
||||
" 'type': 'ai',\n",
|
||||
" 'name': None,\n",
|
||||
" 'id': 'run-d6361e8d-4d4c-45bd-ba47-39520257f773',\n",
|
||||
" 'example': False,\n",
|
||||
" 'tool_calls': [],\n",
|
||||
" 'invalid_tool_calls': [],\n",
|
||||
" 'usage_metadata': None}]}"
|
||||
]
|
||||
},
|
||||
"execution_count": 109,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"stateless_run_result"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Use Webhooks\n",
|
||||
"\n",
|
||||
"You may wish to use webhooks in your client, especially when using async streams in case you want to update something in your service once the API call to LangGraph Cloud has finished running. To do so, you will need to expose an endpoint that can accept POST requests, and then pass it to your API request in the \"webhook\" parameter.\n",
|
||||
"\n",
|
||||
"Currently, the SDK has not exposed this endpoint but you can access it through curl commands as follows.\n",
|
||||
"\n",
|
||||
"The following endpoints accept `webhook` as a parameter: \n",
|
||||
"\n",
|
||||
"- Create Run -> POST /thread/{thread_id}/runs\n",
|
||||
"- Create Thread Cron -> POST /thread/{thread_id}/runs/crons\n",
|
||||
"- Stream Run -> POST /thread/{thread_id}/runs/stream\n",
|
||||
"- Wait Run -> POST /thread/{thread_id}/runs/wait\n",
|
||||
"- Create Cron -> POST /runs/crons\n",
|
||||
"- Stream Run Stateless -> POST /runs/stream\n",
|
||||
"- Wait Run Stateless -> POST /runs/wait\n",
|
||||
"\n",
|
||||
"The following example uses a url from a public website that allows users to create free webhooks, but you should pass in the webhook that you wish to use. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"curl --request POST \\\n",
|
||||
" --url http://localhost:8123/threads/b76d1e94-f251-40e3-8933-796d775cdb4c/runs/stream \\\n",
|
||||
" --header 'Content-Type: application/json' \\\n",
|
||||
" --data '{\n",
|
||||
" \"assistant_id\": \"fe096781-5601-53d2-b2f6-0d3403f7e9ca\",\n",
|
||||
" \"input\" : {\"messages\":[{\"role\": \"user\", \"content\": \"Hello! My name is Bagatur and I am 26 years old.\"}]},\n",
|
||||
" \"metadata\": {},\n",
|
||||
" \"config\": {\n",
|
||||
" \"configurable\": {}\n",
|
||||
" },\n",
|
||||
" \"multitask_strategy\": \"reject\",\n",
|
||||
" \"stream_mode\": [\n",
|
||||
" \"values\"\n",
|
||||
" ],\n",
|
||||
" \"webhook\": \"https://webhook.site/6ca33471-dd65-4103-a851-0a252dae0f2a\"\n",
|
||||
"}'"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"To check that this worked as intended, we can go to the website where our webhook was created and confirm that it received a POST request:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
""
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
@@ -99,46 +99,50 @@
|
||||
" # Backup - we will use this to \"reset\" our DB in each section\n",
|
||||
" shutil.copy(local_file, backup_file)\n",
|
||||
"# Convert the flights to present time for our tutorial\n",
|
||||
"conn = sqlite3.connect(local_file)\n",
|
||||
"cursor = conn.cursor()\n",
|
||||
"def update_dates(file):\n",
|
||||
" shutil.copy(backup_file, file)\n",
|
||||
" conn = sqlite3.connect(file)\n",
|
||||
" cursor = conn.cursor()\n",
|
||||
"\n",
|
||||
"tables = pd.read_sql(\n",
|
||||
" \"SELECT name FROM sqlite_master WHERE type='table';\", conn\n",
|
||||
").name.tolist()\n",
|
||||
"tdf = {}\n",
|
||||
"for t in tables:\n",
|
||||
" tdf[t] = pd.read_sql(f\"SELECT * from {t}\", conn)\n",
|
||||
" tables = pd.read_sql(\n",
|
||||
" \"SELECT name FROM sqlite_master WHERE type='table';\", conn\n",
|
||||
" ).name.tolist()\n",
|
||||
" tdf = {}\n",
|
||||
" for t in tables:\n",
|
||||
" tdf[t] = pd.read_sql(f\"SELECT * from {t}\", conn)\n",
|
||||
"\n",
|
||||
"example_time = pd.to_datetime(\n",
|
||||
" tdf[\"flights\"][\"actual_departure\"].replace(\"\\\\N\", pd.NaT)\n",
|
||||
").max()\n",
|
||||
"current_time = pd.to_datetime(\"now\").tz_localize(example_time.tz)\n",
|
||||
"time_diff = current_time - example_time\n",
|
||||
" example_time = pd.to_datetime(\n",
|
||||
" tdf[\"flights\"][\"actual_departure\"].replace(\"\\\\N\", pd.NaT)\n",
|
||||
" ).max()\n",
|
||||
" current_time = pd.to_datetime(\"now\").tz_localize(example_time.tz)\n",
|
||||
" time_diff = current_time - example_time\n",
|
||||
"\n",
|
||||
"tdf[\"bookings\"][\"book_date\"] = (\n",
|
||||
" pd.to_datetime(tdf[\"bookings\"][\"book_date\"].replace(\"\\\\N\", pd.NaT), utc=True)\n",
|
||||
" + time_diff\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"datetime_columns = [\n",
|
||||
" \"scheduled_departure\",\n",
|
||||
" \"scheduled_arrival\",\n",
|
||||
" \"actual_departure\",\n",
|
||||
" \"actual_arrival\",\n",
|
||||
"]\n",
|
||||
"for column in datetime_columns:\n",
|
||||
" tdf[\"flights\"][column] = (\n",
|
||||
" pd.to_datetime(tdf[\"flights\"][column].replace(\"\\\\N\", pd.NaT)) + time_diff\n",
|
||||
" tdf[\"bookings\"][\"book_date\"] = (\n",
|
||||
" pd.to_datetime(tdf[\"bookings\"][\"book_date\"].replace(\"\\\\N\", pd.NaT), utc=True)\n",
|
||||
" + time_diff\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"for table_name, df in tdf.items():\n",
|
||||
" df.to_sql(table_name, conn, if_exists=\"replace\", index=False)\n",
|
||||
"del df\n",
|
||||
"del tdf\n",
|
||||
"conn.commit()\n",
|
||||
"conn.close()\n",
|
||||
" datetime_columns = [\n",
|
||||
" \"scheduled_departure\",\n",
|
||||
" \"scheduled_arrival\",\n",
|
||||
" \"actual_departure\",\n",
|
||||
" \"actual_arrival\",\n",
|
||||
" ]\n",
|
||||
" for column in datetime_columns:\n",
|
||||
" tdf[\"flights\"][column] = (\n",
|
||||
" pd.to_datetime(tdf[\"flights\"][column].replace(\"\\\\N\", pd.NaT)) + time_diff\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"db = local_file # We'll be using this local file as our DB in this tutorial"
|
||||
" for table_name, df in tdf.items():\n",
|
||||
" df.to_sql(table_name, conn, if_exists=\"replace\", index=False)\n",
|
||||
" del df\n",
|
||||
" del tdf\n",
|
||||
" conn.commit()\n",
|
||||
" conn.close()\n",
|
||||
"\n",
|
||||
" return file\n",
|
||||
"\n",
|
||||
"db = update_dates(local_file)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1750,7 +1754,7 @@
|
||||
"]\n",
|
||||
"\n",
|
||||
"# Update with the backup file so we can restart from the original place in each section\n",
|
||||
"shutil.copy(backup_file, db)\n",
|
||||
"db = update_dates(db)\n",
|
||||
"thread_id = str(uuid.uuid4())\n",
|
||||
"\n",
|
||||
"config = {\n",
|
||||
@@ -2304,7 +2308,7 @@
|
||||
"import uuid\n",
|
||||
"\n",
|
||||
"# Update with the backup file so we can restart from the original place in each section\n",
|
||||
"shutil.copy(backup_file, db)\n",
|
||||
"db = update_dates(db)\n",
|
||||
"thread_id = str(uuid.uuid4())\n",
|
||||
"\n",
|
||||
"config = {\n",
|
||||
@@ -2908,7 +2912,7 @@
|
||||
"import uuid\n",
|
||||
"\n",
|
||||
"# Update with the backup file so we can restart from the original place in each section\n",
|
||||
"shutil.copy(backup_file, db)\n",
|
||||
"db = update_dates(db)\n",
|
||||
"thread_id = str(uuid.uuid4())\n",
|
||||
"\n",
|
||||
"config = {\n",
|
||||
@@ -4330,7 +4334,7 @@
|
||||
"import uuid\n",
|
||||
"\n",
|
||||
"# Update with the backup file so we can restart from the original place in each section\n",
|
||||
"shutil.copy(backup_file, db)\n",
|
||||
"db = update_dates(db)\n",
|
||||
"thread_id = str(uuid.uuid4())\n",
|
||||
"\n",
|
||||
"config = {\n",
|
||||
|
||||
@@ -211,13 +211,6 @@
|
||||
"source": [
|
||||
"runnable.invoke(HumanMessage(\"What is your name?\"))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -680,7 +680,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.1"
|
||||
"version": "3.11.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -1265,7 +1265,6 @@
|
||||
"\n",
|
||||
"from langchain_anthropic import ChatAnthropic\n",
|
||||
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
|
||||
"from langchain_core.messages import BaseMessage\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
@@ -1504,7 +1503,7 @@
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.graph import StateGraph\n",
|
||||
"from langgraph.graph.message import add_messages\n",
|
||||
"from langgraph.prebuilt import ToolNode\n",
|
||||
"from langgraph.prebuilt import ToolNode, tools_condition\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class State(TypedDict):\n",
|
||||
@@ -1583,7 +1582,6 @@
|
||||
"\n",
|
||||
"from langchain_anthropic import ChatAnthropic\n",
|
||||
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
|
||||
"from langchain_core.messages import BaseMessage\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
@@ -1698,7 +1696,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from langchain_core.messages import AIMessage\n",
|
||||
"from langchain_core.messages import AIMessage, ToolMessage\n",
|
||||
"\n",
|
||||
"answer = (\n",
|
||||
" \"LangGraph is a library for building stateful, multi-actor applications with LLMs.\"\n",
|
||||
@@ -2082,7 +2080,6 @@
|
||||
"\n",
|
||||
"from langchain_anthropic import ChatAnthropic\n",
|
||||
"from langchain_community.tools.tavily_search import TavilySearchResults\n",
|
||||
"from langchain_core.messages import BaseMessage\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
|
||||
@@ -646,11 +646,12 @@
|
||||
"def _parse_joiner_output(decision: JoinOutputs) -> List[BaseMessage]:\n",
|
||||
" response = [AIMessage(content=f\"Thought: {decision.thought}\")]\n",
|
||||
" if isinstance(decision.action, Replan):\n",
|
||||
" return response + [\n",
|
||||
" return {\"messages\": response + [\n",
|
||||
" SystemMessage(\n",
|
||||
" content=f\"Context from last attempt: {decision.action.feedback}\"\n",
|
||||
" )\n",
|
||||
" ]\n",
|
||||
" }\n",
|
||||
" else:\n",
|
||||
" return {\"messages\": response + [AIMessage(content=decision.action.response)]}\n",
|
||||
"\n",
|
||||
@@ -933,6 +934,46 @@
|
||||
"print(step['join']['messages'][-1].content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "f9487866",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"#### Complex Replanning Example\n",
|
||||
"\n",
|
||||
"This question is likely to prompt the Replan functionality, but it may need to be run multiple times to see this in action."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "391d6931",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'plan_and_schedule': {'messages': [FunctionMessage(content=\"[{'url': 'https://www.timeanddate.com/weather/japan/tokyo', 'content': '88 / 84 °F. 13. 87 / 82 °F. 14. 84 / 80 °F. Detailed forecast for 14 days. Need some help? Current weather in Tokyo and forecast for today, tomorrow, and next 14 days.'}]\", additional_kwargs={'idx': 1, 'args': {'query': 'current temperature in Tokyo'}}, name='tavily_search_results_json', tool_call_id=1), FunctionMessage(content='join', additional_kwargs={'idx': 2, 'args': ()}, name='join', tool_call_id=2)]}}\n",
|
||||
"{'join': {'messages': [AIMessage(content=\"Thought: The search result provides the current temperature in Tokyo but does not explicitly state which temperature (88 / 84 °F) corresponds to the current condition. It seems to be a range, possibly the day's high and low. Without a clear indication of the exact current temperature, it's challenging to provide a precise flashcard summary.\", id='8ef2a131-69db-4180-a76e-fd9d6f4037c1'), SystemMessage(content='Context from last attempt: The information provided does not explicitly state the current temperature in Tokyo; it provides a temperature range without specifying which is the current temperature. Need to find a source that gives the exact current temperature in Tokyo for a precise flashcard summary.', id='f5bd752c-b068-459a-8d9e-bd1f1b5fa4fe')]}}\n",
|
||||
"{'plan_and_schedule': {'messages': [FunctionMessage(content='join', additional_kwargs={'idx': 3, 'args': ()}, name='join', tool_call_id=3)]}}\n",
|
||||
"{'join': {'messages': [AIMessage(content=\"Thought: The search result provides a temperature range for Tokyo but does not specify the current temperature. This makes it challenging to create a precise flashcard without an exact current temperature. The user's request cannot be fully satisfied without this detail.\", id='3cc41891-4f47-4453-8edf-b989926ab25e'), SystemMessage(content='Context from last attempt: The search did not provide an exact current temperature for Tokyo, making it impossible to create a precise flashcard. A source that explicitly states the current temperature is needed for an accurate response.', id='96290b41-a4c4-4ab5-829a-89cc31dfe6c8')]}}\n",
|
||||
"{'plan_and_schedule': {'messages': [FunctionMessage(content='join', additional_kwargs={'idx': 4, 'args': ()}, name='join', tool_call_id=4)]}}\n",
|
||||
"{'join': {'messages': [AIMessage(content=\"Thought: The search result provides a temperature range for Tokyo but does not specify the current temperature. This makes it challenging to create a precise flashcard without an exact current temperature. The user's request cannot be fully satisfied without this detail.\", id='4724b242-ddb8-47e6-b235-de25de54fe45'), AIMessage(content='I was unable to find the exact current temperature in Tokyo. However, the temperature range for today in Tokyo is between 88°F and 84°F. For the most accurate and up-to-date temperature, I recommend checking a reliable weather forecasting website or app.', id='40e29a47-a001-4f65-a18f-65c2931d1ae5')]}}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for step in chain.stream({\"messages\":\n",
|
||||
" [\n",
|
||||
" HumanMessage(\n",
|
||||
" content=\"Find the current temperature in Tokyo, then, respond with a flashcard summarizing this information\"\n",
|
||||
" )\n",
|
||||
" ]}\n",
|
||||
"):\n",
|
||||
" print(step)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c647d5f3-5e00-4449-9cec-5a9f438c9cff",
|
||||
|
||||
@@ -6,10 +6,10 @@ import numexpr
|
||||
from langchain.chains.openai_functions import create_structured_output_runnable
|
||||
from langchain_core.messages import SystemMessage
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain_core.pydantic_v1 import BaseModel, Field
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langchain_core.tools import StructuredTool
|
||||
from langchain_openai import ChatOpenAI
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
_MATH_DESCRIPTION = (
|
||||
"math(problem: str, context: Optional[list[str]]) -> float:\n"
|
||||
|
||||
@@ -219,7 +219,7 @@
|
||||
"workflow = StateGraph(AgentState)\n",
|
||||
"workflow.add_node(\"Researcher\", research_node)\n",
|
||||
"workflow.add_node(\"Coder\", code_node)\n",
|
||||
"workflow.add_node(\"supervisor\", supervisor_chain)"
|
||||
"workflow.add_node(\"supervisor\", supervisor_agent)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -132,8 +132,6 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from psycopg.rows import dict_row\n",
|
||||
"\n",
|
||||
"connection_kwargs = {\n",
|
||||
" \"autocommit\": True,\n",
|
||||
" \"prepare_threshold\": 0,\n",
|
||||
@@ -161,15 +159,13 @@
|
||||
"source": [
|
||||
"from psycopg_pool import ConnectionPool\n",
|
||||
"\n",
|
||||
"pool = ConnectionPool(\n",
|
||||
"with ConnectionPool(\n",
|
||||
" # Example configuration\n",
|
||||
" conninfo=DB_URI,\n",
|
||||
" max_size=20,\n",
|
||||
" kwargs=connection_kwargs,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"with pool.connection() as conn:\n",
|
||||
" checkpointer = PostgresSaver(conn)\n",
|
||||
") as pool:\n",
|
||||
" checkpointer = PostgresSaver(pool)\n",
|
||||
"\n",
|
||||
" # NOTE: you need to call .setup() the first time you're using your checkpointer\n",
|
||||
" checkpointer.setup()\n",
|
||||
@@ -394,8 +390,8 @@
|
||||
" conninfo=DB_URI,\n",
|
||||
" max_size=20,\n",
|
||||
" kwargs=connection_kwargs,\n",
|
||||
") as pool, pool.connection() as conn:\n",
|
||||
" checkpointer = AsyncPostgresSaver(conn)\n",
|
||||
") as pool:\n",
|
||||
" checkpointer = AsyncPostgresSaver(pool)\n",
|
||||
"\n",
|
||||
" # NOTE: you need to call .setup() the first time you're using your checkpointer\n",
|
||||
" # await checkpointer.setup()\n",
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
"source": [
|
||||
"## Agent state\n",
|
||||
" \n",
|
||||
"We will defined a graph.\n",
|
||||
"We will define a graph.\n",
|
||||
"\n",
|
||||
"A `state` object that it passes around to each node.\n",
|
||||
"\n",
|
||||
|
||||
@@ -429,6 +429,14 @@
|
||||
"]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def find_tool_calls_react(messages):\n",
|
||||
" \"\"\"\n",
|
||||
" Find all tool calls in the messages returned\n",
|
||||
" \"\"\"\n",
|
||||
" tool_calls = [tc['name'] for m in messages['messages'] for tc in getattr(m, 'tool_calls', [])]\n",
|
||||
" return tool_calls\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def check_trajectory_react(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",
|
||||
|
||||
@@ -573,7 +573,7 @@
|
||||
],
|
||||
"source": [
|
||||
"events = graph.stream(\n",
|
||||
" [HumanMessage(content=\"How should we handle the climate crisis?\")],\n",
|
||||
" {\"messages\": [(\"user\", \"How should we handle the climate crisis?\")]},\n",
|
||||
" stream_mode=\"values\",\n",
|
||||
")\n",
|
||||
"for i, step in enumerate(events):\n",
|
||||
|
||||
@@ -1667,13 +1667,6 @@
|
||||
"# We will down-header the sections to create less confusion in this notebook\n",
|
||||
"Markdown(article.replace(\"\\n#\", \"\\n##\"))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to transform inputs and outputs of a subgraph\n",
|
||||
"\n",
|
||||
"It's possible that your subgraph state is completely independent from the parent graph state, i.e. there are no overlapping channels (keys) between the two. For example, you might have a supervisor agent that needs to produce a report with a help of multiple ReAct agents. ReAct agent subgraphs might keep track of a list of messages whereas the supervisor only needs user input and final report in its state, and doesn't need to keep track of messages.\n",
|
||||
"\n",
|
||||
"In such cases you need to transform the inputs to the subgraph before calling it and then transform its outputs before returning. This guide shows how to do that."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph"
|
||||
]
|
||||
},
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Define graph and subgraphs\n",
|
||||
"\n",
|
||||
"Let's define 3 graphs:\n",
|
||||
"- a parent graph\n",
|
||||
"- a child subgraph that will be called by the parent graph\n",
|
||||
"- a grandchild subgraph that will be called by the child graph"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Define grandchild"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from typing import TypedDict\n",
|
||||
"from langgraph.graph.state import StateGraph, START, END\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class GrandChildState(TypedDict):\n",
|
||||
" my_grandchild_key: str\n",
|
||||
"\n",
|
||||
"def grandchild_1(state: GrandChildState) -> GrandChildState:\n",
|
||||
" # NOTE: child or parent keys will not be accessible here\n",
|
||||
" return {\"my_grandchild_key\": state[\"my_grandchild_key\"] + \", how are you\"}\n",
|
||||
"\n",
|
||||
"grandchild = StateGraph(GrandChildState)\n",
|
||||
"grandchild.add_node(\"grandchild_1\", grandchild_1)\n",
|
||||
"\n",
|
||||
"grandchild.add_edge(START, \"grandchild_1\")\n",
|
||||
"grandchild.add_edge(\"grandchild_1\", END)\n",
|
||||
"\n",
|
||||
"grandchild_graph = grandchild.compile()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'my_grandchild_key': 'hi Bob, how are you'}"
|
||||
]
|
||||
},
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"grandchild_graph.invoke({\"my_grandchild_key\": \"hi Bob\"})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Define child"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"class ChildState(TypedDict):\n",
|
||||
" my_child_key: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def call_grandchild_graph(state: ChildState) -> ChildState:\n",
|
||||
" # NOTE: parent or grandchild keys won't be accessible here\n",
|
||||
" # we're transforming the state from the child state channels (`my_child_key`)\n",
|
||||
" # to the child state channels (`my_grandchild_key`)\n",
|
||||
" grandchild_graph_input = {\"my_grandchild_key\": state[\"my_child_key\"]}\n",
|
||||
" # we're transforming the state from the grandchild state channels (`my_grandchild_key`)\n",
|
||||
" # back to the child state channels (`my_child_key`)\n",
|
||||
" grandchild_graph_output = grandchild_graph.invoke(grandchild_graph_input)\n",
|
||||
" return {\"my_child_key\": grandchild_graph_output[\"my_grandchild_key\"] + \" today?\"}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"child = StateGraph(ChildState)\n",
|
||||
"# NOTE: we're passing a function here instead of just compiled graph (`child_graph`)\n",
|
||||
"child.add_node(\"child_1\", call_grandchild_graph)\n",
|
||||
"child.add_edge(START, \"child_1\")\n",
|
||||
"child.add_edge(\"child_1\", END)\n",
|
||||
"child_graph = child.compile()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'my_child_key': 'hi Bob, how are you today?'}"
|
||||
]
|
||||
},
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"child_graph.invoke({\"my_child_key\": \"hi Bob\"})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div class=\"admonition info\">\n",
|
||||
" <p class=\"admonition-title\">Note</p>\n",
|
||||
" <p>\n",
|
||||
" We're wrapping the <code>grandchild_graph</code> invocation in a separate function (<code>call_grandchild_graph</code>) that transforms the input state before calling the grandchild graph and then transforms the output of grandchild graph back to child graph state. If you just pass <code>grandchild_graph</code> directly to <code>.add_node</code> without the transformations, LangGraph will raise an error as there are no shared state channels (keys) between child and grandchild states.\n",
|
||||
" </p>\n",
|
||||
"</div> "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Note that child and grandchild subgraphs have their own, **independent** state that is not shared with the parent graph."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Define parent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"class ParentState(TypedDict):\n",
|
||||
" my_key: str\n",
|
||||
" \n",
|
||||
"def parent_1(state: ParentState) -> ParentState:\n",
|
||||
" # NOTE: child or grandchild keys won't be accessible here\n",
|
||||
" return {\"my_key\": \"hi \" + state[\"my_key\"]}\n",
|
||||
"\n",
|
||||
"def parent_2(state: ParentState) -> ParentState:\n",
|
||||
" return {\"my_key\": state[\"my_key\"] + \" bye!\"}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def call_child_graph(state: ParentState) -> ParentState:\n",
|
||||
" # we're transforming the state from the parent state channels (`my_key`)\n",
|
||||
" # to the child state channels (`my_child_key`)\n",
|
||||
" child_graph_input = {\"my_child_key\": state[\"my_key\"]}\n",
|
||||
" # we're transforming the state from the child state channels (`my_child_key`)\n",
|
||||
" # back to the parent state channels (`my_key`)\n",
|
||||
" child_graph_output = child_graph.invoke(child_graph_input)\n",
|
||||
" return {\"my_key\": child_graph_output[\"my_child_key\"]}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"parent = StateGraph(ParentState)\n",
|
||||
"parent.add_node(\"parent_1\", parent_1)\n",
|
||||
"# NOTE: we're passing a function here instead of just a compiled graph (`<code>child_graph</code>`)\n",
|
||||
"parent.add_node(\"child\", call_child_graph)\n",
|
||||
"parent.add_node(\"parent_2\", parent_2)\n",
|
||||
"\n",
|
||||
"parent.add_edge(START, \"parent_1\")\n",
|
||||
"parent.add_edge(\"parent_1\", \"child\")\n",
|
||||
"parent.add_edge(\"child\", \"parent_2\")\n",
|
||||
"parent.add_edge(\"parent_2\", END)\n",
|
||||
"\n",
|
||||
"parent_graph = parent.compile()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div class=\"admonition info\">\n",
|
||||
" <p class=\"admonition-title\">Note</p>\n",
|
||||
" <p>\n",
|
||||
" We're wrapping the <code>child_graph</code> invocation in a separate function (<code>call_child_graph</code>) that transforms the input state before calling the child graph and then transforms the output of the child graph back to parent graph state. If you just pass <code>child_graph</code> directly to <code>.add_node</code> without the transformations, LangGraph will raise an error as there are no shared state channels (keys) between parent and child states.\n",
|
||||
" </p>\n",
|
||||
"</div> \n",
|
||||
"\n",
|
||||
"Let's run the parent graph and make sure it correctly calls both the child and grandchild subgraphs:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'my_key': 'hi Bob, how are you today? bye!'}"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"parent_graph.invoke({\"my_key\": \"Bob\"})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Perfect! The parent graph correctly calls both the child and grandchild subgraphs (which we know since the \", how are you\" and \"today?\" are added to our original \"my_key\" state value)."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "langgraph",
|
||||
"language": "python",
|
||||
"name": "langgraph"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
+236
-34
@@ -46,7 +46,10 @@
|
||||
"id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_openai"]
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install --quiet -U langgraph langchain_openai"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -62,7 +65,18 @@
|
||||
"id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"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(\"ANTHROPIC_API_KEY\")"]
|
||||
"source": [
|
||||
"import getpass\n",
|
||||
"import os\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _set_env(var: str):\n",
|
||||
" if not os.environ.get(var):\n",
|
||||
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"_set_env(\"OPENAI_API_KEY\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -78,7 +92,10 @@
|
||||
"id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"]
|
||||
"source": [
|
||||
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
|
||||
"_set_env(\"LANGCHAIN_API_KEY\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -96,7 +113,22 @@
|
||||
"id": "f5319e01",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# `add_messages`` essentially does this\n# (with more robust handling)\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"]
|
||||
"source": [
|
||||
"from typing import Annotated\n",
|
||||
"\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"from langgraph.graph.message import add_messages\n",
|
||||
"\n",
|
||||
"# `add_messages`` essentially does this\n",
|
||||
"# (with more robust handling)\n",
|
||||
"# def add_messages(left: list, right: list):\n",
|
||||
"# return left + right\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class State(TypedDict):\n",
|
||||
" messages: Annotated[list, add_messages]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -116,7 +148,19 @@
|
||||
"id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n return [\"The weather is cloudy with a chance of meatballs.\"]\n\n\ntools = [search]"]
|
||||
"source": [
|
||||
"from langchain_core.tools import tool\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@tool\n",
|
||||
"def search(query: str):\n",
|
||||
" \"\"\"Call to surf the web.\"\"\"\n",
|
||||
" # This is a placeholder for the actual implementation\n",
|
||||
" return [\"The weather is cloudy with a chance of meatballs.\"]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"tools = [search]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -133,7 +177,11 @@
|
||||
"id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langgraph.prebuilt import ToolNode\n\ntool_node = ToolNode(tools)"]
|
||||
"source": [
|
||||
"from langgraph.prebuilt import ToolNode\n",
|
||||
"\n",
|
||||
"tool_node = ToolNode(tools)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -157,7 +205,11 @@
|
||||
"id": "892b54b9-75f0-4804-9ed0-88b5e5532989",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"]
|
||||
"source": [
|
||||
"from langchain_openai import ChatOpenAI\n",
|
||||
"\n",
|
||||
"model = ChatOpenAI(temperature=0)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -175,7 +227,9 @@
|
||||
"id": "cd3cbae5-d92c-4559-a4aa-44721b80d107",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["model = model.bind_tools(tools)"]
|
||||
"source": [
|
||||
"model = model.bind_tools(tools)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -210,7 +264,20 @@
|
||||
"id": "3b541bb9-900c-40d0-964d-7b5dfee30667",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from typing import Literal\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state: State) -> Literal[\"continue\", \"end\"]:\n last_message = state[\"messages\"][-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\""]
|
||||
"source": [
|
||||
"from typing import Literal\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Define the function that determines whether to continue or not\n",
|
||||
"def should_continue(state: State) -> Literal[\"continue\", \"end\"]:\n",
|
||||
" last_message = state[\"messages\"][-1]\n",
|
||||
" # If there is no function call, then we finish\n",
|
||||
" if not last_message.tool_calls:\n",
|
||||
" return \"end\"\n",
|
||||
" # Otherwise if there is, we continue\n",
|
||||
" else:\n",
|
||||
" return \"continue\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -228,7 +295,50 @@
|
||||
"id": "812b4e70-4956-4415-8880-db48b3dcbad2",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(State)\n\n\n# Define the two nodes we will cycle between\ndef call_model(state: State) -> State:\n return {\"messages\": model.invoke(state[\"messages\"])}\n\n\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")"]
|
||||
"source": [
|
||||
"from langgraph.graph import END, StateGraph, START\n",
|
||||
"\n",
|
||||
"# Define a new graph\n",
|
||||
"workflow = StateGraph(State)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Define the two nodes we will cycle between\n",
|
||||
"def call_model(state: State) -> State:\n",
|
||||
" return {\"messages\": model.invoke(state[\"messages\"])}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"workflow.add_node(\"agent\", call_model)\n",
|
||||
"workflow.add_node(\"action\", tool_node)\n",
|
||||
"\n",
|
||||
"# Set the entrypoint as `agent`\n",
|
||||
"# This means that this node is the first one called\n",
|
||||
"workflow.add_edge(START, \"agent\")\n",
|
||||
"\n",
|
||||
"# We now add a conditional edge\n",
|
||||
"workflow.add_conditional_edges(\n",
|
||||
" # First, we define the start node. We use `agent`.\n",
|
||||
" # This means these are the edges taken after the `agent` node is called.\n",
|
||||
" \"agent\",\n",
|
||||
" # Next, we pass in the function that will determine which node is called next.\n",
|
||||
" should_continue,\n",
|
||||
" # Finally we pass in a mapping.\n",
|
||||
" # The keys are strings, and the values are other nodes.\n",
|
||||
" # END is a special node marking that the graph should finish.\n",
|
||||
" # What will happen is we will call `should_continue`, and then the output of that\n",
|
||||
" # will be matched against the keys in this mapping.\n",
|
||||
" # Based on which one it matches, that node will then be called.\n",
|
||||
" {\n",
|
||||
" # If `tools`, then we call the tool node.\n",
|
||||
" \"continue\": \"action\",\n",
|
||||
" # Otherwise we finish.\n",
|
||||
" \"end\": END,\n",
|
||||
" },\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# We now add a normal edge from `tools` to `agent`.\n",
|
||||
"# This means that after `tools` is called, `agent` node is called next.\n",
|
||||
"workflow.add_edge(\"action\", \"agent\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -246,7 +356,11 @@
|
||||
"id": "6845ed6a-d155-4105-9160-28849877248b",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langgraph.checkpoint.memory import MemorySaver\n\nmemory = MemorySaver()"]
|
||||
"source": [
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"\n",
|
||||
"memory = MemorySaver()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -254,7 +368,12 @@
|
||||
"id": "79d29875-8aa8-434c-9f20-1c58346a6249",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile(checkpointer=memory)"]
|
||||
"source": [
|
||||
"# Finally, we compile it!\n",
|
||||
"# This compiles it into a LangChain Runnable,\n",
|
||||
"# meaning you can use it as you would any other runnable\n",
|
||||
"app = workflow.compile(checkpointer=memory)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -281,7 +400,15 @@
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": ["from IPython.display import Image, display\n\ntry:\n display(Image(app.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(app.get_graph().draw_mermaid_png()))\n",
|
||||
"except Exception:\n",
|
||||
" # This requires some extra dependencies and is optional\n",
|
||||
" pass"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -312,7 +439,14 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["from langchain_core.messages import HumanMessage\n\nconfig = {\"configurable\": {\"thread_id\": \"2\"}}\ninput_message = HumanMessage(content=\"hi! I'm bob\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
|
||||
"source": [
|
||||
"from langchain_core.messages import HumanMessage\n",
|
||||
"\n",
|
||||
"config = {\"configurable\": {\"thread_id\": \"2\"}}\n",
|
||||
"input_message = HumanMessage(content=\"hi! I'm bob\")\n",
|
||||
"for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
|
||||
" event[\"messages\"][-1].pretty_print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -350,7 +484,9 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["app.get_state(config).values"]
|
||||
"source": [
|
||||
"app.get_state(config).values"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -379,7 +515,9 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["app.get_state(config).next"]
|
||||
"source": [
|
||||
"app.get_state(config).next"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -426,7 +564,12 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["config = {\"configurable\": {\"thread_id\": \"2\"}}\ninput_message = HumanMessage(content=\"what is the weather in sf currently\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"]
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"2\"}}\n",
|
||||
"input_message = HumanMessage(content=\"what is the weather in sf currently\")\n",
|
||||
"for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
|
||||
" event[\"messages\"][-1].pretty_print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -466,7 +609,9 @@
|
||||
"id": "5a68afc0-606f-4294-a872-b2b563be0d69",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["app_w_interrupt = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"]
|
||||
"source": [
|
||||
"app_w_interrupt = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -490,7 +635,14 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["config = {\"configurable\": {\"thread_id\": \"4\"}}\ninput_message = HumanMessage(content=\"what is the weather in sf currently\")\nfor event in app_w_interrupt.stream(\n {\"messages\": [input_message]}, config, stream_mode=\"values\"\n):\n event[\"messages\"][-1].pretty_print()"]
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"4\"}}\n",
|
||||
"input_message = HumanMessage(content=\"what is the weather in sf currently\")\n",
|
||||
"for event in app_w_interrupt.stream(\n",
|
||||
" {\"messages\": [input_message]}, config, stream_mode=\"values\"\n",
|
||||
"):\n",
|
||||
" event[\"messages\"][-1].pretty_print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -526,7 +678,10 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["current_values = app_w_interrupt.get_state(config)\ncurrent_values.next"]
|
||||
"source": [
|
||||
"current_values = app_w_interrupt.get_state(config)\n",
|
||||
"current_values.next"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -555,7 +710,9 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["current_values.values[\"messages\"][-1].tool_calls"]
|
||||
"source": [
|
||||
"current_values.values[\"messages\"][-1].tool_calls"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -571,7 +728,11 @@
|
||||
"id": "060e2e33-1f6a-40ef-850e-161b308986fb",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["current_values.values[\"messages\"][-1].tool_calls[0][\"args\"][\n \"query\"\n] = \"weather in San Francisco today\""]
|
||||
"source": [
|
||||
"current_values.values[\"messages\"][-1].tool_calls[0][\"args\"][\n",
|
||||
" \"query\"\n",
|
||||
"] = \"weather in San Francisco today\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -591,7 +752,9 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["app_w_interrupt.update_state(config, current_values.values)"]
|
||||
"source": [
|
||||
"app_w_interrupt.update_state(config, current_values.values)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -629,7 +792,9 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["app_w_interrupt.get_state(config).values"]
|
||||
"source": [
|
||||
"app_w_interrupt.get_state(config).values"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -648,7 +813,9 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["app_w_interrupt.get_state(config).next"]
|
||||
"source": [
|
||||
"app_w_interrupt.get_state(config).next"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -679,7 +846,11 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["for event in app_w_interrupt.stream(None, config):\n for v in event.values():\n print(v)"]
|
||||
"source": [
|
||||
"for event in app_w_interrupt.stream(None, config):\n",
|
||||
" for v in event.values():\n",
|
||||
" print(v)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -726,7 +897,13 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["for state in app_w_interrupt.get_state_history(config):\n print(state)\n print(\"--\")\n if len(state.values[\"messages\"]) == 2:\n to_replay = state"]
|
||||
"source": [
|
||||
"for state in app_w_interrupt.get_state_history(config):\n",
|
||||
" print(state)\n",
|
||||
" print(\"--\")\n",
|
||||
" if len(state.values[\"messages\"]) == 2:\n",
|
||||
" to_replay = state"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -754,7 +931,9 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["to_replay.values"]
|
||||
"source": [
|
||||
"to_replay.values"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -773,7 +952,9 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["to_replay.next"]
|
||||
"source": [
|
||||
"to_replay.next"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -806,7 +987,11 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": ["for event in app_w_interrupt.stream(None, to_replay.config):\n for v in event.values():\n print(v)"]
|
||||
"source": [
|
||||
"for event in app_w_interrupt.stream(None, to_replay.config):\n",
|
||||
" for v in event.values():\n",
|
||||
" print(v)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -834,7 +1019,18 @@
|
||||
"id": "b084f141-5800-487b-b115-d2e58421b963",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langchain_core.messages import AIMessage\n\nbranch_config = app_w_interrupt.update_state(\n to_replay.config,\n {\n \"messages\": [\n AIMessage(content=\"All done here!\", id=to_replay.values[\"messages\"][-1].id)\n ]\n },\n)"]
|
||||
"source": [
|
||||
"from langchain_core.messages import AIMessage\n",
|
||||
"\n",
|
||||
"branch_config = app_w_interrupt.update_state(\n",
|
||||
" to_replay.config,\n",
|
||||
" {\n",
|
||||
" \"messages\": [\n",
|
||||
" AIMessage(content=\"All done here!\", id=to_replay.values[\"messages\"][-1].id)\n",
|
||||
" ]\n",
|
||||
" },\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -842,7 +1038,9 @@
|
||||
"id": "1a7cfcd4-289e-419e-8b49-dfaef4f88641",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["branch_state = app_w_interrupt.get_state(branch_config)"]
|
||||
"source": [
|
||||
"branch_state = app_w_interrupt.get_state(branch_config)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -862,7 +1060,9 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["branch_state.values"]
|
||||
"source": [
|
||||
"branch_state.values"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -881,7 +1081,9 @@
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": ["branch_state.next"]
|
||||
"source": [
|
||||
"branch_state.next"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
|
||||
@@ -604,14 +604,6 @@
|
||||
"\n",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "8eb46884-08aa-4ab4-8bb9-f72277c3b35b",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -1266,14 +1266,6 @@
|
||||
"\n",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "bfa533b4-007f-4f86-b146-02c56a4e667a",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -337,14 +337,6 @@
|
||||
"\n",
|
||||
"https://smith.langchain.com/public/7a4938e3-f94f-4e04-a162-bf592fba4643/r"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "74b813cb-18ed-42d8-b313-6ee56ded4bcc",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any, AsyncIterator, Optional, Union
|
||||
from typing import Any, AsyncIterator, Iterator, List, Optional, Union
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline
|
||||
@@ -51,6 +51,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
self.conn = conn
|
||||
self.pipe = pipe
|
||||
self.lock = asyncio.Lock()
|
||||
self.loop = asyncio.get_running_loop()
|
||||
|
||||
@classmethod
|
||||
@asynccontextmanager
|
||||
@@ -329,3 +330,96 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
binary=True, row_factory=dict_row
|
||||
) as cur:
|
||||
yield cur
|
||||
|
||||
def list(
|
||||
self,
|
||||
config: Optional[RunnableConfig],
|
||||
*,
|
||||
filter: Optional[dict[str, Any]] = None,
|
||||
before: Optional[RunnableConfig] = None,
|
||||
limit: Optional[int] = None,
|
||||
) -> Iterator[CheckpointTuple]:
|
||||
"""List checkpoints from the database.
|
||||
|
||||
This method retrieves a list of checkpoint tuples from the Postgres database based
|
||||
on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
|
||||
|
||||
Args:
|
||||
config (Optional[RunnableConfig]): Base configuration for filtering checkpoints.
|
||||
filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata.
|
||||
before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
||||
limit (Optional[int]): Maximum number of checkpoints to return.
|
||||
|
||||
Yields:
|
||||
Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
|
||||
"""
|
||||
aiter_ = self.alist(config, filter=filter, before=before, limit=limit)
|
||||
while True:
|
||||
try:
|
||||
yield asyncio.run_coroutine_threadsafe(
|
||||
anext(aiter_), self.loop
|
||||
).result()
|
||||
except StopAsyncIteration:
|
||||
break
|
||||
|
||||
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||
"""Get a checkpoint tuple from the database.
|
||||
|
||||
This method retrieves a checkpoint tuple from the Postgres database based on the
|
||||
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||
the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint
|
||||
for the given thread ID is retrieved.
|
||||
|
||||
Args:
|
||||
config (RunnableConfig): The config to use for retrieving the checkpoint.
|
||||
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
"""
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
self.aget_tuple(config), self.loop
|
||||
).result()
|
||||
|
||||
def put(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
checkpoint: Checkpoint,
|
||||
metadata: CheckpointMetadata,
|
||||
new_versions: ChannelVersions,
|
||||
) -> RunnableConfig:
|
||||
"""Save a checkpoint to the database.
|
||||
|
||||
This method saves a checkpoint to the Postgres database. The checkpoint is associated
|
||||
with the provided config and its parent config (if any).
|
||||
|
||||
Args:
|
||||
config (RunnableConfig): The config to associate with the checkpoint.
|
||||
checkpoint (Checkpoint): The checkpoint to save.
|
||||
metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.
|
||||
new_versions (ChannelVersions): New channel versions as of this write.
|
||||
|
||||
Returns:
|
||||
RunnableConfig: Updated configuration after storing the checkpoint.
|
||||
"""
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
self.aput(config, checkpoint, metadata, new_versions), self.loop
|
||||
).result()
|
||||
|
||||
def put_writes(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
writes: List[tuple[str, Any]],
|
||||
task_id: str,
|
||||
) -> None:
|
||||
"""Store intermediate writes linked to a checkpoint.
|
||||
|
||||
This method saves intermediate writes associated with a checkpoint to the database.
|
||||
|
||||
Args:
|
||||
config (RunnableConfig): Configuration of the related checkpoint.
|
||||
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.
|
||||
"""
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
self.aput_writes(config, writes, task_id), self.loop
|
||||
).result()
|
||||
|
||||
Generated
+3
-4
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.3 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.6"
|
||||
version = "1.0.8"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -766,7 +766,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||
@@ -972,4 +971,4 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
content-hash = "b139531e8c6f4e24cea4bdfc29d111d1ea000a6a8a604ba81be4bff0977a2466"
|
||||
content-hash = "e294b6996aa6c8f671e6aaf65be8b4aba94c18e2f237dd3ee0e1b777849ce8a8"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "1.0.5"
|
||||
version = "1.0.6"
|
||||
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0,<4.0"
|
||||
langgraph-checkpoint = "^1.0.1"
|
||||
langgraph-checkpoint = "^1.0.8"
|
||||
orjson = ">=3.10.1"
|
||||
psycopg = "^3.0.0"
|
||||
psycopg-pool = "^3.0.0"
|
||||
|
||||
@@ -104,10 +104,12 @@ class SqliteSaver(BaseCheckpointSaver):
|
||||
with SqliteSaver.from_conn_string("checkpoints.sqlite") as memory:
|
||||
...
|
||||
"""
|
||||
with sqlite3.connect(
|
||||
conn_string,
|
||||
# https://ricardoanderegg.com/posts/python-sqlite-thread-safety/
|
||||
check_same_thread=False,
|
||||
with closing(
|
||||
sqlite3.connect(
|
||||
conn_string,
|
||||
# https://ricardoanderegg.com/posts/python-sqlite-thread-safety/
|
||||
check_same_thread=False,
|
||||
)
|
||||
) as conn:
|
||||
yield SqliteSaver(conn)
|
||||
|
||||
@@ -163,14 +165,15 @@ class SqliteSaver(BaseCheckpointSaver):
|
||||
Yields:
|
||||
sqlite3.Cursor: A cursor for the SQLite database.
|
||||
"""
|
||||
self.setup()
|
||||
cur = self.conn.cursor()
|
||||
try:
|
||||
yield cur
|
||||
finally:
|
||||
if transaction:
|
||||
self.conn.commit()
|
||||
cur.close()
|
||||
with self.lock:
|
||||
self.setup()
|
||||
cur = self.conn.cursor()
|
||||
try:
|
||||
yield cur
|
||||
finally:
|
||||
if transaction:
|
||||
self.conn.commit()
|
||||
cur.close()
|
||||
|
||||
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||
"""Get a checkpoint tuple from the database.
|
||||
@@ -396,7 +399,7 @@ class SqliteSaver(BaseCheckpointSaver):
|
||||
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
||||
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
|
||||
serialized_metadata = self.jsonplus_serde.dumps(metadata)
|
||||
with self.lock, self.cursor() as cur:
|
||||
with self.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
@@ -432,7 +435,7 @@ class SqliteSaver(BaseCheckpointSaver):
|
||||
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.
|
||||
"""
|
||||
with self.lock, self.cursor() as cur:
|
||||
with self.cursor() as cur:
|
||||
cur.executemany(
|
||||
"INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import asyncio
|
||||
import functools
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import (
|
||||
Any,
|
||||
AsyncIterator,
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
@@ -31,20 +31,6 @@ from langgraph.checkpoint.sqlite.utils import search_where
|
||||
T = TypeVar("T", bound=callable)
|
||||
|
||||
|
||||
def not_implemented_sync_method(func: T) -> T:
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
raise NotImplementedError(
|
||||
"The AsyncSqliteSaver does not support synchronous methods. "
|
||||
"Consider using the SqliteSaver instead.\n"
|
||||
"from langgraph.checkpoint.sqlite import SqliteSaver\n"
|
||||
"See https://langchain-ai.github.io/langgraph/reference/checkpoints/langgraph.checkpoint.sqlite.SqliteSaver "
|
||||
"for more information."
|
||||
)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
"""An asynchronous checkpoint saver that stores checkpoints in a SQLite database.
|
||||
|
||||
@@ -132,6 +118,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
self.jsonplus_serde = JsonPlusSerializer()
|
||||
self.conn = conn
|
||||
self.lock = asyncio.Lock()
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.is_setup = False
|
||||
|
||||
@classmethod
|
||||
@@ -150,16 +137,24 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
async with aiosqlite.connect(conn_string) as conn:
|
||||
yield AsyncSqliteSaver(conn)
|
||||
|
||||
@not_implemented_sync_method
|
||||
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||
"""Get a checkpoint tuple from the database.
|
||||
|
||||
Note:
|
||||
This method is not implemented for the AsyncSqliteSaver. Use `aget` instead.
|
||||
Or consider using the [SqliteSaver][sqlitesaver] checkpointer.
|
||||
"""
|
||||
This method retrieves a checkpoint tuple from the SQLite database based on the
|
||||
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||
the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
|
||||
for the given thread ID is retrieved.
|
||||
|
||||
Args:
|
||||
config (RunnableConfig): The config to use for retrieving the checkpoint.
|
||||
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
"""
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
self.aget_tuple(config), self.loop
|
||||
).result()
|
||||
|
||||
@not_implemented_sync_method
|
||||
def list(
|
||||
self,
|
||||
config: Optional[RunnableConfig],
|
||||
@@ -168,21 +163,60 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
before: Optional[RunnableConfig] = None,
|
||||
limit: Optional[int] = None,
|
||||
) -> Iterator[CheckpointTuple]:
|
||||
"""List checkpoints from the database.
|
||||
"""List checkpoints from the database asynchronously.
|
||||
|
||||
Note:
|
||||
This method is not implemented for the AsyncSqliteSaver. Use `alist` instead.
|
||||
Or consider using the [SqliteSaver][sqlitesaver] checkpointer.
|
||||
This method retrieves a list of checkpoint tuples from the SQLite database based
|
||||
on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
|
||||
|
||||
Args:
|
||||
config (Optional[RunnableConfig]): Base configuration for filtering checkpoints.
|
||||
filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata.
|
||||
before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
||||
limit (Optional[int]): Maximum number of checkpoints to return.
|
||||
|
||||
Yields:
|
||||
Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
|
||||
"""
|
||||
aiter_ = self.alist(config, filter=filter, before=before, limit=limit)
|
||||
while True:
|
||||
try:
|
||||
yield asyncio.run_coroutine_threadsafe(
|
||||
anext(aiter_), self.loop
|
||||
).result()
|
||||
except StopAsyncIteration:
|
||||
break
|
||||
|
||||
@not_implemented_sync_method
|
||||
def put(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
checkpoint: Checkpoint,
|
||||
metadata: CheckpointMetadata,
|
||||
new_versions: ChannelVersions,
|
||||
) -> RunnableConfig:
|
||||
"""Save a checkpoint to the database. FOO"""
|
||||
"""Save a checkpoint to the database.
|
||||
|
||||
This method saves a checkpoint to the SQLite database. The checkpoint is associated
|
||||
with the provided config and its parent config (if any).
|
||||
|
||||
Args:
|
||||
config (RunnableConfig): The config to associate with the checkpoint.
|
||||
checkpoint (Checkpoint): The checkpoint to save.
|
||||
metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.
|
||||
new_versions (ChannelVersions): New channel versions as of this write.
|
||||
|
||||
Returns:
|
||||
RunnableConfig: Updated configuration after storing the checkpoint.
|
||||
"""
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
self.aput(config, checkpoint, metadata, new_versions), self.loop
|
||||
).result()
|
||||
|
||||
def put_writes(
|
||||
self, config: RunnableConfig, writes: List[Tuple[str, Any]], task_id: str
|
||||
) -> None:
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
self.aput_writes(config, writes, task_id), self.loop
|
||||
).result()
|
||||
|
||||
async def setup(self) -> None:
|
||||
"""Set up the checkpoint database asynchronously.
|
||||
@@ -242,7 +276,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
"""
|
||||
await self.setup()
|
||||
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
||||
async with self.conn.cursor() as cur:
|
||||
async with self.lock, self.conn.cursor() as cur:
|
||||
# find the latest checkpoint for the thread_id
|
||||
if checkpoint_id := get_checkpoint_id(config):
|
||||
await cur.execute(
|
||||
@@ -337,7 +371,9 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
ORDER BY checkpoint_id DESC"""
|
||||
if limit:
|
||||
query += f" LIMIT {limit}"
|
||||
async with self.conn.execute(query, params) as cur, self.conn.cursor() as wcur:
|
||||
async with self.lock, self.conn.execute(
|
||||
query, params
|
||||
) as cur, self.conn.cursor() as wcur:
|
||||
async for (
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
@@ -404,7 +440,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
||||
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
|
||||
serialized_metadata = self.jsonplus_serde.dumps(metadata)
|
||||
async with self.conn.execute(
|
||||
async with self.lock, self.conn.execute(
|
||||
"INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
str(config["configurable"]["thread_id"]),
|
||||
@@ -441,7 +477,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
task_id (str): Identifier for the task creating the writes.
|
||||
"""
|
||||
await self.setup()
|
||||
async with self.conn.cursor() as cur:
|
||||
async with self.lock, 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 (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[
|
||||
|
||||
Generated
+3
-4
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiosqlite"
|
||||
@@ -252,7 +252,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.8"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -651,7 +651,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||
@@ -835,4 +834,4 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.9.0"
|
||||
content-hash = "d50ec7c6b55075d19193e080cc95d3b0998b9efa1a0ed407bd6055b5b5e867e0"
|
||||
content-hash = "752a22dc2b57a0818a3a4d9bf5f62226ba7f8e0c458892551bf8b4c93723dbc1"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "1.0.1"
|
||||
version = "1.0.3"
|
||||
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0"
|
||||
langgraph-checkpoint = "^1.0.1"
|
||||
langgraph-checkpoint = "^1.0.8"
|
||||
aiosqlite = "^0.20.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
||||
@@ -3,7 +3,7 @@ https://github.com/oittaa/uuid6-python/blob/main/src/uuid6/__init__.py#L95
|
||||
Bundled in to avoid install issues with uuid6 package
|
||||
"""
|
||||
|
||||
import secrets
|
||||
import random
|
||||
import time
|
||||
import uuid
|
||||
from typing import Optional, Tuple
|
||||
@@ -96,9 +96,9 @@ def uuid6(node: Optional[int] = None, clock_seq: Optional[int] = None) -> UUID:
|
||||
timestamp = _last_v6_timestamp + 1
|
||||
_last_v6_timestamp = timestamp
|
||||
if clock_seq is None:
|
||||
clock_seq = secrets.randbits(14) # instead of stable storage
|
||||
clock_seq = random.getrandbits(14) # instead of stable storage
|
||||
if node is None:
|
||||
node = secrets.randbits(48)
|
||||
node = random.getrandbits(48)
|
||||
time_high_and_time_mid = (timestamp >> 12) & 0xFFFFFFFFFFFF
|
||||
time_low_and_version = timestamp & 0x0FFF
|
||||
uuid_int = time_high_and_time_mid << 80
|
||||
|
||||
Generated
+6
-5
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
@@ -227,13 +227,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.2.24"
|
||||
version = "0.2.38"
|
||||
description = "Building applications with LLMs through composability"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.8.1"
|
||||
files = [
|
||||
{file = "langchain_core-0.2.24-py3-none-any.whl", hash = "sha256:9444fc082d21ef075d925590a684a73fe1f9688a3d90087580ec929751be55e7"},
|
||||
{file = "langchain_core-0.2.24.tar.gz", hash = "sha256:f2e3fa200b124e8c45d270da9bf836bed9c09532612c96ff3225e59b9a232f5a"},
|
||||
{file = "langchain_core-0.2.38-py3-none-any.whl", hash = "sha256:8a5729bc7e68b4af089af20eff44fe4e7ca21d0e0c87ec21cef7621981fd1a4a"},
|
||||
{file = "langchain_core-0.2.38.tar.gz", hash = "sha256:eb69dbedd344f2ee1f15bcea6c71a05884b867588fadc42d04632e727c1238f3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -246,6 +246,7 @@ pydantic = [
|
||||
]
|
||||
PyYAML = ">=5.3"
|
||||
tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0"
|
||||
typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langsmith"
|
||||
@@ -850,4 +851,4 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
content-hash = "de79db3dc7701542739b3417e9d3f02c3b41167719603ee4d08b92e23b7443ee"
|
||||
content-hash = "d4c13800471766fa9e2d11d2f1092f02fbf28cc507189aeea8a3d5b297286068"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "1.0.8"
|
||||
version = "1.0.9"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0,<4.0"
|
||||
langchain-core = ">=0.2.22,<0.3"
|
||||
langchain-core = ">=0.2.38,<0.4"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.6.2"
|
||||
|
||||
@@ -10,9 +10,9 @@ 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 pydantic.v1 import BaseModel as BaseModelV1
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||
@@ -23,7 +23,7 @@ class MyPydantic(BaseModel):
|
||||
bar: int
|
||||
|
||||
|
||||
class MyFunnyPydantic(LcBaseModel):
|
||||
class MyFunnyPydantic(BaseModelV1):
|
||||
foo: str
|
||||
bar: int
|
||||
|
||||
|
||||
@@ -14,12 +14,12 @@ from langchain_core.messages import (
|
||||
)
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from langchain_core.pydantic_v1 import BaseModel, Field
|
||||
from langchain_core.runnables import RunnableConfig, RunnableLambda
|
||||
from langchain_core.runnables import chain as as_runnable
|
||||
from langchain_core.tools import tool
|
||||
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
|
||||
from langgraph.graph import END, StateGraph
|
||||
from pydantic import BaseModel, Field
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
fast_llm = ChatOpenAI(model="gpt-3.5-turbo")
|
||||
|
||||
@@ -15,7 +15,7 @@ coverage:
|
||||
--cov-report term-missing:skip-covered
|
||||
|
||||
start-postgres:
|
||||
docker compose -f tests/compose-postgres.yml up -V --force-recreate --wait
|
||||
docker compose -f tests/compose-postgres.yml up -V --force-recreate --wait --remove-orphans
|
||||
|
||||
stop-postgres:
|
||||
docker compose -f tests/compose-postgres.yml down -v
|
||||
|
||||
@@ -14,11 +14,13 @@ CONFIG_KEY_CHECKPOINT_MAP = "checkpoint_map"
|
||||
INTERRUPT = "__interrupt__"
|
||||
ERROR = "__error__"
|
||||
TASKS = "__pregel_tasks"
|
||||
SUBSCRIPTIONS = "__pregel_subscriptions"
|
||||
RUNTIME_PLACEHOLDER = "__pregel_runtime_placeholder__"
|
||||
RESERVED = {
|
||||
INTERRUPT,
|
||||
ERROR,
|
||||
TASKS,
|
||||
SUBSCRIPTIONS,
|
||||
CONFIG_KEY_SEND,
|
||||
CONFIG_KEY_READ,
|
||||
CONFIG_KEY_CHECKPOINTER,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from typing import (
|
||||
@@ -38,7 +39,7 @@ from langgraph.pregel import Channel, Pregel
|
||||
from langgraph.pregel.read import PregelNode
|
||||
from langgraph.pregel.types import All
|
||||
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
|
||||
from langgraph.utils import RunnableCallable, coerce_to_runnable
|
||||
from langgraph.utils.runnable import RunnableCallable, coerce_to_runnable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -55,7 +56,7 @@ class Branch(NamedTuple):
|
||||
|
||||
def run(
|
||||
self,
|
||||
writer: Callable[[list[str]], Optional[Runnable]],
|
||||
writer: Callable[[list[str], RunnableConfig], None],
|
||||
reader: Optional[Callable[[RunnableConfig], Any]] = None,
|
||||
) -> None:
|
||||
return ChannelWrite.register_writer(
|
||||
@@ -75,7 +76,7 @@ class Branch(NamedTuple):
|
||||
config: RunnableConfig,
|
||||
*,
|
||||
reader: Optional[Callable[[], Any]],
|
||||
writer: Callable[[list[str]], Optional[Runnable]],
|
||||
writer: Callable[[list[str], RunnableConfig], None],
|
||||
) -> Runnable:
|
||||
if reader:
|
||||
value = reader(config)
|
||||
@@ -86,7 +87,7 @@ class Branch(NamedTuple):
|
||||
else:
|
||||
value = input
|
||||
result = self.path.invoke(value, config)
|
||||
return self._finish(writer, input, result)
|
||||
return self._finish(writer, input, result, config)
|
||||
|
||||
async def _aroute(
|
||||
self,
|
||||
@@ -94,10 +95,10 @@ class Branch(NamedTuple):
|
||||
config: RunnableConfig,
|
||||
*,
|
||||
reader: Optional[Callable[[], Any]],
|
||||
writer: Callable[[list[str]], Optional[Runnable]],
|
||||
writer: Callable[[list[str], RunnableConfig], Optional[Runnable]],
|
||||
) -> Runnable:
|
||||
if reader:
|
||||
value = reader(config)
|
||||
value = await asyncio.to_thread(reader, config)
|
||||
# passthrough additional keys from node to branch
|
||||
# only doable when using dict states
|
||||
if isinstance(value, dict) and isinstance(input, dict):
|
||||
@@ -105,10 +106,14 @@ class Branch(NamedTuple):
|
||||
else:
|
||||
value = input
|
||||
result = await self.path.ainvoke(value, config)
|
||||
return self._finish(writer, input, result)
|
||||
return self._finish(writer, input, result, config)
|
||||
|
||||
def _finish(
|
||||
self, writer: Callable[[list[str]], Optional[Runnable]], input: Any, result: Any
|
||||
self,
|
||||
writer: Callable[[list[str], RunnableConfig], None],
|
||||
input: Any,
|
||||
result: Any,
|
||||
config: RunnableConfig,
|
||||
):
|
||||
if not isinstance(result, list):
|
||||
result = [result]
|
||||
@@ -120,7 +125,7 @@ class Branch(NamedTuple):
|
||||
raise ValueError("Branch did not return a valid destination")
|
||||
if any(p.node == END for p in destinations if isinstance(p, Send)):
|
||||
raise InvalidUpdateError("Cannot send a packet to the END node")
|
||||
return writer(destinations) or input
|
||||
return writer(destinations, config) or input
|
||||
|
||||
|
||||
class Graph:
|
||||
@@ -424,6 +429,10 @@ class Graph:
|
||||
class CompiledGraph(Pregel):
|
||||
builder: Graph
|
||||
|
||||
def __init__(self, *, builder: Graph, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.builder = builder
|
||||
|
||||
def attach_node(self, key: str, node: NodeSpec) -> None:
|
||||
self.channels[key] = EphemeralValue(Any)
|
||||
self.nodes[key] = (
|
||||
@@ -445,7 +454,9 @@ class CompiledGraph(Pregel):
|
||||
self.nodes[end].channels.append(start)
|
||||
|
||||
def attach_branch(self, start: str, name: str, branch: Branch) -> None:
|
||||
def branch_writer(packets: list[Union[str, Send]]) -> Optional[ChannelWrite]:
|
||||
def branch_writer(
|
||||
packets: list[Union[str, Send]], config: RunnableConfig
|
||||
) -> Optional[ChannelWrite]:
|
||||
writes = [
|
||||
(
|
||||
ChannelWriteEntry(f"branch:{start}:{name}:{p}" if p != END else END)
|
||||
@@ -501,7 +512,9 @@ class CompiledGraph(Pregel):
|
||||
for key, n in self.builder.nodes.items():
|
||||
node = n.runnable
|
||||
metadata = n.metadata or {}
|
||||
if key in self.interrupt_before_nodes:
|
||||
if key in self.interrupt_before_nodes and key in self.interrupt_after_nodes:
|
||||
metadata["__interrupt"] = "before,after"
|
||||
elif key in self.interrupt_before_nodes:
|
||||
metadata["__interrupt"] = "before"
|
||||
elif key in self.interrupt_after_nodes:
|
||||
metadata["__interrupt"] = "after"
|
||||
|
||||
@@ -17,12 +17,11 @@ from typing import (
|
||||
overload,
|
||||
)
|
||||
|
||||
from langchain_core.pydantic_v1 import BaseModel
|
||||
from langchain_core.runnables import Runnable, RunnableConfig
|
||||
from langchain_core.runnables.base import RunnableLike
|
||||
from langchain_core.runnables.utils import (
|
||||
create_model,
|
||||
)
|
||||
from langchain_core.runnables.utils import create_model
|
||||
from pydantic import BaseModel
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.channels.binop import BinaryOperatorAggregate
|
||||
@@ -33,14 +32,7 @@ from langgraph.channels.named_barrier_value import NamedBarrierValue
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.constants import NS_END, NS_SEP, TAG_HIDDEN
|
||||
from langgraph.errors import InvalidUpdateError
|
||||
from langgraph.graph.graph import (
|
||||
END,
|
||||
START,
|
||||
Branch,
|
||||
CompiledGraph,
|
||||
Graph,
|
||||
Send,
|
||||
)
|
||||
from langgraph.graph.graph import END, START, Branch, CompiledGraph, Graph, Send
|
||||
from langgraph.managed.base import (
|
||||
ChannelKeyPlaceholder,
|
||||
ChannelTypePlaceholder,
|
||||
@@ -53,7 +45,8 @@ from langgraph.pregel.read import ChannelRead, PregelNode
|
||||
from langgraph.pregel.types import All, RetryPolicy
|
||||
from langgraph.pregel.write import SKIP_WRITE, ChannelWrite, ChannelWriteEntry
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.utils import RunnableCallable, coerce_to_runnable
|
||||
from langgraph.utils.fields import get_field_default
|
||||
from langgraph.utils.runnable import coerce_to_runnable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -481,10 +474,8 @@ class CompiledStateGraph(CompiledGraph):
|
||||
def get_input_schema(
|
||||
self, config: Optional[RunnableConfig] = None
|
||||
) -> type[BaseModel]:
|
||||
from pydantic import BaseModel as BaseModelP
|
||||
|
||||
if isclass(self.builder.input) and issubclass(
|
||||
self.builder.input, (BaseModel, BaseModelP)
|
||||
self.builder.input, (BaseModel, BaseModelV1)
|
||||
):
|
||||
return self.builder.input
|
||||
else:
|
||||
@@ -498,7 +489,16 @@ class CompiledStateGraph(CompiledGraph):
|
||||
return create_model( # type: ignore[call-overload]
|
||||
self.get_name("Input"),
|
||||
**{
|
||||
k: (self.channels[k].UpdateType, None)
|
||||
k: (
|
||||
self.channels[k].UpdateType,
|
||||
(
|
||||
get_field_default(
|
||||
k,
|
||||
self.channels[k].UpdateType,
|
||||
self.builder.input,
|
||||
)
|
||||
),
|
||||
)
|
||||
for k in self.builder.schemas[self.builder.input]
|
||||
if isinstance(self.channels[k], BaseChannel)
|
||||
},
|
||||
@@ -507,10 +507,8 @@ class CompiledStateGraph(CompiledGraph):
|
||||
def get_output_schema(
|
||||
self, config: Optional[RunnableConfig] = None
|
||||
) -> type[BaseModel]:
|
||||
from pydantic import BaseModel as BaseModelP
|
||||
|
||||
if isclass(self.builder.input) and issubclass(
|
||||
self.builder.output, (BaseModel, BaseModelP)
|
||||
if isclass(self.builder.output) and issubclass(
|
||||
self.builder.output, (BaseModel, BaseModelV1)
|
||||
):
|
||||
return self.builder.output
|
||||
|
||||
@@ -530,9 +528,7 @@ class CompiledStateGraph(CompiledGraph):
|
||||
if is_writable_managed_value(v)
|
||||
]
|
||||
|
||||
def _get_state_key(
|
||||
input: Union[None, dict, Any], config: RunnableConfig, *, key: str
|
||||
) -> Any:
|
||||
def _get_state_key(input: Union[None, dict, Any], *, key: str) -> Any:
|
||||
if input is None:
|
||||
return SKIP_WRITE
|
||||
elif isinstance(input, dict):
|
||||
@@ -548,12 +544,7 @@ class CompiledStateGraph(CompiledGraph):
|
||||
[ChannelWriteEntry("__root__", skip_none=True)]
|
||||
if output_keys == ["__root__"]
|
||||
else [
|
||||
ChannelWriteEntry(
|
||||
key,
|
||||
mapper=RunnableCallable(
|
||||
_get_state_key, key=key, trace=False, recurse=False
|
||||
),
|
||||
)
|
||||
ChannelWriteEntry(key, mapper=partial(_get_state_key, key=key))
|
||||
for key in output_keys
|
||||
]
|
||||
)
|
||||
@@ -596,7 +587,8 @@ class CompiledStateGraph(CompiledGraph):
|
||||
],
|
||||
metadata=node.metadata,
|
||||
retry_policy=node.retry_policy,
|
||||
).pipe(node.runnable)
|
||||
bound=node.runnable,
|
||||
)
|
||||
|
||||
def attach_edge(self, starts: Union[str, Sequence[str]], end: str) -> None:
|
||||
if isinstance(starts, str):
|
||||
@@ -626,7 +618,9 @@ class CompiledStateGraph(CompiledGraph):
|
||||
)
|
||||
|
||||
def attach_branch(self, start: str, name: str, branch: Branch) -> None:
|
||||
def branch_writer(packets: list[Union[str, Send]]) -> Optional[ChannelWrite]:
|
||||
def branch_writer(
|
||||
packets: list[Union[str, Send]], config: RunnableConfig
|
||||
) -> Optional[ChannelWrite]:
|
||||
if filtered := [p for p in packets if p != END]:
|
||||
writes = [
|
||||
(
|
||||
@@ -645,7 +639,7 @@ class CompiledStateGraph(CompiledGraph):
|
||||
),
|
||||
)
|
||||
)
|
||||
return ChannelWrite(writes, tags=[TAG_HIDDEN])
|
||||
ChannelWrite.do_write(config, writes)
|
||||
|
||||
# attach branch publisher
|
||||
schema = (
|
||||
|
||||
@@ -6,7 +6,7 @@ from langchain_core.tools import BaseTool
|
||||
from langchain_core.tools import tool as create_tool
|
||||
|
||||
from langgraph._api.deprecation import deprecated
|
||||
from langgraph.utils import RunnableCallable
|
||||
from langgraph.utils.runnable import RunnableCallable
|
||||
|
||||
INVALID_TOOL_MSG_TEMPLATE = (
|
||||
"{requested_tool_name} is not a valid tool, "
|
||||
|
||||
@@ -21,7 +21,7 @@ from langchain_core.tools import BaseTool, InjectedToolArg
|
||||
from langchain_core.tools import tool as create_tool
|
||||
from typing_extensions import get_args
|
||||
|
||||
from langgraph.utils import RunnableCallable
|
||||
from langgraph.utils.runnable import RunnableCallable
|
||||
|
||||
INVALID_TOOL_NAME_ERROR_TEMPLATE = (
|
||||
"Error: {requested_tool} is not a valid tool, try one of [{available_tools}]."
|
||||
|
||||
@@ -24,20 +24,22 @@ from langchain_core.messages import (
|
||||
ToolCall,
|
||||
ToolMessage,
|
||||
)
|
||||
from langchain_core.pydantic_v1 import BaseModel, ValidationError
|
||||
from langchain_core.runnables import (
|
||||
RunnableConfig,
|
||||
)
|
||||
from langchain_core.runnables.config import get_executor_for_config
|
||||
from langchain_core.tools import BaseTool, create_schema_from_function
|
||||
from pydantic import BaseModel as BaseModelV2
|
||||
from pydantic import ValidationError as ValidationErrorV2
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
from pydantic.v1 import ValidationError as ValidationErrorV1
|
||||
|
||||
from langgraph.utils import RunnableCallable
|
||||
from langgraph.utils.runnable import RunnableCallable
|
||||
|
||||
|
||||
def _default_format_error(
|
||||
error: BaseException, call: ToolCall, schema: Type[BaseModel]
|
||||
error: BaseException,
|
||||
call: ToolCall,
|
||||
schema: Union[Type[BaseModel], Type[BaseModelV1]],
|
||||
) -> str:
|
||||
"""Default error formatting function."""
|
||||
return f"{repr(error)}\n\nRespond after fixing all validation errors."
|
||||
@@ -75,7 +77,7 @@ class ValidationNode(RunnableCallable):
|
||||
>>> from typing import Literal, Annotated, TypedDict
|
||||
...
|
||||
>>> from langchain_anthropic import ChatAnthropic
|
||||
>>> from langchain_core.pydantic_v1 import BaseModel, validator
|
||||
>>> from pydantic import BaseModel, validator
|
||||
...
|
||||
>>> from langgraph.graph import END, START, StateGraph
|
||||
>>> from langgraph.prebuilt import ValidationNode
|
||||
@@ -176,7 +178,7 @@ class ValidationNode(RunnableCallable):
|
||||
)
|
||||
self.schemas_by_name[schema.name] = schema.args_schema
|
||||
elif isinstance(schema, type) and issubclass(
|
||||
schema, (BaseModel, BaseModelV2)
|
||||
schema, (BaseModel, BaseModelV1)
|
||||
):
|
||||
self.schemas_by_name[schema.__name__] = cast(Type[BaseModel], schema)
|
||||
elif callable(schema):
|
||||
@@ -212,13 +214,22 @@ class ValidationNode(RunnableCallable):
|
||||
def run_one(call: ToolCall):
|
||||
schema = self.schemas_by_name[call["name"]]
|
||||
try:
|
||||
output = schema.validate(call["args"])
|
||||
if issubclass(schema, BaseModel):
|
||||
output = schema.model_validate(call["args"])
|
||||
content = output.model_dump_json()
|
||||
elif issubclass(schema, BaseModelV1):
|
||||
output = schema.validate(call["args"])
|
||||
content = output.json()
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported schema type: {type(schema)}. Expected BaseModel or BaseModelV1."
|
||||
)
|
||||
return ToolMessage(
|
||||
content=output.json(),
|
||||
content=content,
|
||||
name=call["name"],
|
||||
tool_call_id=cast(str, call["id"]),
|
||||
)
|
||||
except (ValidationError, ValidationErrorV2) as e:
|
||||
except (ValidationError, ValidationErrorV1) as e:
|
||||
return ToolMessage(
|
||||
content=self._format_error(e, call, schema),
|
||||
name=call["name"],
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import time
|
||||
from collections import deque
|
||||
from functools import partial
|
||||
from typing import (
|
||||
Any,
|
||||
AsyncIterator,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterator,
|
||||
@@ -25,21 +22,17 @@ from uuid import UUID, uuid5
|
||||
|
||||
from langchain_core.globals import get_debug
|
||||
from langchain_core.load.dump import dumpd
|
||||
from langchain_core.pydantic_v1 import BaseModel, Field, root_validator
|
||||
from langchain_core.runnables import (
|
||||
Runnable,
|
||||
RunnableLambda,
|
||||
RunnableSequence,
|
||||
RunnableSerializable,
|
||||
)
|
||||
from langchain_core.runnables.base import Input, Output, coerce_to_runnable
|
||||
from langchain_core.runnables.base import Input, Output
|
||||
from langchain_core.runnables.config import (
|
||||
RunnableConfig,
|
||||
ensure_config,
|
||||
get_async_callback_manager_for_config,
|
||||
get_callback_manager_for_config,
|
||||
merge_configs,
|
||||
patch_config,
|
||||
)
|
||||
from langchain_core.runnables.utils import (
|
||||
ConfigurableFieldSpec,
|
||||
@@ -48,6 +41,7 @@ from langchain_core.runnables.utils import (
|
||||
get_unique_config_specs,
|
||||
)
|
||||
from langchain_core.tracers._streaming import _StreamingCallbackHandler
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.channels.base import (
|
||||
@@ -67,12 +61,11 @@ from langgraph.constants import (
|
||||
CONFIG_KEY_SEND,
|
||||
CONFIG_KEY_STREAM,
|
||||
CONFIG_KEY_TASK_ID,
|
||||
ERROR,
|
||||
INTERRUPT,
|
||||
NS_END,
|
||||
NS_SEP,
|
||||
)
|
||||
from langgraph.errors import GraphInterrupt, GraphRecursionError, InvalidUpdateError
|
||||
from langgraph.errors import GraphRecursionError, InvalidUpdateError
|
||||
from langgraph.managed.base import ManagedValueSpec
|
||||
from langgraph.pregel.algo import (
|
||||
apply_writes,
|
||||
@@ -80,18 +73,13 @@ from langgraph.pregel.algo import (
|
||||
local_write,
|
||||
prepare_next_tasks,
|
||||
)
|
||||
from langgraph.pregel.config import patch_checkpoint_map, patch_configurable
|
||||
from langgraph.pregel.debug import (
|
||||
print_step_checkpoint,
|
||||
print_step_tasks,
|
||||
print_step_writes,
|
||||
tasks_w_writes,
|
||||
)
|
||||
from langgraph.pregel.debug import tasks_w_writes
|
||||
from langgraph.pregel.io import read_channels
|
||||
from langgraph.pregel.loop import AsyncPregelLoop, SyncPregelLoop
|
||||
from langgraph.pregel.manager import AsyncChannelsManager, ChannelsManager
|
||||
from langgraph.pregel.read import PregelNode
|
||||
from langgraph.pregel.retry import RetryPolicy, arun_with_retry, run_with_retry
|
||||
from langgraph.pregel.retry import RetryPolicy
|
||||
from langgraph.pregel.runner import PregelRunner
|
||||
from langgraph.pregel.types import (
|
||||
All,
|
||||
PregelExecutableTask,
|
||||
@@ -104,14 +92,15 @@ from langgraph.pregel.utils import (
|
||||
from langgraph.pregel.validate import validate_graph, validate_keys
|
||||
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.utils import RunnableCallable
|
||||
from langgraph.utils.config import (
|
||||
merge_configs,
|
||||
patch_checkpoint_map,
|
||||
patch_config,
|
||||
patch_configurable,
|
||||
)
|
||||
from langgraph.utils.runnable import RunnableCallable
|
||||
|
||||
WriteValue = Union[
|
||||
Runnable[Input, Output],
|
||||
Callable[[Input], Output],
|
||||
Callable[[Input], Awaitable[Output]],
|
||||
Any,
|
||||
]
|
||||
WriteValue = Union[Callable[[Input], Output], Any]
|
||||
|
||||
|
||||
class Channel:
|
||||
@@ -176,26 +165,18 @@ class Channel:
|
||||
return ChannelWrite(
|
||||
[ChannelWriteEntry(c) for c in channels]
|
||||
+ [
|
||||
(
|
||||
ChannelWriteEntry(k, skip_none=True, mapper=coerce_to_runnable(v))
|
||||
if isinstance(v, Runnable) or callable(v)
|
||||
else ChannelWriteEntry(k, value=v)
|
||||
)
|
||||
ChannelWriteEntry(k, mapper=v)
|
||||
if callable(v)
|
||||
else ChannelWriteEntry(k, value=v)
|
||||
for k, v in kwargs.items()
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class Pregel(
|
||||
RunnableSerializable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]
|
||||
):
|
||||
class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]):
|
||||
nodes: Mapping[str, PregelNode]
|
||||
|
||||
channels: Mapping[str, Union[BaseChannel, ManagedValueSpec]] = Field(
|
||||
default_factory=dict
|
||||
)
|
||||
|
||||
auto_validate: bool = True
|
||||
channels: Mapping[str, Union[BaseChannel, ManagedValueSpec]]
|
||||
|
||||
stream_mode: StreamMode = "values"
|
||||
"""Mode to stream output, defaults to 'values'."""
|
||||
@@ -205,16 +186,16 @@ class Pregel(
|
||||
stream_channels: Optional[Union[str, Sequence[str]]] = None
|
||||
"""Channels to stream, defaults to all channels not in reserved channels"""
|
||||
|
||||
interrupt_after_nodes: Union[All, Sequence[str]] = Field(default_factory=list)
|
||||
interrupt_after_nodes: Union[All, Sequence[str]]
|
||||
|
||||
interrupt_before_nodes: Union[All, Sequence[str]] = Field(default_factory=list)
|
||||
interrupt_before_nodes: Union[All, Sequence[str]]
|
||||
|
||||
input_channels: Union[str, Sequence[str]]
|
||||
|
||||
step_timeout: Optional[float] = None
|
||||
"""Maximum time to wait for a step to complete, in seconds. Defaults to None."""
|
||||
|
||||
debug: bool = Field(default_factory=get_debug)
|
||||
debug: bool
|
||||
"""Whether to print debug information during execution. Defaults to False."""
|
||||
|
||||
checkpointer: Optional[BaseCheckpointSaver] = None
|
||||
@@ -232,36 +213,50 @@ class Pregel(
|
||||
|
||||
name: str = "LangGraph"
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
nodes: Mapping[str, PregelNode],
|
||||
channels: Mapping[str, Union[BaseChannel, ManagedValueSpec]] = None,
|
||||
auto_validate: bool = True,
|
||||
stream_mode: StreamMode = "values",
|
||||
output_channels: Union[str, Sequence[str]],
|
||||
stream_channels: Optional[Union[str, Sequence[str]]] = None,
|
||||
interrupt_after_nodes: Union[All, Sequence[str]] = (),
|
||||
interrupt_before_nodes: Union[All, Sequence[str]] = (),
|
||||
input_channels: Union[str, Sequence[str]],
|
||||
step_timeout: Optional[float] = None,
|
||||
debug: Optional[bool] = None,
|
||||
checkpointer: Optional[BaseCheckpointSaver] = None,
|
||||
store: Optional[BaseStore] = None,
|
||||
retry_policy: Optional[RetryPolicy] = None,
|
||||
config_type: Optional[Type[Any]] = None,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
name: str = "LangGraph",
|
||||
) -> None:
|
||||
self.nodes = nodes
|
||||
self.channels = channels or {}
|
||||
self.stream_mode = stream_mode
|
||||
self.output_channels = output_channels
|
||||
self.stream_channels = stream_channels
|
||||
self.interrupt_after_nodes = interrupt_after_nodes
|
||||
self.interrupt_before_nodes = interrupt_before_nodes
|
||||
self.input_channels = input_channels
|
||||
self.step_timeout = step_timeout
|
||||
self.debug = debug if debug is not None else get_debug()
|
||||
self.checkpointer = checkpointer
|
||||
self.store = store
|
||||
self.retry_policy = retry_policy
|
||||
self.config_type = config_type
|
||||
self.config = config
|
||||
self.name = name
|
||||
if auto_validate:
|
||||
self.validate()
|
||||
|
||||
def with_config(self, config: RunnableConfig | None = None, **kwargs: Any) -> Self:
|
||||
return self.copy(
|
||||
update={"config": cast(RunnableConfig, {**(config or {}), **kwargs})}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""Return whether the graph can be serialized by Langchain."""
|
||||
return True
|
||||
|
||||
@root_validator(skip_on_failure=True)
|
||||
def validate_on_init(cls, values: dict[str, Any]) -> dict[str, Any]:
|
||||
if not values["auto_validate"]:
|
||||
return values
|
||||
validate_graph(
|
||||
values["nodes"],
|
||||
values["channels"],
|
||||
values["input_channels"],
|
||||
values["output_channels"],
|
||||
values["stream_channels"],
|
||||
values["interrupt_after_nodes"],
|
||||
values["interrupt_before_nodes"],
|
||||
)
|
||||
if values["interrupt_after_nodes"] or values["interrupt_before_nodes"]:
|
||||
if not values["checkpointer"]:
|
||||
raise ValueError("Interrupts require a checkpointer")
|
||||
return values
|
||||
attrs = {**self.__dict__}
|
||||
attrs["config"] = merge_configs(self.config, config, kwargs)
|
||||
return self.__class__(**attrs)
|
||||
|
||||
def validate(self) -> Self:
|
||||
validate_graph(
|
||||
@@ -809,7 +804,7 @@ class Pregel(
|
||||
managed,
|
||||
):
|
||||
# create task to run all writers of the chosen node
|
||||
writers = self.nodes[as_node].get_writers()
|
||||
writers = self.nodes[as_node].flat_writers
|
||||
if not writers:
|
||||
raise InvalidUpdateError(f"Node {as_node} has no writers")
|
||||
task = PregelExecutableTask(
|
||||
@@ -973,7 +968,7 @@ class Pregel(
|
||||
managed,
|
||||
):
|
||||
# create task to run all writers of the chosen node
|
||||
writers = self.nodes[as_node].get_writers()
|
||||
writers = self.nodes[as_node].flat_writers
|
||||
if not writers:
|
||||
raise InvalidUpdateError(f"Node {as_node} has no writers")
|
||||
task = PregelExecutableTask(
|
||||
@@ -1163,9 +1158,11 @@ class Pregel(
|
||||
```
|
||||
"""
|
||||
|
||||
stream = deque()
|
||||
|
||||
def output() -> Iterator:
|
||||
while loop.stream:
|
||||
ns, mode, payload = loop.stream.popleft()
|
||||
while stream:
|
||||
ns, mode, payload = stream.popleft()
|
||||
if mode in stream_modes:
|
||||
if subgraphs and isinstance(stream_mode, list):
|
||||
yield (tuple(ns.split(NS_SEP)) if ns else (), mode, payload)
|
||||
@@ -1210,6 +1207,7 @@ class Pregel(
|
||||
|
||||
with SyncPregelLoop(
|
||||
input,
|
||||
stream=stream.append,
|
||||
config=config,
|
||||
store=self.store,
|
||||
checkpointer=checkpointer,
|
||||
@@ -1217,7 +1215,14 @@ class Pregel(
|
||||
specs=self.channels,
|
||||
output_keys=output_keys,
|
||||
stream_keys=self.stream_channels_asis,
|
||||
debug=debug,
|
||||
) as loop:
|
||||
# create runner
|
||||
runner = PregelRunner(
|
||||
submit=loop.submit,
|
||||
put_writes=loop.put_writes,
|
||||
)
|
||||
# enable subgraph streaming
|
||||
if subgraphs:
|
||||
loop.config["configurable"][CONFIG_KEY_STREAM] = loop.stream
|
||||
# Similarly to Bulk Synchronous Parallel / Pregel model
|
||||
@@ -1231,85 +1236,14 @@ class Pregel(
|
||||
interrupt_after=interrupt_after,
|
||||
manager=run_manager,
|
||||
):
|
||||
# debug flag
|
||||
if debug:
|
||||
print_step_checkpoint(
|
||||
loop.checkpoint_metadata,
|
||||
loop.channels,
|
||||
self.stream_channels_list,
|
||||
)
|
||||
# emit output
|
||||
yield from output()
|
||||
# debug flag
|
||||
if debug:
|
||||
print_step_tasks(loop.step, loop.tasks)
|
||||
|
||||
# execute tasks, and wait for one to fail or all to finish.
|
||||
# each task is independent from all other concurrent tasks
|
||||
# yield updates/debug output as each task finishes
|
||||
futures = {
|
||||
loop.submit(
|
||||
run_with_retry,
|
||||
task,
|
||||
self.retry_policy,
|
||||
): task
|
||||
for task in loop.tasks
|
||||
if not task.writes
|
||||
}
|
||||
all_futures = futures.copy()
|
||||
end_time = (
|
||||
self.step_timeout + time.monotonic()
|
||||
if self.step_timeout
|
||||
else None
|
||||
)
|
||||
if not futures:
|
||||
done, inflight = set(), set()
|
||||
while futures:
|
||||
done, inflight = concurrent.futures.wait(
|
||||
futures,
|
||||
return_when=concurrent.futures.FIRST_COMPLETED,
|
||||
timeout=(
|
||||
max(0, end_time - time.monotonic())
|
||||
if end_time
|
||||
else None
|
||||
),
|
||||
)
|
||||
if not done:
|
||||
break # timed out
|
||||
for fut in done:
|
||||
task = futures.pop(fut)
|
||||
if exc := _exception(fut):
|
||||
# save error to checkpointer
|
||||
if isinstance(exc, GraphInterrupt):
|
||||
loop.put_writes(
|
||||
task.id, [(INTERRUPT, i) for i in exc.args[0]]
|
||||
)
|
||||
else:
|
||||
loop.put_writes(task.id, [(ERROR, exc)])
|
||||
|
||||
else:
|
||||
# save task writes to checkpointer
|
||||
loop.put_writes(task.id, task.writes)
|
||||
else:
|
||||
# remove references to loop vars
|
||||
del fut, task
|
||||
for _ in runner.tick(
|
||||
loop.tasks,
|
||||
timeout=self.step_timeout,
|
||||
retry_policy=self.retry_policy,
|
||||
):
|
||||
# emit output
|
||||
yield from output()
|
||||
# maybe stop other tasks
|
||||
if _should_stop_others(done):
|
||||
break
|
||||
|
||||
# panic on failure or timeout
|
||||
_panic_or_proceed(all_futures, loop.step)
|
||||
# don't keep futures around in memory longer than needed
|
||||
del done, inflight, futures
|
||||
# debug flag
|
||||
if debug:
|
||||
print_step_writes(
|
||||
loop.step,
|
||||
[w for t in loop.tasks for w in t.writes],
|
||||
self.stream_channels_list,
|
||||
)
|
||||
for o in output():
|
||||
yield o
|
||||
# emit output
|
||||
yield from output()
|
||||
# handle exit
|
||||
@@ -1405,9 +1339,11 @@ class Pregel(
|
||||
```
|
||||
"""
|
||||
|
||||
stream = deque()
|
||||
|
||||
def output() -> Iterator:
|
||||
while loop.stream:
|
||||
ns, mode, payload = loop.stream.popleft()
|
||||
while stream:
|
||||
ns, mode, payload = stream.popleft()
|
||||
if mode in stream_modes:
|
||||
if subgraphs and isinstance(stream_mode, list):
|
||||
yield (tuple(ns.split(NS_SEP)) if ns else (), mode, payload)
|
||||
@@ -1460,6 +1396,7 @@ class Pregel(
|
||||
)
|
||||
async with AsyncPregelLoop(
|
||||
input,
|
||||
stream=stream.append,
|
||||
config=config,
|
||||
store=self.store,
|
||||
checkpointer=checkpointer,
|
||||
@@ -1468,103 +1405,35 @@ class Pregel(
|
||||
output_keys=output_keys,
|
||||
stream_keys=self.stream_channels_asis,
|
||||
) as loop:
|
||||
# create runner
|
||||
runner = PregelRunner(
|
||||
submit=loop.submit,
|
||||
put_writes=loop.put_writes,
|
||||
use_astream=do_stream is not None,
|
||||
)
|
||||
# enable subgraph streaming
|
||||
if subgraphs:
|
||||
loop.config["configurable"][CONFIG_KEY_STREAM] = loop.stream
|
||||
aioloop = asyncio.get_event_loop()
|
||||
# Similarly to Bulk Synchronous Parallel / Pregel model
|
||||
# computation proceeds in steps, while there are channel updates
|
||||
# channel updates from step N are only visible in step N+1
|
||||
# channels are guaranteed to be immutable for the duration of the step,
|
||||
# with channel updates applied only at the transition between steps
|
||||
while loop.tick(
|
||||
while await asyncio.to_thread(
|
||||
loop.tick,
|
||||
input_keys=self.input_channels,
|
||||
interrupt_before=interrupt_before,
|
||||
interrupt_after=interrupt_after,
|
||||
manager=run_manager,
|
||||
):
|
||||
# debug flag
|
||||
if debug:
|
||||
print_step_checkpoint(
|
||||
loop.checkpoint_metadata,
|
||||
loop.channels,
|
||||
self.stream_channels_list,
|
||||
)
|
||||
# emit output
|
||||
for o in output():
|
||||
yield o
|
||||
# debug flag
|
||||
if debug:
|
||||
print_step_tasks(loop.step, loop.tasks)
|
||||
|
||||
# execute tasks, and wait for one to fail or all to finish.
|
||||
# each task is independent from all other concurrent tasks
|
||||
# yield updates/debug output as each task finishes
|
||||
futures = {
|
||||
loop.submit(
|
||||
arun_with_retry,
|
||||
task,
|
||||
self.retry_policy,
|
||||
stream=do_stream,
|
||||
__name__=task.name,
|
||||
__cancel_on_exit__=True,
|
||||
): task
|
||||
for task in loop.tasks
|
||||
if not task.writes
|
||||
}
|
||||
all_futures = futures.copy()
|
||||
end_time = (
|
||||
self.step_timeout + aioloop.time()
|
||||
if self.step_timeout
|
||||
else None
|
||||
)
|
||||
if not futures:
|
||||
done, inflight = set(), set()
|
||||
while futures:
|
||||
done, inflight = await asyncio.wait(
|
||||
futures,
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
timeout=(
|
||||
max(0, end_time - aioloop.time()) if end_time else None
|
||||
),
|
||||
)
|
||||
if not done:
|
||||
break # timed out
|
||||
|
||||
for fut in done:
|
||||
task = futures.pop(fut)
|
||||
if exc := _exception(fut):
|
||||
# save error to checkpointer
|
||||
if isinstance(exc, GraphInterrupt):
|
||||
loop.put_writes(
|
||||
task.id, [(INTERRUPT, i) for i in exc.args[0]]
|
||||
)
|
||||
else:
|
||||
loop.put_writes(task.id, [(ERROR, exc)])
|
||||
|
||||
else:
|
||||
# save task writes to checkpointer
|
||||
loop.put_writes(task.id, task.writes)
|
||||
else:
|
||||
# remove references to loop vars
|
||||
del fut, task
|
||||
async for _ in runner.atick(
|
||||
loop.tasks,
|
||||
timeout=self.step_timeout,
|
||||
retry_policy=self.retry_policy,
|
||||
):
|
||||
# emit output
|
||||
for o in output():
|
||||
yield o
|
||||
# maybe stop other tasks
|
||||
if _should_stop_others(done):
|
||||
break
|
||||
|
||||
# panic on failure or timeout
|
||||
_panic_or_proceed(all_futures, loop.step, asyncio.TimeoutError)
|
||||
# don't keep futures around in memory longer than needed
|
||||
del done, inflight, futures
|
||||
# debug flag
|
||||
if debug:
|
||||
print_step_writes(
|
||||
loop.step,
|
||||
[w for t in loop.tasks for w in t.writes],
|
||||
self.stream_channels_list,
|
||||
)
|
||||
# emit output
|
||||
for o in output():
|
||||
yield o
|
||||
@@ -1685,57 +1554,3 @@ class Pregel(
|
||||
return latest
|
||||
else:
|
||||
return chunks
|
||||
|
||||
|
||||
def _should_stop_others(
|
||||
done: Union[set[concurrent.futures.Future[Any]], set[asyncio.Task[Any]]],
|
||||
) -> bool:
|
||||
for fut in done:
|
||||
if fut.cancelled():
|
||||
return True
|
||||
if exc := fut.exception():
|
||||
return not isinstance(exc, GraphInterrupt)
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def _exception(
|
||||
fut: Union[concurrent.futures.Future[Any], asyncio.Task[Any]],
|
||||
) -> Optional[BaseException]:
|
||||
if fut.cancelled():
|
||||
if isinstance(fut, asyncio.Task):
|
||||
return asyncio.CancelledError()
|
||||
else:
|
||||
return concurrent.futures.CancelledError()
|
||||
else:
|
||||
return fut.exception()
|
||||
|
||||
|
||||
def _panic_or_proceed(
|
||||
futs: Union[set[concurrent.futures.Future[Any]], set[asyncio.Task[Any]]],
|
||||
step: int,
|
||||
timeout_exc_cls: Type[Exception] = TimeoutError,
|
||||
) -> None:
|
||||
done: set[Union[concurrent.futures.Future[Any], asyncio.Task[Any]]] = set()
|
||||
inflight: set[Union[concurrent.futures.Future[Any], asyncio.Task[Any]]] = set()
|
||||
for fut in futs:
|
||||
if fut.done():
|
||||
done.add(fut)
|
||||
else:
|
||||
inflight.add(fut)
|
||||
while done:
|
||||
# if any task failed
|
||||
if exc := _exception(done.pop()):
|
||||
# cancel all pending tasks
|
||||
while inflight:
|
||||
inflight.pop().cancel()
|
||||
# raise the exception
|
||||
raise exc
|
||||
|
||||
if inflight:
|
||||
# if we got here means we timed out
|
||||
while inflight:
|
||||
# cancel all pending tasks
|
||||
inflight.pop().cancel()
|
||||
# raise timeout error
|
||||
raise timeout_exc_cls(f"Timed out at step {step}")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
from collections import defaultdict, deque
|
||||
from functools import partial
|
||||
from hashlib import sha1
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
@@ -14,22 +14,13 @@ from typing import (
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
from uuid import UUID, uuid5
|
||||
from uuid import UUID
|
||||
|
||||
from langchain_core.callbacks.manager import AsyncParentRunManager, ParentRunManager
|
||||
from langchain_core.runnables.config import (
|
||||
RunnableConfig,
|
||||
merge_configs,
|
||||
patch_config,
|
||||
)
|
||||
from langchain_core.runnables.config import RunnableConfig
|
||||
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.checkpoint.base import (
|
||||
BaseCheckpointSaver,
|
||||
Checkpoint,
|
||||
copy_checkpoint,
|
||||
create_checkpoint,
|
||||
)
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver, Checkpoint, copy_checkpoint
|
||||
from langgraph.constants import (
|
||||
CONFIG_KEY_CHECKPOINT_MAP,
|
||||
CONFIG_KEY_CHECKPOINTER,
|
||||
@@ -40,6 +31,7 @@ from langgraph.constants import (
|
||||
INTERRUPT,
|
||||
NS_SEP,
|
||||
RESERVED,
|
||||
SUBSCRIPTIONS,
|
||||
TAG_HIDDEN,
|
||||
TASKS,
|
||||
Send,
|
||||
@@ -51,6 +43,7 @@ from langgraph.pregel.log import logger
|
||||
from langgraph.pregel.manager import ChannelsManager
|
||||
from langgraph.pregel.read import PregelNode
|
||||
from langgraph.pregel.types import All, PregelExecutableTask, PregelTask
|
||||
from langgraph.utils.config import merge_configs, patch_config
|
||||
|
||||
|
||||
class WritesProtocol(Protocol):
|
||||
@@ -106,17 +99,25 @@ def local_read(
|
||||
) -> Union[dict[str, Any], Any]:
|
||||
if isinstance(select, str):
|
||||
managed_keys = []
|
||||
for c, _ in task.writes:
|
||||
if c == select:
|
||||
updated = {c}
|
||||
break
|
||||
else:
|
||||
updated = set()
|
||||
else:
|
||||
managed_keys = [k for k in select if k in managed]
|
||||
select = [k for k in select if k not in managed]
|
||||
if fresh:
|
||||
new_checkpoint = create_checkpoint(copy_checkpoint(checkpoint), channels, -1)
|
||||
with ChannelsManager(channels, new_checkpoint, config, skip_context=True) as (
|
||||
channels,
|
||||
_,
|
||||
):
|
||||
apply_writes(new_checkpoint, channels, [task], None)
|
||||
values = read_channels(channels, select)
|
||||
updated = set(select).intersection(c for c, _ in task.writes)
|
||||
if fresh and updated:
|
||||
with ChannelsManager(
|
||||
{k: v for k, v in channels.items() if k in updated},
|
||||
checkpoint,
|
||||
config,
|
||||
skip_context=True,
|
||||
) as (local_channels, _):
|
||||
apply_writes(copy_checkpoint(checkpoint), local_channels, [task], None)
|
||||
values = read_channels({**channels, **local_channels}, select)
|
||||
else:
|
||||
values = read_channels(channels, select)
|
||||
if managed_keys:
|
||||
@@ -175,7 +176,10 @@ def apply_writes(
|
||||
|
||||
# Consume all channels that were read
|
||||
for chan in {
|
||||
chan for task in tasks for chan in task.triggers if chan not in RESERVED
|
||||
chan
|
||||
for task in tasks
|
||||
for chan in task.triggers
|
||||
if chan not in RESERVED and chan in channels
|
||||
}:
|
||||
if channels[chan].consume():
|
||||
if get_next_version is not None:
|
||||
@@ -273,50 +277,220 @@ def prepare_next_tasks(
|
||||
checkpointer: Optional[BaseCheckpointSaver] = None,
|
||||
manager: Union[None, ParentRunManager, AsyncParentRunManager] = None,
|
||||
) -> Union[list[PregelTask], list[PregelExecutableTask]]:
|
||||
configurable = config.get("configurable", {})
|
||||
parent_ns = configurable.get("checkpoint_ns", "")
|
||||
tasks: Union[list[PregelTask], list[PregelExecutableTask]] = []
|
||||
# Consume pending packets
|
||||
for packet in checkpoint["pending_sends"]:
|
||||
for idx, _ in enumerate(checkpoint["pending_sends"]):
|
||||
if task := prepare_single_task(
|
||||
(TASKS, idx),
|
||||
None,
|
||||
checkpoint=checkpoint,
|
||||
processes=processes,
|
||||
channels=channels,
|
||||
managed=managed,
|
||||
config=config,
|
||||
step=step,
|
||||
for_execution=for_execution,
|
||||
is_resuming=is_resuming,
|
||||
checkpointer=checkpointer,
|
||||
manager=manager,
|
||||
):
|
||||
tasks.append(task)
|
||||
# Check if any processes should be run in next step
|
||||
# If so, prepare the values to be passed to them
|
||||
for name in processes:
|
||||
if task := prepare_single_task(
|
||||
(SUBSCRIPTIONS, name),
|
||||
None,
|
||||
checkpoint=checkpoint,
|
||||
processes=processes,
|
||||
channels=channels,
|
||||
managed=managed,
|
||||
config=config,
|
||||
step=step,
|
||||
for_execution=for_execution,
|
||||
is_resuming=is_resuming,
|
||||
checkpointer=checkpointer,
|
||||
manager=manager,
|
||||
):
|
||||
tasks.append(task)
|
||||
return tasks
|
||||
|
||||
|
||||
def prepare_single_task(
|
||||
task_path: tuple[str, Union[int, str]],
|
||||
task_id_checksum: Optional[str],
|
||||
*,
|
||||
checkpoint: Checkpoint,
|
||||
processes: Mapping[str, PregelNode],
|
||||
channels: Mapping[str, BaseChannel],
|
||||
managed: ManagedValueMapping,
|
||||
config: RunnableConfig,
|
||||
step: int,
|
||||
for_execution: bool,
|
||||
is_resuming: bool = False,
|
||||
checkpointer: Optional[BaseCheckpointSaver] = None,
|
||||
manager: Union[None, ParentRunManager, AsyncParentRunManager] = None,
|
||||
) -> Union[None, PregelTask, PregelExecutableTask]:
|
||||
checkpoint_id = UUID(checkpoint["id"]).bytes
|
||||
configurable = config.get("configurable", {})
|
||||
parent_ns = configurable.get("checkpoint_ns", "")
|
||||
|
||||
if task_path[0] == TASKS:
|
||||
idx = int(task_path[1])
|
||||
packet = checkpoint["pending_sends"][idx]
|
||||
if not isinstance(packet, Send):
|
||||
logger.warn(f"Ignoring invalid packet type {type(packet)} in pending sends")
|
||||
continue
|
||||
logger.warning(
|
||||
f"Ignoring invalid packet type {type(packet)} in pending sends"
|
||||
)
|
||||
return
|
||||
if packet.node not in processes:
|
||||
logger.warn(f"Ignoring unknown node name {packet.node} in pending sends")
|
||||
continue
|
||||
logger.warning(f"Ignoring unknown node name {packet.node} in pending sends")
|
||||
return
|
||||
# create task id
|
||||
triggers = [TASKS]
|
||||
metadata = {
|
||||
"langgraph_step": step,
|
||||
"langgraph_node": packet.node,
|
||||
"langgraph_triggers": triggers,
|
||||
"langgraph_task_idx": len(tasks),
|
||||
"langgraph_path": task_path,
|
||||
}
|
||||
checkpoint_ns = (
|
||||
f"{parent_ns}{NS_SEP}{packet.node}" if parent_ns else packet.node
|
||||
)
|
||||
task_id = str(
|
||||
uuid5(UUID(checkpoint["id"]), json.dumps((checkpoint_ns, metadata)))
|
||||
task_id = _uuid5_str(
|
||||
checkpoint_id,
|
||||
checkpoint_ns,
|
||||
str(step),
|
||||
packet.node,
|
||||
TASKS,
|
||||
str(idx),
|
||||
)
|
||||
if task_id_checksum is not None:
|
||||
assert task_id == task_id_checksum
|
||||
if for_execution:
|
||||
proc = processes[packet.node]
|
||||
if node := proc.get_node():
|
||||
if node := proc.node:
|
||||
managed.replace_runtime_placeholders(step, packet.arg)
|
||||
writes = deque()
|
||||
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
|
||||
tasks.append(
|
||||
PregelExecutableTask(
|
||||
packet.node,
|
||||
packet.arg,
|
||||
return PregelExecutableTask(
|
||||
packet.node,
|
||||
packet.arg,
|
||||
node,
|
||||
writes,
|
||||
patch_config(
|
||||
merge_configs(
|
||||
config,
|
||||
processes[packet.node].config,
|
||||
{"metadata": metadata},
|
||||
),
|
||||
run_name=packet.node,
|
||||
callbacks=(
|
||||
manager.get_child(f"graph:step:{step}") if manager else None
|
||||
),
|
||||
configurable={
|
||||
CONFIG_KEY_TASK_ID: task_id,
|
||||
# deque.extend is thread-safe
|
||||
CONFIG_KEY_SEND: partial(
|
||||
local_write,
|
||||
step,
|
||||
writes.extend,
|
||||
processes,
|
||||
channels,
|
||||
managed,
|
||||
),
|
||||
CONFIG_KEY_READ: partial(
|
||||
local_read,
|
||||
step,
|
||||
checkpoint,
|
||||
channels,
|
||||
managed,
|
||||
PregelTaskWrites(packet.node, writes, triggers),
|
||||
config,
|
||||
),
|
||||
CONFIG_KEY_CHECKPOINTER: (
|
||||
checkpointer
|
||||
or configurable.get(CONFIG_KEY_CHECKPOINTER)
|
||||
),
|
||||
CONFIG_KEY_CHECKPOINT_MAP: {
|
||||
**configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
|
||||
parent_ns: checkpoint["id"],
|
||||
},
|
||||
CONFIG_KEY_RESUMING: is_resuming,
|
||||
"checkpoint_id": None,
|
||||
"checkpoint_ns": task_checkpoint_ns,
|
||||
},
|
||||
),
|
||||
triggers,
|
||||
proc.retry_policy,
|
||||
None,
|
||||
task_id,
|
||||
)
|
||||
|
||||
else:
|
||||
return PregelTask(task_id, packet.node)
|
||||
elif task_path[0] == SUBSCRIPTIONS:
|
||||
name = str(task_path[1])
|
||||
proc = processes[name]
|
||||
version_type = type(next(iter(checkpoint["channel_versions"].values()), None))
|
||||
null_version = version_type()
|
||||
if null_version is None:
|
||||
return
|
||||
seen = checkpoint["versions_seen"].get(name, {})
|
||||
# If any of the channels read by this process were updated
|
||||
if triggers := sorted(
|
||||
chan
|
||||
for chan in proc.triggers
|
||||
if not isinstance(
|
||||
read_channel(channels, chan, return_exception=True), EmptyChannelError
|
||||
)
|
||||
and checkpoint["channel_versions"].get(chan, null_version)
|
||||
> seen.get(chan, null_version)
|
||||
):
|
||||
try:
|
||||
val = next(
|
||||
_proc_input(
|
||||
step, proc, managed, channels, for_execution=for_execution
|
||||
)
|
||||
)
|
||||
except StopIteration:
|
||||
return
|
||||
|
||||
# create task id
|
||||
metadata = {
|
||||
"langgraph_step": step,
|
||||
"langgraph_node": name,
|
||||
"langgraph_triggers": triggers,
|
||||
"langgraph_path": task_path,
|
||||
}
|
||||
checkpoint_ns = f"{parent_ns}{NS_SEP}{name}" if parent_ns else name
|
||||
task_id = _uuid5_str(
|
||||
checkpoint_id,
|
||||
checkpoint_ns,
|
||||
str(step),
|
||||
name,
|
||||
SUBSCRIPTIONS,
|
||||
*triggers,
|
||||
)
|
||||
if task_id_checksum is not None:
|
||||
assert task_id == task_id_checksum
|
||||
|
||||
if for_execution:
|
||||
if node := proc.node:
|
||||
writes = deque()
|
||||
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
|
||||
return PregelExecutableTask(
|
||||
name,
|
||||
val,
|
||||
node,
|
||||
writes,
|
||||
patch_config(
|
||||
merge_configs(
|
||||
config,
|
||||
processes[packet.node].config,
|
||||
proc.config,
|
||||
{"metadata": metadata},
|
||||
),
|
||||
run_name=packet.node,
|
||||
run_name=name,
|
||||
callbacks=(
|
||||
manager.get_child(f"graph:step:{step}")
|
||||
if manager
|
||||
@@ -339,7 +513,7 @@ def prepare_next_tasks(
|
||||
checkpoint,
|
||||
channels,
|
||||
managed,
|
||||
PregelTaskWrites(packet.node, writes, triggers),
|
||||
PregelTaskWrites(name, writes, triggers),
|
||||
config,
|
||||
),
|
||||
CONFIG_KEY_CHECKPOINTER: (
|
||||
@@ -351,7 +525,6 @@ def prepare_next_tasks(
|
||||
parent_ns: checkpoint["id"],
|
||||
},
|
||||
CONFIG_KEY_RESUMING: is_resuming,
|
||||
"checkpoint_id": None,
|
||||
"checkpoint_ns": task_checkpoint_ns,
|
||||
},
|
||||
),
|
||||
@@ -360,116 +533,8 @@ def prepare_next_tasks(
|
||||
None,
|
||||
task_id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
tasks.append(PregelTask(task_id, packet.node))
|
||||
# Check if any processes should be run in next step
|
||||
# If so, prepare the values to be passed to them
|
||||
version_type = type(next(iter(checkpoint["channel_versions"].values()), None))
|
||||
null_version = version_type()
|
||||
if null_version is None:
|
||||
return tasks
|
||||
for name, proc in processes.items():
|
||||
seen = checkpoint["versions_seen"].get(name, {})
|
||||
# If any of the channels read by this process were updated
|
||||
if triggers := sorted(
|
||||
chan
|
||||
for chan in proc.triggers
|
||||
if not isinstance(
|
||||
read_channel(channels, chan, return_exception=True), EmptyChannelError
|
||||
)
|
||||
and checkpoint["channel_versions"].get(chan, null_version)
|
||||
> seen.get(chan, null_version)
|
||||
):
|
||||
try:
|
||||
val = next(
|
||||
_proc_input(
|
||||
step, proc, managed, channels, for_execution=for_execution
|
||||
)
|
||||
)
|
||||
except StopIteration:
|
||||
continue
|
||||
|
||||
# create task id
|
||||
metadata = {
|
||||
"langgraph_step": step,
|
||||
"langgraph_node": name,
|
||||
"langgraph_triggers": triggers,
|
||||
"langgraph_task_idx": len(tasks),
|
||||
}
|
||||
checkpoint_ns = f"{parent_ns}{NS_SEP}{name}" if parent_ns else name
|
||||
task_id = str(
|
||||
uuid5(
|
||||
UUID(checkpoint["id"]),
|
||||
json.dumps((checkpoint_ns, metadata)),
|
||||
)
|
||||
)
|
||||
|
||||
if for_execution:
|
||||
if node := proc.get_node():
|
||||
writes = deque()
|
||||
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
|
||||
tasks.append(
|
||||
PregelExecutableTask(
|
||||
name,
|
||||
val,
|
||||
node,
|
||||
writes,
|
||||
patch_config(
|
||||
merge_configs(
|
||||
config,
|
||||
proc.config,
|
||||
{"metadata": metadata},
|
||||
),
|
||||
run_name=name,
|
||||
callbacks=(
|
||||
manager.get_child(f"graph:step:{step}")
|
||||
if manager
|
||||
else None
|
||||
),
|
||||
configurable={
|
||||
CONFIG_KEY_TASK_ID: task_id,
|
||||
# deque.extend is thread-safe
|
||||
CONFIG_KEY_SEND: partial(
|
||||
local_write,
|
||||
step,
|
||||
writes.extend,
|
||||
processes,
|
||||
channels,
|
||||
managed,
|
||||
),
|
||||
CONFIG_KEY_READ: partial(
|
||||
local_read,
|
||||
step,
|
||||
checkpoint,
|
||||
channels,
|
||||
managed,
|
||||
PregelTaskWrites(name, writes, triggers),
|
||||
config,
|
||||
),
|
||||
CONFIG_KEY_CHECKPOINTER: (
|
||||
checkpointer
|
||||
or configurable.get(CONFIG_KEY_CHECKPOINTER)
|
||||
),
|
||||
CONFIG_KEY_CHECKPOINT_MAP: {
|
||||
**configurable.get(
|
||||
CONFIG_KEY_CHECKPOINT_MAP, {}
|
||||
),
|
||||
parent_ns: checkpoint["id"],
|
||||
},
|
||||
CONFIG_KEY_RESUMING: is_resuming,
|
||||
"checkpoint_ns": task_checkpoint_ns,
|
||||
},
|
||||
),
|
||||
triggers,
|
||||
proc.retry_policy,
|
||||
None,
|
||||
task_id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
tasks.append(PregelTask(task_id, name))
|
||||
return tasks
|
||||
return PregelTask(task_id, name)
|
||||
|
||||
|
||||
def _proc_input(
|
||||
@@ -516,3 +581,12 @@ def _proc_input(
|
||||
val = proc.mapper(val)
|
||||
|
||||
yield val
|
||||
|
||||
|
||||
def _uuid5_str(namespace: bytes, *parts: str) -> str:
|
||||
"""Generate a UUID from the SHA-1 hash of a namespace UUID and a name."""
|
||||
|
||||
sha = sha1(namespace, usedforsecurity=False)
|
||||
sha.update(b"".join(p.encode() for p in parts))
|
||||
hex = sha.hexdigest()
|
||||
return f"{hex[:8]}-{hex[8:12]}-{hex[12:16]}-{hex[16:20]}-{hex[20:32]}"
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
from typing import Any, Optional
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
from langgraph.checkpoint.base import CheckpointMetadata
|
||||
from langgraph.constants import CONFIG_KEY_CHECKPOINT_MAP
|
||||
|
||||
|
||||
def patch_configurable(
|
||||
config: Optional[RunnableConfig], patch: dict[str, Any]
|
||||
) -> RunnableConfig:
|
||||
if config is None:
|
||||
return {"configurable": patch}
|
||||
else:
|
||||
return {**config, "configurable": {**config["configurable"], **patch}}
|
||||
|
||||
|
||||
def patch_checkpoint_map(
|
||||
config: RunnableConfig, metadata: Optional[CheckpointMetadata]
|
||||
) -> RunnableConfig:
|
||||
if parents := (metadata.get("parents") if metadata else None):
|
||||
return patch_configurable(
|
||||
config,
|
||||
{
|
||||
CONFIG_KEY_CHECKPOINT_MAP: {
|
||||
**parents,
|
||||
config["configurable"]["checkpoint_ns"]: config["configurable"][
|
||||
"checkpoint_id"
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
else:
|
||||
return config
|
||||
@@ -1,10 +1,9 @@
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from dataclasses import asdict
|
||||
from datetime import datetime, timezone
|
||||
from pprint import pformat
|
||||
from typing import Any, Iterator, Literal, Mapping, Optional, Sequence, TypedDict, Union
|
||||
from uuid import UUID, uuid5
|
||||
from uuid import UUID
|
||||
|
||||
from langchain_core.runnables.config import RunnableConfig
|
||||
from langchain_core.utils.input import get_bolded_text, get_colored_text
|
||||
@@ -82,17 +81,12 @@ def map_debug_tasks(
|
||||
if task.config is not None and TAG_HIDDEN in task.config.get("tags", []):
|
||||
continue
|
||||
|
||||
metadata = task.config["metadata"].copy()
|
||||
metadata.pop("checkpoint_id", None)
|
||||
|
||||
yield {
|
||||
"type": "task",
|
||||
"timestamp": ts,
|
||||
"step": step,
|
||||
"payload": {
|
||||
"id": str(
|
||||
uuid5(TASK_NAMESPACE, json.dumps((task.name, step, metadata)))
|
||||
),
|
||||
"id": task.id,
|
||||
"name": task.name,
|
||||
"input": task.input,
|
||||
"triggers": task.triggers,
|
||||
@@ -102,35 +96,25 @@ def map_debug_tasks(
|
||||
|
||||
def map_debug_task_results(
|
||||
step: int,
|
||||
tasks: list[tuple[PregelExecutableTask, Sequence[tuple[str, Any]]]],
|
||||
task_tup: tuple[PregelExecutableTask, Sequence[tuple[str, Any]]],
|
||||
stream_keys: Union[str, Sequence[str]],
|
||||
) -> Iterator[DebugOutputTaskResult]:
|
||||
stream_channels_list = (
|
||||
[stream_keys] if isinstance(stream_keys, str) else stream_keys
|
||||
)
|
||||
ts = datetime.now(timezone.utc).isoformat()
|
||||
for task, writes in tasks:
|
||||
if task.config is not None and TAG_HIDDEN in task.config.get("tags", []):
|
||||
continue
|
||||
|
||||
metadata = task.config["metadata"].copy()
|
||||
metadata.pop("checkpoint_id", None)
|
||||
# TODO: make task IDs deterministic in tests and reuse task IDs for payload ID
|
||||
|
||||
yield {
|
||||
"type": "task_result",
|
||||
"timestamp": ts,
|
||||
"step": step,
|
||||
"payload": {
|
||||
"id": str(
|
||||
uuid5(TASK_NAMESPACE, json.dumps((task.name, step, metadata)))
|
||||
),
|
||||
"name": task.name,
|
||||
"error": next((w[1] for w in writes if w[0] == ERROR), None),
|
||||
"result": [w for w in writes if w[0] in stream_channels_list],
|
||||
"interrupts": [asdict(w[1]) for w in writes if w[0] == INTERRUPT],
|
||||
},
|
||||
}
|
||||
task, writes = task_tup
|
||||
yield {
|
||||
"type": "task_result",
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"step": step,
|
||||
"payload": {
|
||||
"id": task.id,
|
||||
"name": task.name,
|
||||
"error": next((w[1] for w in writes if w[0] == ERROR), None),
|
||||
"result": [w for w in writes if w[0] in stream_channels_list],
|
||||
"interrupts": [asdict(w[1]) for w in writes if w[0] == INTERRUPT],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def map_debug_checkpoint(
|
||||
|
||||
@@ -99,6 +99,7 @@ class AsyncBackgroundExecutor(AsyncContextManager):
|
||||
self.context_not_supported = sys.version_info < (3, 11)
|
||||
self.tasks: dict[asyncio.Task, bool] = {}
|
||||
self.sentinel = object()
|
||||
self.loop = asyncio.get_running_loop()
|
||||
|
||||
def submit(
|
||||
self,
|
||||
@@ -110,9 +111,9 @@ class AsyncBackgroundExecutor(AsyncContextManager):
|
||||
) -> asyncio.Task[T]:
|
||||
coro = fn(*args, **kwargs)
|
||||
if self.context_not_supported:
|
||||
task = asyncio.create_task(coro, name=__name__)
|
||||
task = self.loop.create_task(coro, name=__name__)
|
||||
else:
|
||||
task = asyncio.create_task(coro, name=__name__, context=copy_context())
|
||||
task = self.loop.create_task(coro, name=__name__, context=copy_context())
|
||||
self.tasks[task] = __cancel_on_exit__
|
||||
task.add_done_callback(self.done)
|
||||
return task
|
||||
|
||||
@@ -107,26 +107,27 @@ def map_output_updates(
|
||||
(t, ww)
|
||||
for t, ww in tasks
|
||||
if (not t.config or TAG_HIDDEN not in t.config.get("tags"))
|
||||
and all(k not in (ERROR, INTERRUPT) for k, _ in ww)
|
||||
and ww[0][0] != ERROR
|
||||
and ww[0][0] != INTERRUPT
|
||||
]
|
||||
if not output_tasks:
|
||||
return
|
||||
if isinstance(output_channels, str):
|
||||
updated = [
|
||||
updated = (
|
||||
(task.name, value)
|
||||
for task, writes in output_tasks
|
||||
for chan, value in writes
|
||||
if chan == output_channels
|
||||
]
|
||||
)
|
||||
else:
|
||||
updated = [
|
||||
updated = (
|
||||
(
|
||||
task.name,
|
||||
{chan: value for chan, value in task.writes if chan in output_channels},
|
||||
)
|
||||
for task, writes in output_tasks
|
||||
if any(chan in output_channels for chan, _ in writes)
|
||||
]
|
||||
)
|
||||
grouped = {t.name: [] for t, _ in output_tasks}
|
||||
for node, value in updated:
|
||||
grouped[node].append(value)
|
||||
|
||||
@@ -2,7 +2,6 @@ import asyncio
|
||||
import concurrent.futures
|
||||
from collections import deque
|
||||
from contextlib import AsyncExitStack, ExitStack
|
||||
from itertools import tee
|
||||
from types import TracebackType
|
||||
from typing import (
|
||||
Any,
|
||||
@@ -46,6 +45,7 @@ from langgraph.constants import (
|
||||
ERROR,
|
||||
INPUT,
|
||||
INTERRUPT,
|
||||
TAG_HIDDEN,
|
||||
)
|
||||
from langgraph.errors import EmptyInputError, GraphInterrupt
|
||||
from langgraph.managed.base import (
|
||||
@@ -60,11 +60,13 @@ from langgraph.pregel.algo import (
|
||||
prepare_next_tasks,
|
||||
should_interrupt,
|
||||
)
|
||||
from langgraph.pregel.config import patch_configurable
|
||||
from langgraph.pregel.debug import (
|
||||
map_debug_checkpoint,
|
||||
map_debug_task_results,
|
||||
map_debug_tasks,
|
||||
print_step_checkpoint,
|
||||
print_step_tasks,
|
||||
print_step_writes,
|
||||
)
|
||||
from langgraph.pregel.executor import (
|
||||
AsyncBackgroundExecutor,
|
||||
@@ -84,6 +86,7 @@ from langgraph.pregel.types import PregelExecutableTask
|
||||
from langgraph.pregel.utils import get_new_channel_versions
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.store.batch import AsyncBatchedStore
|
||||
from langgraph.utils.config import patch_configurable
|
||||
|
||||
V = TypeVar("V")
|
||||
INPUT_DONE = object()
|
||||
@@ -92,24 +95,16 @@ EMPTY_SEQ = ()
|
||||
|
||||
|
||||
class StreamProtocol(Protocol):
|
||||
def extend(self, values: Iterable[Tuple[str, str, Any]]) -> None: ...
|
||||
def popleft(self) -> Tuple[str, str, Any]: ...
|
||||
def __bool__(self) -> bool: ...
|
||||
def __call__(self, values: Iterable[Tuple[str, str, Any]]) -> None: ...
|
||||
|
||||
|
||||
class DuplexStream(StreamProtocol):
|
||||
def __init__(self, *streams: StreamProtocol) -> None:
|
||||
self.streams = streams
|
||||
def __init__(self, *queues: StreamProtocol) -> None:
|
||||
self.queues = queues
|
||||
|
||||
def extend(self, values: Iterable[Tuple[str, str, Any]]) -> None:
|
||||
for stream, vv in zip(self.streams, tee(values, len(self.streams))):
|
||||
stream.extend(vv)
|
||||
|
||||
def popleft(self) -> Tuple[str, str, Any]:
|
||||
return self.streams[0].popleft()
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self.streams[0])
|
||||
def __call__(self, value: Tuple[str, str, Any]) -> None:
|
||||
for queue in self.queues:
|
||||
queue(value)
|
||||
|
||||
|
||||
class PregelLoop:
|
||||
@@ -121,8 +116,9 @@ class PregelLoop:
|
||||
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]]
|
||||
output_keys: Union[str, Sequence[str]]
|
||||
stream_keys: Union[str, Sequence[str]]
|
||||
is_nested: bool
|
||||
stream: Optional[StreamProtocol]
|
||||
skip_done_tasks: bool
|
||||
is_nested: bool
|
||||
|
||||
checkpointer_get_next_version: Callable[[Optional[V]], V]
|
||||
checkpointer_put_writes: Optional[
|
||||
@@ -154,7 +150,6 @@ class PregelLoop:
|
||||
"pending", "done", "interrupt_before", "interrupt_after", "out_of_steps"
|
||||
]
|
||||
tasks: Sequence[PregelExecutableTask]
|
||||
stream: StreamProtocol
|
||||
output: Union[None, dict[str, Any], Any] = None
|
||||
|
||||
# public
|
||||
@@ -163,6 +158,7 @@ class PregelLoop:
|
||||
self,
|
||||
input: Optional[Any],
|
||||
*,
|
||||
stream: Optional[StreamProtocol],
|
||||
config: RunnableConfig,
|
||||
store: Optional[BaseStore],
|
||||
checkpointer: Optional[BaseCheckpointSaver],
|
||||
@@ -170,8 +166,9 @@ class PregelLoop:
|
||||
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]],
|
||||
output_keys: Union[str, Sequence[str]],
|
||||
stream_keys: Union[str, Sequence[str]],
|
||||
debug: bool = False,
|
||||
) -> None:
|
||||
self.stream = deque()
|
||||
self.stream = stream
|
||||
self.input = input
|
||||
self.config = config
|
||||
self.store = store
|
||||
@@ -182,6 +179,7 @@ class PregelLoop:
|
||||
self.stream_keys = stream_keys
|
||||
self.is_nested = CONFIG_KEY_TASK_ID in self.config.get("configurable", {})
|
||||
self.skip_done_tasks = "checkpoint_id" not in config["configurable"]
|
||||
self.debug = debug
|
||||
if CONFIG_KEY_STREAM in config["configurable"]:
|
||||
self.stream = DuplexStream(
|
||||
self.stream, config["configurable"][CONFIG_KEY_STREAM]
|
||||
@@ -229,22 +227,6 @@ class PregelLoop:
|
||||
)
|
||||
self._output_writes(task_id, writes)
|
||||
|
||||
def _output_writes(
|
||||
self, task_id: str, writes: Sequence[tuple[str, Any]], *, cached: bool = False
|
||||
) -> None:
|
||||
if task := next((t for t in self.tasks if t.id == task_id), None):
|
||||
self.stream.extend(
|
||||
(self.config["configurable"].get("checkpoint_ns", ""), "updates", v)
|
||||
for v in map_output_updates(self.output_keys, [(task, writes)], cached)
|
||||
)
|
||||
if not cached:
|
||||
self.stream.extend(
|
||||
(self.config["configurable"].get("checkpoint_ns", ""), "debug", v)
|
||||
for v in map_debug_task_results(
|
||||
self.step, [(task, writes)], self.stream_keys
|
||||
)
|
||||
)
|
||||
|
||||
def tick(
|
||||
self,
|
||||
*,
|
||||
@@ -263,6 +245,15 @@ class PregelLoop:
|
||||
self._first(input_keys=input_keys)
|
||||
elif all(task.writes for task in self.tasks):
|
||||
writes = [w for t in self.tasks for w in t.writes]
|
||||
# debug flag
|
||||
if self.debug:
|
||||
print_step_writes(
|
||||
self.step,
|
||||
writes,
|
||||
[self.stream_keys]
|
||||
if isinstance(self.stream_keys, str)
|
||||
else self.stream_keys,
|
||||
)
|
||||
# all tasks have finished
|
||||
mv_writes = apply_writes(
|
||||
self.checkpoint,
|
||||
@@ -274,7 +265,7 @@ class PregelLoop:
|
||||
for key, values in mv_writes.items():
|
||||
self._update_mv(key, values)
|
||||
# produce values output
|
||||
self.stream.extend(
|
||||
self._emit(
|
||||
(self.config["configurable"].get("checkpoint_ns", ""), "values", v)
|
||||
for v in map_output_values(self.output_keys, writes, self.channels)
|
||||
)
|
||||
@@ -322,7 +313,7 @@ class PregelLoop:
|
||||
|
||||
# produce debug output
|
||||
if self._checkpointer_put_after_previous is not None:
|
||||
self.stream.extend(
|
||||
self._emit(
|
||||
(self.config["configurable"].get("checkpoint_ns", ""), "debug", v)
|
||||
for v in map_debug_checkpoint(
|
||||
self.step - 1, # printing checkpoint for previous step
|
||||
@@ -371,11 +362,15 @@ class PregelLoop:
|
||||
return False
|
||||
|
||||
# produce debug output
|
||||
self.stream.extend(
|
||||
self._emit(
|
||||
(self.config["configurable"].get("checkpoint_ns", ""), "debug", v)
|
||||
for v in map_debug_tasks(self.step, self.tasks)
|
||||
)
|
||||
|
||||
# debug flag
|
||||
if self.debug:
|
||||
print_step_tasks(self.step, self.tasks)
|
||||
|
||||
return True
|
||||
|
||||
# private
|
||||
@@ -397,7 +392,7 @@ class PregelLoop:
|
||||
version = self.checkpoint["channel_versions"][k]
|
||||
self.checkpoint["versions_seen"][INTERRUPT][k] = version
|
||||
# produce values output
|
||||
self.stream.extend(
|
||||
self._emit(
|
||||
(self.config["configurable"].get("checkpoint_ns", ""), "values", v)
|
||||
for v in map_output_values(self.output_keys, True, self.channels)
|
||||
)
|
||||
@@ -434,14 +429,20 @@ class PregelLoop:
|
||||
metadata["parents"] = self.config["configurable"].get(
|
||||
CONFIG_KEY_CHECKPOINT_MAP, {}
|
||||
)
|
||||
# debug flag
|
||||
if self.debug:
|
||||
print_step_checkpoint(
|
||||
metadata,
|
||||
self.channels,
|
||||
[self.stream_keys]
|
||||
if isinstance(self.stream_keys, str)
|
||||
else self.stream_keys,
|
||||
)
|
||||
# create new checkpoint
|
||||
self.checkpoint = create_checkpoint(self.checkpoint, self.channels, self.step)
|
||||
# bail if no checkpointer
|
||||
if self._checkpointer_put_after_previous is not None:
|
||||
# create new checkpoint
|
||||
self.checkpoint_metadata = metadata
|
||||
self.checkpoint = create_checkpoint(
|
||||
self.checkpoint, self.channels, self.step
|
||||
)
|
||||
|
||||
self.checkpoint_config = {
|
||||
**self.checkpoint_config,
|
||||
"configurable": {
|
||||
@@ -456,7 +457,6 @@ class PregelLoop:
|
||||
new_versions = get_new_channel_versions(
|
||||
self.checkpoint_previous_versions, channel_versions
|
||||
)
|
||||
|
||||
self.checkpoint_previous_versions = channel_versions
|
||||
|
||||
# save it, without blocking
|
||||
@@ -497,12 +497,40 @@ class PregelLoop:
|
||||
# suppress interrupt
|
||||
return True
|
||||
|
||||
def _emit(self, values: Sequence[tuple[str, str, Any]]) -> None:
|
||||
if self.stream is None:
|
||||
return
|
||||
for v in values:
|
||||
self.stream(v)
|
||||
|
||||
def _output_writes(
|
||||
self, task_id: str, writes: Sequence[tuple[str, Any]], *, cached: bool = False
|
||||
) -> None:
|
||||
if task := next((t for t in self.tasks if t.id == task_id), None):
|
||||
if task.config is not None and TAG_HIDDEN in task.config.get("tags"):
|
||||
return
|
||||
if writes[0][0] != ERROR and writes[0][0] != INTERRUPT:
|
||||
self._emit(
|
||||
(self.config["configurable"].get("checkpoint_ns", ""), "updates", v)
|
||||
for v in map_output_updates(
|
||||
self.output_keys, [(task, writes)], cached
|
||||
)
|
||||
)
|
||||
if not cached:
|
||||
self._emit(
|
||||
(self.config["configurable"].get("checkpoint_ns", ""), "debug", v)
|
||||
for v in map_debug_task_results(
|
||||
self.step, (task, writes), self.stream_keys
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class SyncPregelLoop(PregelLoop, ContextManager):
|
||||
def __init__(
|
||||
self,
|
||||
input: Optional[Any],
|
||||
*,
|
||||
stream: Optional[StreamProtocol],
|
||||
config: RunnableConfig,
|
||||
store: Optional[BaseStore],
|
||||
checkpointer: Optional[BaseCheckpointSaver],
|
||||
@@ -510,9 +538,11 @@ class SyncPregelLoop(PregelLoop, ContextManager):
|
||||
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]],
|
||||
output_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
|
||||
stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
|
||||
debug: bool = False,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
input,
|
||||
stream=stream,
|
||||
config=config,
|
||||
checkpointer=checkpointer,
|
||||
store=store,
|
||||
@@ -520,6 +550,7 @@ class SyncPregelLoop(PregelLoop, ContextManager):
|
||||
specs=specs,
|
||||
output_keys=output_keys,
|
||||
stream_keys=stream_keys,
|
||||
debug=debug,
|
||||
)
|
||||
self.stack = ExitStack()
|
||||
if checkpointer:
|
||||
@@ -594,6 +625,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager):
|
||||
self,
|
||||
input: Optional[Any],
|
||||
*,
|
||||
stream: Optional[StreamProtocol],
|
||||
config: RunnableConfig,
|
||||
store: Optional[BaseStore],
|
||||
checkpointer: Optional[BaseCheckpointSaver],
|
||||
@@ -601,9 +633,11 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager):
|
||||
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]],
|
||||
output_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
|
||||
stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
|
||||
debug: bool = False,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
input,
|
||||
stream=stream,
|
||||
config=config,
|
||||
checkpointer=checkpointer,
|
||||
store=store,
|
||||
@@ -611,6 +645,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager):
|
||||
specs=specs,
|
||||
output_keys=output_keys,
|
||||
stream_keys=stream_keys,
|
||||
debug=debug,
|
||||
)
|
||||
self.store = AsyncBatchedStore(self.store) if self.store else None
|
||||
self.stack = AsyncExitStack()
|
||||
|
||||
@@ -2,7 +2,7 @@ import asyncio
|
||||
from contextlib import AsyncExitStack, ExitStack, asynccontextmanager, contextmanager
|
||||
from typing import AsyncIterator, Iterator, Mapping, Optional, Union
|
||||
|
||||
from langchain_core.runnables import RunnableConfig, patch_config
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.checkpoint.base import Checkpoint
|
||||
@@ -14,6 +14,7 @@ from langgraph.managed.base import (
|
||||
)
|
||||
from langgraph.managed.context import Context
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.utils.config import patch_configurable
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -26,7 +27,7 @@ def ChannelsManager(
|
||||
skip_context: bool = False,
|
||||
) -> Iterator[tuple[Mapping[str, BaseChannel], ManagedValueMapping]]:
|
||||
"""Manage channels for the lifetime of a Pregel invocation (multiple steps)."""
|
||||
config_for_managed = patch_config(config, configurable={CONFIG_KEY_STORE: store})
|
||||
config_for_managed = patch_configurable(config, {CONFIG_KEY_STORE: store})
|
||||
channel_specs: Mapping[str, BaseChannel] = {}
|
||||
managed_specs: Mapping[str, ManagedValueSpec] = {}
|
||||
for k, v in specs.items():
|
||||
@@ -69,7 +70,7 @@ async def AsyncChannelsManager(
|
||||
skip_context: bool = False,
|
||||
) -> AsyncIterator[Mapping[str, BaseChannel]]:
|
||||
"""Manage channels for the lifetime of a Pregel invocation (multiple steps)."""
|
||||
config_for_managed = patch_config(config, configurable={CONFIG_KEY_STORE: store})
|
||||
config_for_managed = patch_configurable(config, {CONFIG_KEY_STORE: store})
|
||||
channel_specs: Mapping[str, BaseChannel] = {}
|
||||
managed_specs: Mapping[str, ManagedValueSpec] = {}
|
||||
for k, v in specs.items():
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, Mapping, Optional, Sequence, Union
|
||||
from functools import cached_property
|
||||
from typing import (
|
||||
Any,
|
||||
AsyncIterator,
|
||||
Callable,
|
||||
Iterator,
|
||||
Mapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
Union,
|
||||
)
|
||||
|
||||
from langchain_core.pydantic_v1 import Field
|
||||
from langchain_core.runnables import (
|
||||
Runnable,
|
||||
RunnableConfig,
|
||||
RunnablePassthrough,
|
||||
RunnableSequence,
|
||||
RunnableSerializable,
|
||||
)
|
||||
from langchain_core.runnables.base import Other, RunnableBindingBase, coerce_to_runnable
|
||||
from langchain_core.runnables.config import merge_configs
|
||||
from langchain_core.runnables.base import Input, Other, Output, coerce_to_runnable
|
||||
from langchain_core.runnables.utils import ConfigurableFieldSpec
|
||||
|
||||
from langgraph.constants import CONFIG_KEY_READ
|
||||
from langgraph.pregel.retry import RetryPolicy
|
||||
from langgraph.pregel.write import ChannelWrite
|
||||
from langgraph.utils import RunnableCallable
|
||||
from langgraph.utils.config import merge_configs
|
||||
from langgraph.utils.runnable import RunnableCallable, RunnableSeq
|
||||
|
||||
READ_TYPE = Callable[[str, bool], Union[Any, dict[str, Any]]]
|
||||
|
||||
@@ -99,22 +107,50 @@ class ChannelRead(RunnableCallable):
|
||||
DEFAULT_BOUND: RunnablePassthrough = RunnablePassthrough()
|
||||
|
||||
|
||||
class PregelNode(RunnableBindingBase):
|
||||
class PregelNode(Runnable):
|
||||
channels: Union[list[str], Mapping[str, str]]
|
||||
|
||||
triggers: list[str] = Field(default_factory=list)
|
||||
triggers: list[str]
|
||||
|
||||
mapper: Optional[Callable[[Any], Any]] = None
|
||||
mapper: Optional[Callable[[Any], Any]]
|
||||
|
||||
writers: list[Runnable] = Field(default_factory=list)
|
||||
writers: list[Runnable]
|
||||
|
||||
bound: Runnable[Any, Any] = Field(default=DEFAULT_BOUND)
|
||||
bound: Runnable[Any, Any]
|
||||
|
||||
kwargs: Mapping[str, Any] = Field(default_factory=dict)
|
||||
retry_policy: Optional[RetryPolicy]
|
||||
|
||||
retry_policy: Optional[RetryPolicy] = None
|
||||
config: RunnableConfig
|
||||
|
||||
def get_writers(self) -> list[Runnable]:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
channels: Union[list[str], Mapping[str, str]],
|
||||
triggers: Sequence[str],
|
||||
mapper: Optional[Callable[[Any], Any]] = None,
|
||||
writers: Optional[list[Runnable]] = None,
|
||||
tags: Optional[list[str]] = None,
|
||||
metadata: Optional[Mapping[str, Any]] = None,
|
||||
bound: Optional[Runnable[Any, Any]] = None,
|
||||
retry_policy: Optional[RetryPolicy] = None,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
) -> None:
|
||||
self.channels = channels
|
||||
self.triggers = list(triggers)
|
||||
self.mapper = mapper
|
||||
self.writers = writers or []
|
||||
self.bound = bound if bound is not None else DEFAULT_BOUND
|
||||
self.retry_policy = retry_policy
|
||||
self.config = merge_configs(
|
||||
config, {"tags": tags or [], "metadata": metadata or {}}
|
||||
)
|
||||
|
||||
def copy(self, update: dict[str, Any]) -> PregelNode:
|
||||
attrs = {**self.__dict__, **update}
|
||||
return PregelNode(**attrs)
|
||||
|
||||
@cached_property
|
||||
def flat_writers(self) -> list[Runnable]:
|
||||
"""Get writers with optimizations applied."""
|
||||
writers = self.writers.copy()
|
||||
while (
|
||||
@@ -132,51 +168,20 @@ class PregelNode(RunnableBindingBase):
|
||||
writers.pop()
|
||||
return writers
|
||||
|
||||
def get_node(self) -> Optional[Runnable[Any, Any]]:
|
||||
writers = self.get_writers()
|
||||
@cached_property
|
||||
def node(self) -> Optional[Runnable[Any, Any]]:
|
||||
writers = self.flat_writers
|
||||
if self.bound is DEFAULT_BOUND and not writers:
|
||||
return None
|
||||
elif self.bound is DEFAULT_BOUND and len(writers) == 1:
|
||||
return writers[0]
|
||||
elif self.bound is DEFAULT_BOUND:
|
||||
return RunnableSequence(*writers)
|
||||
return RunnableSeq(*writers)
|
||||
elif writers:
|
||||
return RunnableSequence(self.bound, *writers)
|
||||
return RunnableSeq(self.bound, *writers)
|
||||
else:
|
||||
return self.bound
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
channels: Union[list[str], Mapping[str, str]],
|
||||
triggers: Sequence[str],
|
||||
mapper: Optional[Callable[[Any], Any]] = None,
|
||||
writers: Optional[list[Runnable]] = None,
|
||||
tags: Optional[list[str]] = None,
|
||||
metadata: Optional[Mapping[str, Any]] = None,
|
||||
bound: Optional[Runnable[Any, Any]] = None,
|
||||
kwargs: Optional[Mapping[str, Any]] = None,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
retry_policy: Optional[RetryPolicy] = None,
|
||||
**other_kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
channels=channels,
|
||||
triggers=triggers,
|
||||
mapper=mapper,
|
||||
writers=writers or [],
|
||||
bound=bound or DEFAULT_BOUND,
|
||||
kwargs=kwargs or {},
|
||||
retry_policy=retry_policy,
|
||||
config=merge_configs(
|
||||
config, {"tags": tags or [], "metadata": metadata or {}}
|
||||
),
|
||||
**other_kwargs,
|
||||
)
|
||||
|
||||
def __repr_args__(self) -> Any:
|
||||
return [(k, v) for k, v in super().__repr_args__() if k != "bound"]
|
||||
|
||||
def join(self, channels: Sequence[str]) -> PregelNode:
|
||||
assert isinstance(channels, list) or isinstance(
|
||||
channels, tuple
|
||||
@@ -206,7 +211,7 @@ class PregelNode(RunnableBindingBase):
|
||||
elif self.bound is DEFAULT_BOUND:
|
||||
return self.copy(update=dict(bound=coerce_to_runnable(other)))
|
||||
else:
|
||||
return self.copy(update=dict(bound=self.bound | other))
|
||||
return self.copy(update=dict(bound=RunnableSeq(self.bound, other)))
|
||||
|
||||
def pipe(
|
||||
self,
|
||||
@@ -226,3 +231,42 @@ class PregelNode(RunnableBindingBase):
|
||||
],
|
||||
) -> RunnableSerializable:
|
||||
raise NotImplementedError()
|
||||
|
||||
def invoke(
|
||||
self,
|
||||
input: Input,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
**kwargs: Optional[Any],
|
||||
) -> Output:
|
||||
return self.bound.invoke(input, merge_configs(self.config, config), **kwargs)
|
||||
|
||||
async def ainvoke(
|
||||
self,
|
||||
input: Input,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
**kwargs: Optional[Any],
|
||||
) -> Output:
|
||||
return await self.bound.ainvoke(
|
||||
input, merge_configs(self.config, config), **kwargs
|
||||
)
|
||||
|
||||
def stream(
|
||||
self,
|
||||
input: Input,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
**kwargs: Optional[Any],
|
||||
) -> Iterator[Output]:
|
||||
yield from self.bound.stream(
|
||||
input, merge_configs(self.config, config), **kwargs
|
||||
)
|
||||
|
||||
async def astream(
|
||||
self,
|
||||
input: Input,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
**kwargs: Optional[Any],
|
||||
) -> AsyncIterator[Output]:
|
||||
async for item in self.bound.astream(
|
||||
input, merge_configs(self.config, config), **kwargs
|
||||
):
|
||||
yield item
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import time
|
||||
from typing import (
|
||||
Any,
|
||||
AsyncIterator,
|
||||
Callable,
|
||||
Iterator,
|
||||
Optional,
|
||||
Sequence,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
|
||||
from langgraph.constants import ERROR, INTERRUPT
|
||||
from langgraph.errors import GraphInterrupt
|
||||
from langgraph.pregel.executor import Submit
|
||||
from langgraph.pregel.retry import arun_with_retry, run_with_retry
|
||||
from langgraph.pregel.types import PregelExecutableTask, RetryPolicy
|
||||
|
||||
|
||||
class PregelRunner:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
submit: Submit,
|
||||
put_writes: Callable[[str, Sequence[tuple[str, Any]]], None],
|
||||
use_astream: bool = False,
|
||||
) -> None:
|
||||
self.submit = submit
|
||||
self.put_writes = put_writes
|
||||
self.use_astream = use_astream
|
||||
|
||||
def tick(
|
||||
self,
|
||||
tasks: list[PregelExecutableTask],
|
||||
*,
|
||||
timeout: Optional[float] = None,
|
||||
retry_policy: Optional[RetryPolicy] = None,
|
||||
) -> Iterator[None]:
|
||||
# execute tasks, and wait for one to fail or all to finish.
|
||||
# each task is independent from all other concurrent tasks
|
||||
# yield updates/debug output as each task finishes
|
||||
futures = {
|
||||
self.submit(
|
||||
run_with_retry,
|
||||
task,
|
||||
retry_policy,
|
||||
): task
|
||||
for task in tasks
|
||||
if not task.writes
|
||||
}
|
||||
all_futures = futures.copy()
|
||||
end_time = timeout + time.monotonic() if timeout else None
|
||||
while futures:
|
||||
done, _ = concurrent.futures.wait(
|
||||
futures,
|
||||
return_when=concurrent.futures.FIRST_COMPLETED,
|
||||
timeout=(max(0, end_time - time.monotonic()) if end_time else None),
|
||||
)
|
||||
if not done:
|
||||
break # timed out
|
||||
for fut in done:
|
||||
task = futures.pop(fut)
|
||||
if exc := _exception(fut):
|
||||
if isinstance(exc, GraphInterrupt):
|
||||
# save interrupt to checkpointer
|
||||
self.put_writes(task.id, [(INTERRUPT, i) for i in exc.args[0]])
|
||||
else:
|
||||
# save error to checkpointer
|
||||
self.put_writes(task.id, [(ERROR, exc)])
|
||||
|
||||
else:
|
||||
# save task writes to checkpointer
|
||||
self.put_writes(task.id, task.writes)
|
||||
else:
|
||||
# remove references to loop vars
|
||||
del fut, task
|
||||
# maybe stop other tasks
|
||||
if _should_stop_others(done):
|
||||
break
|
||||
# give control back to the caller
|
||||
yield
|
||||
# panic on failure or timeout
|
||||
_panic_or_proceed(all_futures)
|
||||
|
||||
async def atick(
|
||||
self,
|
||||
tasks: list[PregelExecutableTask],
|
||||
*,
|
||||
timeout: Optional[float] = None,
|
||||
retry_policy: Optional[RetryPolicy] = None,
|
||||
) -> AsyncIterator[None]:
|
||||
loop = asyncio.get_event_loop()
|
||||
# execute tasks, and wait for one to fail or all to finish.
|
||||
# each task is independent from all other concurrent tasks
|
||||
# yield updates/debug output as each task finishes
|
||||
futures = {
|
||||
self.submit(
|
||||
arun_with_retry,
|
||||
task,
|
||||
retry_policy,
|
||||
stream=self.use_astream,
|
||||
__name__=task.name,
|
||||
__cancel_on_exit__=True,
|
||||
): task
|
||||
for task in tasks
|
||||
if not task.writes
|
||||
}
|
||||
all_futures = futures.copy()
|
||||
end_time = timeout + loop.time() if timeout else None
|
||||
while futures:
|
||||
done, _ = await asyncio.wait(
|
||||
futures,
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
timeout=(max(0, end_time - loop.time()) if end_time else None),
|
||||
)
|
||||
if not done:
|
||||
break # timed out
|
||||
for fut in done:
|
||||
task = futures.pop(fut)
|
||||
if exc := _exception(fut):
|
||||
if isinstance(exc, GraphInterrupt):
|
||||
# save interrupt to checkpointer
|
||||
self.put_writes(task.id, [(INTERRUPT, i) for i in exc.args[0]])
|
||||
else:
|
||||
# save error to checkpointer
|
||||
self.put_writes(task.id, [(ERROR, exc)])
|
||||
else:
|
||||
# save task writes to checkpointer
|
||||
self.put_writes(task.id, task.writes)
|
||||
else:
|
||||
# remove references to loop vars
|
||||
del fut, task
|
||||
# maybe stop other tasks
|
||||
if _should_stop_others(done):
|
||||
break
|
||||
# give control back to the caller
|
||||
yield
|
||||
# panic on failure or timeout
|
||||
_panic_or_proceed(all_futures, asyncio.TimeoutError)
|
||||
|
||||
|
||||
def _should_stop_others(
|
||||
done: Union[set[concurrent.futures.Future[Any]], set[asyncio.Task[Any]]],
|
||||
) -> bool:
|
||||
for fut in done:
|
||||
if fut.cancelled():
|
||||
return True
|
||||
if exc := fut.exception():
|
||||
return not isinstance(exc, GraphInterrupt)
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def _exception(
|
||||
fut: Union[concurrent.futures.Future[Any], asyncio.Task[Any]],
|
||||
) -> Optional[BaseException]:
|
||||
if fut.cancelled():
|
||||
if isinstance(fut, asyncio.Task):
|
||||
return asyncio.CancelledError()
|
||||
else:
|
||||
return concurrent.futures.CancelledError()
|
||||
else:
|
||||
return fut.exception()
|
||||
|
||||
|
||||
def _panic_or_proceed(
|
||||
futs: Union[set[concurrent.futures.Future[Any]], set[asyncio.Task[Any]]],
|
||||
timeout_exc_cls: Type[Exception] = TimeoutError,
|
||||
) -> None:
|
||||
done: set[Union[concurrent.futures.Future[Any], asyncio.Task[Any]]] = set()
|
||||
inflight: set[Union[concurrent.futures.Future[Any], asyncio.Task[Any]]] = set()
|
||||
for fut in futs:
|
||||
if fut.done():
|
||||
done.add(fut)
|
||||
else:
|
||||
inflight.add(fut)
|
||||
while done:
|
||||
# if any task failed
|
||||
if exc := _exception(done.pop()):
|
||||
# cancel all pending tasks
|
||||
while inflight:
|
||||
inflight.pop().cancel()
|
||||
# raise the exception
|
||||
raise exc
|
||||
if inflight:
|
||||
# if we got here means we timed out
|
||||
while inflight:
|
||||
# cancel all pending tasks
|
||||
inflight.pop().cancel()
|
||||
# raise timeout error
|
||||
raise timeout_exc_cls("Timed out")
|
||||
@@ -4,11 +4,9 @@ import asyncio
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
List,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
@@ -18,7 +16,7 @@ from langchain_core.runnables.utils import ConfigurableFieldSpec
|
||||
|
||||
from langgraph.constants import CONFIG_KEY_SEND, TASKS, Send
|
||||
from langgraph.errors import InvalidUpdateError
|
||||
from langgraph.utils import RunnableCallable
|
||||
from langgraph.utils.runnable import RunnableCallable
|
||||
|
||||
TYPE_SEND = Callable[[Sequence[tuple[str, Any]]], None]
|
||||
R = TypeVar("R", bound=Runnable)
|
||||
@@ -32,7 +30,7 @@ class ChannelWriteEntry(NamedTuple):
|
||||
channel: str
|
||||
value: Any = PASSTHROUGH
|
||||
skip_none: bool = False
|
||||
mapper: Optional[Runnable] = None
|
||||
mapper: Optional[Callable] = None
|
||||
|
||||
|
||||
class ChannelWrite(RunnableCallable):
|
||||
@@ -59,9 +57,6 @@ class ChannelWrite(RunnableCallable):
|
||||
self.writes = writes
|
||||
self.require_at_least_one_of = require_at_least_one_of
|
||||
|
||||
def __repr_args__(self) -> Any:
|
||||
return [("writes", self.writes)]
|
||||
|
||||
def get_name(
|
||||
self, suffix: Optional[str] = None, *, name: Optional[str] = None
|
||||
) -> str:
|
||||
@@ -82,65 +77,29 @@ class ChannelWrite(RunnableCallable):
|
||||
]
|
||||
|
||||
def _write(self, input: Any, config: RunnableConfig) -> None:
|
||||
# split packets and entries
|
||||
writes = [(TASKS, packet) for packet in self.writes if isinstance(packet, Send)]
|
||||
entries = [
|
||||
write for write in self.writes if isinstance(write, ChannelWriteEntry)
|
||||
writes = [
|
||||
ChannelWriteEntry(write.channel, input, write.skip_none, write.mapper)
|
||||
if isinstance(write, ChannelWriteEntry) and write.value is PASSTHROUGH
|
||||
else write
|
||||
for write in self.writes
|
||||
]
|
||||
for entry in entries:
|
||||
if entry.channel == TASKS:
|
||||
raise InvalidUpdateError("Cannot write to the reserved channel TASKS")
|
||||
# process entries into values
|
||||
values = [
|
||||
input if write.value is PASSTHROUGH else write.value for write in entries
|
||||
]
|
||||
values = [
|
||||
val if write.mapper is None else write.mapper.invoke(val, config)
|
||||
for val, write in zip(values, entries)
|
||||
]
|
||||
values = [
|
||||
(write.channel, val)
|
||||
for val, write in zip(values, entries)
|
||||
if not write.skip_none or val is not None
|
||||
]
|
||||
# write packets and values
|
||||
self.do_write(
|
||||
config,
|
||||
writes + values,
|
||||
writes,
|
||||
self.require_at_least_one_of if input is not None else None,
|
||||
)
|
||||
return input
|
||||
|
||||
async def _awrite(self, input: Any, config: RunnableConfig) -> None:
|
||||
# split packets and entries
|
||||
writes = [(TASKS, packet) for packet in self.writes if isinstance(packet, Send)]
|
||||
entries = [
|
||||
write for write in self.writes if isinstance(write, ChannelWriteEntry)
|
||||
writes = [
|
||||
ChannelWriteEntry(write.channel, input, write.skip_none, write.mapper)
|
||||
if isinstance(write, ChannelWriteEntry) and write.value is PASSTHROUGH
|
||||
else write
|
||||
for write in self.writes
|
||||
]
|
||||
for entry in entries:
|
||||
if entry.channel == TASKS:
|
||||
raise InvalidUpdateError("Cannot write to the reserved channel TASKS")
|
||||
# process entries into values
|
||||
values = [
|
||||
input if write.value is PASSTHROUGH else write.value for write in entries
|
||||
]
|
||||
values = await asyncio.gather(
|
||||
*(
|
||||
_mk_future(val)
|
||||
if write.mapper is None
|
||||
else write.mapper.ainvoke(val, config)
|
||||
for val, write in zip(values, entries)
|
||||
)
|
||||
)
|
||||
values = [
|
||||
(write.channel, val)
|
||||
for val, write in zip(values, entries)
|
||||
if not write.skip_none or val is not None
|
||||
]
|
||||
# write packets and values
|
||||
self.do_write(
|
||||
config,
|
||||
writes + values,
|
||||
writes,
|
||||
self.require_at_least_one_of if input is not None else None,
|
||||
)
|
||||
return input
|
||||
@@ -148,9 +107,32 @@ class ChannelWrite(RunnableCallable):
|
||||
@staticmethod
|
||||
def do_write(
|
||||
config: RunnableConfig,
|
||||
values: List[Tuple[str, Any]],
|
||||
writes: Sequence[Union[ChannelWriteEntry, Send]],
|
||||
require_at_least_one_of: Optional[Sequence[str]] = None,
|
||||
) -> None:
|
||||
# validate
|
||||
for w in writes:
|
||||
if isinstance(w, ChannelWriteEntry):
|
||||
if w.channel == TASKS:
|
||||
raise InvalidUpdateError(
|
||||
"Cannot write to the reserved channel TASKS"
|
||||
)
|
||||
if w.value is PASSTHROUGH:
|
||||
raise InvalidUpdateError("PASSTHROUGH value must be replaced")
|
||||
# split packets and entries
|
||||
sends = [(TASKS, packet) for packet in writes if isinstance(packet, Send)]
|
||||
entries = [write for write in writes if isinstance(write, ChannelWriteEntry)]
|
||||
# process entries into values
|
||||
values = [
|
||||
write.mapper(write.value) if write.mapper is not None else write.value
|
||||
for write in entries
|
||||
]
|
||||
values = [
|
||||
(write.channel, val)
|
||||
for val, write in zip(values, entries)
|
||||
if not write.skip_none or val is not None
|
||||
]
|
||||
# filter out SKIP_WRITE values
|
||||
filtered = [(chan, val) for chan, val in values if val is not SKIP_WRITE]
|
||||
if require_at_least_one_of is not None:
|
||||
if not {chan for chan, _ in filtered} & set(require_at_least_one_of):
|
||||
@@ -158,7 +140,7 @@ class ChannelWrite(RunnableCallable):
|
||||
f"Must write to at least one of {require_at_least_one_of}"
|
||||
)
|
||||
write: TYPE_SEND = config["configurable"][CONFIG_KEY_SEND]
|
||||
write(filtered)
|
||||
write(sends + filtered)
|
||||
|
||||
@staticmethod
|
||||
def is_writer(runnable: Runnable) -> bool:
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
import asyncio
|
||||
import enum
|
||||
import inspect
|
||||
import sys
|
||||
from contextvars import copy_context
|
||||
from functools import partial, wraps
|
||||
from typing import Any, AsyncIterator, Awaitable, Callable, Optional
|
||||
|
||||
from langchain_core.runnables.base import (
|
||||
Runnable,
|
||||
RunnableConfig,
|
||||
RunnableLambda,
|
||||
RunnableLike,
|
||||
RunnableParallel,
|
||||
)
|
||||
from langchain_core.runnables.config import (
|
||||
merge_configs,
|
||||
run_in_executor,
|
||||
var_child_runnable_config,
|
||||
)
|
||||
from langchain_core.runnables.utils import accepts_config
|
||||
from typing_extensions import TypeGuard
|
||||
|
||||
try:
|
||||
from langchain_core.runnables.config import _set_config_context
|
||||
except ImportError:
|
||||
# For forwards compatibility
|
||||
def _set_config_context(context: RunnableConfig) -> None: # type: ignore
|
||||
"""Set the context for the current thread."""
|
||||
var_child_runnable_config.set(context)
|
||||
|
||||
|
||||
# Before Python 3.11 native StrEnum is not available
|
||||
class StrEnum(str, enum.Enum):
|
||||
"""A string enum."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RunnableCallable(Runnable):
|
||||
"""A much simpler version of RunnableLambda that requires sync and async functions."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[..., Optional[Runnable]],
|
||||
afunc: Optional[Callable[..., Awaitable[Optional[Runnable]]]] = None,
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
tags: Optional[list[str]] = None,
|
||||
trace: bool = True,
|
||||
recurse: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
if name is not None:
|
||||
self.name = name
|
||||
elif func:
|
||||
try:
|
||||
if func.__name__ != "<lambda>":
|
||||
self.name = func.__name__
|
||||
except AttributeError:
|
||||
pass
|
||||
elif afunc:
|
||||
try:
|
||||
self.name = afunc.__name__
|
||||
except AttributeError:
|
||||
pass
|
||||
self.func = func
|
||||
self.afunc = afunc
|
||||
self.config: Optional[RunnableConfig] = {"tags": tags} if tags else None
|
||||
self.kwargs = kwargs
|
||||
self.trace = trace
|
||||
self.recurse = recurse
|
||||
|
||||
def __repr__(self) -> str:
|
||||
repr_args = {
|
||||
k: v
|
||||
for k, v in self.__dict__.items()
|
||||
if k not in {"name", "func", "afunc", "config", "kwargs", "trace"}
|
||||
}
|
||||
return f"{self.get_name()}({', '.join(f'{k}={v!r}' for k, v in repr_args.items())})"
|
||||
|
||||
def invoke(
|
||||
self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
) -> Any:
|
||||
if self.func is None:
|
||||
raise TypeError(
|
||||
f'No synchronous function provided to "{self.name}".'
|
||||
"\nEither initialize with a synchronous function or invoke"
|
||||
" via the async API (ainvoke, astream, etc.)"
|
||||
)
|
||||
kwargs = {**self.kwargs, **kwargs}
|
||||
if self.trace:
|
||||
ret = self._call_with_config(
|
||||
self.func, input, merge_configs(self.config, config), **kwargs
|
||||
)
|
||||
else:
|
||||
config = merge_configs(self.config, config)
|
||||
context = copy_context()
|
||||
context.run(_set_config_context, config)
|
||||
if accepts_config(self.func):
|
||||
kwargs["config"] = config
|
||||
ret = context.run(self.func, input, **kwargs)
|
||||
if isinstance(ret, Runnable) and self.recurse:
|
||||
return ret.invoke(input, config)
|
||||
return ret
|
||||
|
||||
async def ainvoke(
|
||||
self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
) -> Any:
|
||||
if not self.afunc:
|
||||
return self.invoke(input, config)
|
||||
kwargs = {**self.kwargs, **kwargs}
|
||||
if self.trace:
|
||||
ret = await self._acall_with_config(
|
||||
self.afunc, input, merge_configs(self.config, config), **kwargs
|
||||
)
|
||||
else:
|
||||
config = merge_configs(self.config, config)
|
||||
context = copy_context()
|
||||
context.run(_set_config_context, config)
|
||||
if accepts_config(self.afunc):
|
||||
kwargs["config"] = config
|
||||
if sys.version_info >= (3, 11):
|
||||
ret = await asyncio.create_task(
|
||||
self.afunc(input, **kwargs), context=context
|
||||
)
|
||||
else:
|
||||
ret = await self.afunc(input, **kwargs)
|
||||
if isinstance(ret, Runnable) and self.recurse:
|
||||
return await ret.ainvoke(input, config)
|
||||
return ret
|
||||
|
||||
|
||||
def is_async_callable(
|
||||
func: Any,
|
||||
) -> TypeGuard[Callable[..., Awaitable]]:
|
||||
"""Check if a function is async."""
|
||||
return (
|
||||
asyncio.iscoroutinefunction(func)
|
||||
or hasattr(func, "__call__")
|
||||
and asyncio.iscoroutinefunction(func.__call__)
|
||||
)
|
||||
|
||||
|
||||
def is_async_generator(
|
||||
func: Any,
|
||||
) -> TypeGuard[Callable[..., AsyncIterator]]:
|
||||
"""Check if a function is an async generator."""
|
||||
return (
|
||||
inspect.isasyncgenfunction(func)
|
||||
or hasattr(func, "__call__")
|
||||
and inspect.isasyncgenfunction(func.__call__)
|
||||
)
|
||||
|
||||
|
||||
def coerce_to_runnable(thing: RunnableLike, *, name: str, trace: bool) -> Runnable:
|
||||
"""Coerce a runnable-like object into a Runnable.
|
||||
|
||||
Args:
|
||||
thing: A runnable-like object.
|
||||
|
||||
Returns:
|
||||
A Runnable.
|
||||
"""
|
||||
if isinstance(thing, Runnable):
|
||||
return thing
|
||||
elif is_async_generator(thing) or inspect.isgeneratorfunction(thing):
|
||||
return RunnableLambda(thing, name=name)
|
||||
elif callable(thing):
|
||||
if is_async_callable(thing):
|
||||
return RunnableCallable(None, thing, name=name, trace=trace)
|
||||
else:
|
||||
return RunnableCallable(
|
||||
thing,
|
||||
wraps(thing)(partial(run_in_executor, None, thing)),
|
||||
name=name,
|
||||
trace=trace,
|
||||
)
|
||||
elif isinstance(thing, dict):
|
||||
return RunnableParallel(thing)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Expected a Runnable, callable or dict."
|
||||
f"Instead got an unsupported type: {type(thing)}"
|
||||
)
|
||||
@@ -0,0 +1,152 @@
|
||||
from typing import Any, Optional
|
||||
|
||||
from langchain_core.callbacks import Callbacks
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langchain_core.runnables.config import COPIABLE_KEYS, DEFAULT_RECURSION_LIMIT
|
||||
|
||||
from langgraph.checkpoint.base import CheckpointMetadata
|
||||
from langgraph.constants import CONFIG_KEY_CHECKPOINT_MAP
|
||||
|
||||
|
||||
def patch_configurable(
|
||||
config: Optional[RunnableConfig], patch: dict[str, Any]
|
||||
) -> RunnableConfig:
|
||||
if config is None:
|
||||
return {"configurable": patch}
|
||||
elif "configurable" not in config:
|
||||
return {**config, "configurable": patch}
|
||||
else:
|
||||
return {**config, "configurable": {**config["configurable"], **patch}}
|
||||
|
||||
|
||||
def patch_checkpoint_map(
|
||||
config: RunnableConfig, metadata: Optional[CheckpointMetadata]
|
||||
) -> RunnableConfig:
|
||||
if parents := (metadata.get("parents") if metadata else None):
|
||||
return patch_configurable(
|
||||
config,
|
||||
{
|
||||
CONFIG_KEY_CHECKPOINT_MAP: {
|
||||
**parents,
|
||||
config["configurable"]["checkpoint_ns"]: config["configurable"][
|
||||
"checkpoint_id"
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
else:
|
||||
return config
|
||||
|
||||
|
||||
def merge_configs(*configs: Optional[RunnableConfig]) -> RunnableConfig:
|
||||
"""Merge multiple configs into one.
|
||||
|
||||
Args:
|
||||
*configs (Optional[RunnableConfig]): The configs to merge.
|
||||
|
||||
Returns:
|
||||
RunnableConfig: The merged config.
|
||||
"""
|
||||
base: RunnableConfig = {}
|
||||
# Even though the keys aren't literals, this is correct
|
||||
# because both dicts are the same type
|
||||
for config in configs:
|
||||
if config is None:
|
||||
continue
|
||||
for key in config:
|
||||
if key == "metadata":
|
||||
base[key] = { # type: ignore
|
||||
**base.get(key, {}), # type: ignore
|
||||
**(config.get(key) or {}), # type: ignore
|
||||
}
|
||||
elif key == "tags":
|
||||
base[key] = sorted( # type: ignore
|
||||
set(base.get(key, []) + (config.get(key) or [])), # type: ignore
|
||||
)
|
||||
elif key == "configurable":
|
||||
base[key] = { # type: ignore
|
||||
**base.get(key, {}), # type: ignore
|
||||
**(config.get(key) or {}), # type: ignore
|
||||
}
|
||||
elif key == "callbacks":
|
||||
base_callbacks = base.get("callbacks")
|
||||
these_callbacks = config["callbacks"]
|
||||
# callbacks can be either None, list[handler] or manager
|
||||
# so merging two callbacks values has 6 cases
|
||||
if isinstance(these_callbacks, list):
|
||||
if base_callbacks is None:
|
||||
base["callbacks"] = these_callbacks.copy()
|
||||
elif isinstance(base_callbacks, list):
|
||||
base["callbacks"] = base_callbacks + these_callbacks
|
||||
else:
|
||||
# base_callbacks is a manager
|
||||
mngr = base_callbacks.copy()
|
||||
for callback in these_callbacks:
|
||||
mngr.add_handler(callback, inherit=True)
|
||||
base["callbacks"] = mngr
|
||||
elif these_callbacks is not None:
|
||||
# these_callbacks is a manager
|
||||
if base_callbacks is None:
|
||||
base["callbacks"] = these_callbacks.copy()
|
||||
elif isinstance(base_callbacks, list):
|
||||
mngr = these_callbacks.copy()
|
||||
for callback in base_callbacks:
|
||||
mngr.add_handler(callback, inherit=True)
|
||||
base["callbacks"] = mngr
|
||||
else:
|
||||
# base_callbacks is also a manager
|
||||
base["callbacks"] = base_callbacks.merge(these_callbacks)
|
||||
elif key == "recursion_limit":
|
||||
if config["recursion_limit"] != DEFAULT_RECURSION_LIMIT:
|
||||
base["recursion_limit"] = config["recursion_limit"]
|
||||
elif key in COPIABLE_KEYS and config[key] is not None: # type: ignore[literal-required]
|
||||
base[key] = config[key].copy() # type: ignore[literal-required]
|
||||
else:
|
||||
base[key] = config[key] or base.get(key) # type: ignore
|
||||
return base
|
||||
|
||||
|
||||
def patch_config(
|
||||
config: Optional[RunnableConfig],
|
||||
*,
|
||||
callbacks: Optional[Callbacks] = None,
|
||||
recursion_limit: Optional[int] = None,
|
||||
max_concurrency: Optional[int] = None,
|
||||
run_name: Optional[str] = None,
|
||||
configurable: Optional[dict[str, Any]] = None,
|
||||
) -> RunnableConfig:
|
||||
"""Patch a config with new values.
|
||||
|
||||
Args:
|
||||
config (Optional[RunnableConfig]): The config to patch.
|
||||
callbacks (Optional[BaseCallbackManager], optional): The callbacks to set.
|
||||
Defaults to None.
|
||||
recursion_limit (Optional[int], optional): The recursion limit to set.
|
||||
Defaults to None.
|
||||
max_concurrency (Optional[int], optional): The max concurrency to set.
|
||||
Defaults to None.
|
||||
run_name (Optional[str], optional): The run name to set. Defaults to None.
|
||||
configurable (Optional[Dict[str, Any]], optional): The configurable to set.
|
||||
Defaults to None.
|
||||
|
||||
Returns:
|
||||
RunnableConfig: The patched config.
|
||||
"""
|
||||
config = config.copy() or {}
|
||||
if callbacks is not None:
|
||||
# If we're replacing callbacks, we need to unset run_name
|
||||
# As that should apply only to the same run as the original callbacks
|
||||
config["callbacks"] = callbacks
|
||||
if "run_name" in config:
|
||||
del config["run_name"]
|
||||
if "run_id" in config:
|
||||
del config["run_id"]
|
||||
if recursion_limit is not None:
|
||||
config["recursion_limit"] = recursion_limit
|
||||
if max_concurrency is not None:
|
||||
config["max_concurrency"] = max_concurrency
|
||||
if run_name is not None:
|
||||
config["run_name"] = run_name
|
||||
if configurable is not None:
|
||||
config["configurable"] = {**config.get("configurable", {}), **configurable}
|
||||
return config
|
||||
@@ -0,0 +1,101 @@
|
||||
from typing import Any, Optional, Type, Union
|
||||
|
||||
from typing_extensions import (
|
||||
Annotated,
|
||||
NotRequired,
|
||||
ReadOnly,
|
||||
Required,
|
||||
get_origin,
|
||||
)
|
||||
|
||||
|
||||
def _is_optional_type(type_: Any) -> bool:
|
||||
"""Check if a type is Optional."""
|
||||
|
||||
if hasattr(type_, "__origin__") and hasattr(type_, "__args__"):
|
||||
origin = get_origin(type_)
|
||||
if origin is Optional:
|
||||
return True
|
||||
if origin is Union:
|
||||
return any(
|
||||
arg is type(None) or _is_optional_type(arg) for arg in type_.__args__
|
||||
)
|
||||
if origin is Annotated:
|
||||
return _is_optional_type(type_.__args__[0])
|
||||
return origin is None
|
||||
if hasattr(type_, "__bound__") and type_.__bound__ is not None:
|
||||
return _is_optional_type(type_.__bound__)
|
||||
return type_ is None
|
||||
|
||||
|
||||
def _is_required_type(type_: Any) -> Optional[bool]:
|
||||
"""Check if an annotation is marked as Required/NotRequired.
|
||||
|
||||
Returns:
|
||||
- True if required
|
||||
- False if not required
|
||||
- None if not annotated with either
|
||||
"""
|
||||
origin = get_origin(type_)
|
||||
if origin is Required:
|
||||
return True
|
||||
if origin is NotRequired:
|
||||
return False
|
||||
if origin is Annotated or getattr(origin, "__args__", None):
|
||||
# See https://typing.readthedocs.io/en/latest/spec/typeddict.html#interaction-with-annotated
|
||||
return _is_required_type(type_.__args__[0])
|
||||
return None
|
||||
|
||||
|
||||
def _is_readonly_type(type_: Any) -> bool:
|
||||
"""Check if an annotation is marked as ReadOnly.
|
||||
|
||||
Returns:
|
||||
- True if is read only
|
||||
- False if not read only
|
||||
"""
|
||||
|
||||
# See: https://typing.readthedocs.io/en/latest/spec/typeddict.html#typing-readonly-type-qualifier
|
||||
origin = get_origin(type_)
|
||||
if origin is Annotated:
|
||||
return _is_readonly_type(type_.__args__[0])
|
||||
if origin is ReadOnly:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
_DEFAULT_KEYS = frozenset()
|
||||
|
||||
|
||||
def get_field_default(name: str, type_: Any, schema: Type[Any]) -> Any:
|
||||
"""Determine the default value for a field in a state schema.
|
||||
|
||||
This is based on:
|
||||
If TypedDict:
|
||||
- Required/NotRequired
|
||||
- total=False -> everything optional
|
||||
- Type annotation (Optional/Union[None])
|
||||
"""
|
||||
optional_keys = getattr(schema, "__optional_keys__", _DEFAULT_KEYS)
|
||||
irq = _is_required_type(type_)
|
||||
if name in optional_keys:
|
||||
# Either total=False or explicit NotRequired.
|
||||
# No type annotation trumps this.
|
||||
if irq:
|
||||
# Unless it's earlier versions of python & explicit Required
|
||||
return ...
|
||||
return None
|
||||
if irq is not None:
|
||||
if irq:
|
||||
# Handle Required[<type>]
|
||||
# (we already handled NotRequired and total=False)
|
||||
return ...
|
||||
# Handle NotRequired[<type>] for earlier versions of python
|
||||
return None
|
||||
# Note, we ignore ReadOnly attributes,
|
||||
# as they don't make much sense. (we don't care if you mutate the state in your node)
|
||||
# and mutating state in your node has no effect on our graph state.
|
||||
# Base case is the annotation
|
||||
if _is_optional_type(type_):
|
||||
return None
|
||||
return ...
|
||||
@@ -0,0 +1,519 @@
|
||||
import asyncio
|
||||
import enum
|
||||
import inspect
|
||||
import sys
|
||||
from contextlib import AsyncExitStack
|
||||
from contextvars import copy_context
|
||||
from functools import partial, wraps
|
||||
from typing import Any, AsyncIterator, Awaitable, Callable, Iterator, Optional
|
||||
|
||||
from langchain_core.runnables.base import (
|
||||
Runnable,
|
||||
RunnableConfig,
|
||||
RunnableLambda,
|
||||
RunnableLike,
|
||||
RunnableParallel,
|
||||
RunnableSequence,
|
||||
)
|
||||
from langchain_core.runnables.config import (
|
||||
ensure_config,
|
||||
get_async_callback_manager_for_config,
|
||||
get_callback_manager_for_config,
|
||||
run_in_executor,
|
||||
var_child_runnable_config,
|
||||
)
|
||||
from langchain_core.runnables.utils import Input, Output, accepts_config
|
||||
from langchain_core.tracers._streaming import _StreamingCallbackHandler
|
||||
from typing_extensions import TypeGuard
|
||||
|
||||
from langgraph.utils.config import merge_configs, patch_config
|
||||
|
||||
try:
|
||||
from langchain_core.runnables.config import _set_config_context
|
||||
except ImportError:
|
||||
# For forwards compatibility
|
||||
def _set_config_context(context: RunnableConfig) -> None: # type: ignore
|
||||
"""Set the context for the current thread."""
|
||||
var_child_runnable_config.set(context)
|
||||
|
||||
|
||||
# Before Python 3.11 native StrEnum is not available
|
||||
class StrEnum(str, enum.Enum):
|
||||
"""A string enum."""
|
||||
|
||||
|
||||
ASYNCIO_ACCEPTS_CONTEXT = sys.version_info >= (3, 11)
|
||||
|
||||
|
||||
class RunnableCallable(Runnable):
|
||||
"""A much simpler version of RunnableLambda that requires sync and async functions."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[..., Optional[Runnable]],
|
||||
afunc: Optional[Callable[..., Awaitable[Optional[Runnable]]]] = None,
|
||||
*,
|
||||
name: Optional[str] = None,
|
||||
tags: Optional[list[str]] = None,
|
||||
trace: bool = True,
|
||||
recurse: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.name = name
|
||||
if self.name is None:
|
||||
if func:
|
||||
try:
|
||||
if func.__name__ != "<lambda>":
|
||||
self.name = func.__name__
|
||||
except AttributeError:
|
||||
pass
|
||||
elif afunc:
|
||||
try:
|
||||
self.name = afunc.__name__
|
||||
except AttributeError:
|
||||
pass
|
||||
self.func = func
|
||||
if func is not None:
|
||||
self.func_accepts_config = accepts_config(func)
|
||||
self.afunc = afunc
|
||||
if afunc is not None:
|
||||
self.afunc_accepts_config = accepts_config(afunc)
|
||||
self.config: Optional[RunnableConfig] = {"tags": tags} if tags else None
|
||||
self.kwargs = kwargs
|
||||
self.trace = trace
|
||||
self.recurse = recurse
|
||||
|
||||
def __repr__(self) -> str:
|
||||
repr_args = {
|
||||
k: v
|
||||
for k, v in self.__dict__.items()
|
||||
if k not in {"name", "func", "afunc", "config", "kwargs", "trace"}
|
||||
}
|
||||
return f"{self.get_name()}({', '.join(f'{k}={v!r}' for k, v in repr_args.items())})"
|
||||
|
||||
def invoke(
|
||||
self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
) -> Any:
|
||||
if self.func is None:
|
||||
raise TypeError(
|
||||
f'No synchronous function provided to "{self.name}".'
|
||||
"\nEither initialize with a synchronous function or invoke"
|
||||
" via the async API (ainvoke, astream, etc.)"
|
||||
)
|
||||
kwargs = {**self.kwargs, **kwargs}
|
||||
if self.func_accepts_config:
|
||||
kwargs["config"] = config
|
||||
config = ensure_config(merge_configs(self.config, config))
|
||||
context = copy_context()
|
||||
if self.trace:
|
||||
config = ensure_config(config)
|
||||
callback_manager = get_callback_manager_for_config(config)
|
||||
run_manager = callback_manager.on_chain_start(
|
||||
None,
|
||||
input,
|
||||
name=config.get("run_name") or self.get_name(),
|
||||
run_id=config.pop("run_id", None),
|
||||
)
|
||||
try:
|
||||
child_config = patch_config(config, callbacks=run_manager.get_child())
|
||||
context = copy_context()
|
||||
context.run(_set_config_context, child_config)
|
||||
ret = context.run(self.func, input, **kwargs)
|
||||
except BaseException as e:
|
||||
run_manager.on_chain_error(e)
|
||||
raise
|
||||
else:
|
||||
run_manager.on_chain_end(ret)
|
||||
else:
|
||||
context.run(_set_config_context, config)
|
||||
ret = context.run(self.func, input, **kwargs)
|
||||
if isinstance(ret, Runnable) and self.recurse:
|
||||
return ret.invoke(input, config)
|
||||
return ret
|
||||
|
||||
async def ainvoke(
|
||||
self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
) -> Any:
|
||||
if not self.afunc:
|
||||
return self.invoke(input, config)
|
||||
kwargs = {**self.kwargs, **kwargs}
|
||||
if self.afunc_accepts_config:
|
||||
kwargs["config"] = config
|
||||
config = ensure_config(merge_configs(self.config, config))
|
||||
context = copy_context()
|
||||
if self.trace:
|
||||
callback_manager = get_async_callback_manager_for_config(config)
|
||||
run_manager = await callback_manager.on_chain_start(
|
||||
None,
|
||||
input,
|
||||
name=config.get("run_name") or self.name,
|
||||
run_id=config.pop("run_id", None),
|
||||
)
|
||||
try:
|
||||
child_config = patch_config(config, callbacks=run_manager.get_child())
|
||||
context.run(_set_config_context, child_config)
|
||||
coro = self.afunc(input, **kwargs)
|
||||
if ASYNCIO_ACCEPTS_CONTEXT:
|
||||
ret = await asyncio.create_task(coro, context=context)
|
||||
else:
|
||||
ret = await coro
|
||||
except BaseException as e:
|
||||
await run_manager.on_chain_error(e)
|
||||
raise
|
||||
else:
|
||||
await run_manager.on_chain_end(ret)
|
||||
else:
|
||||
context.run(_set_config_context, config)
|
||||
if ASYNCIO_ACCEPTS_CONTEXT:
|
||||
ret = await asyncio.create_task(
|
||||
self.afunc(input, **kwargs), context=context
|
||||
)
|
||||
else:
|
||||
ret = await self.afunc(input, **kwargs)
|
||||
if isinstance(ret, Runnable) and self.recurse:
|
||||
return await ret.ainvoke(input, config)
|
||||
return ret
|
||||
|
||||
|
||||
def is_async_callable(
|
||||
func: Any,
|
||||
) -> TypeGuard[Callable[..., Awaitable]]:
|
||||
"""Check if a function is async."""
|
||||
return (
|
||||
asyncio.iscoroutinefunction(func)
|
||||
or hasattr(func, "__call__")
|
||||
and asyncio.iscoroutinefunction(func.__call__)
|
||||
)
|
||||
|
||||
|
||||
def is_async_generator(
|
||||
func: Any,
|
||||
) -> TypeGuard[Callable[..., AsyncIterator]]:
|
||||
"""Check if a function is an async generator."""
|
||||
return (
|
||||
inspect.isasyncgenfunction(func)
|
||||
or hasattr(func, "__call__")
|
||||
and inspect.isasyncgenfunction(func.__call__)
|
||||
)
|
||||
|
||||
|
||||
def coerce_to_runnable(thing: RunnableLike, *, name: str, trace: bool) -> Runnable:
|
||||
"""Coerce a runnable-like object into a Runnable.
|
||||
|
||||
Args:
|
||||
thing: A runnable-like object.
|
||||
|
||||
Returns:
|
||||
A Runnable.
|
||||
"""
|
||||
if isinstance(thing, Runnable):
|
||||
return thing
|
||||
elif is_async_generator(thing) or inspect.isgeneratorfunction(thing):
|
||||
return RunnableLambda(thing, name=name)
|
||||
elif callable(thing):
|
||||
if is_async_callable(thing):
|
||||
return RunnableCallable(None, thing, name=name, trace=trace)
|
||||
else:
|
||||
return RunnableCallable(
|
||||
thing,
|
||||
wraps(thing)(partial(run_in_executor, None, thing)),
|
||||
name=name,
|
||||
trace=trace,
|
||||
)
|
||||
elif isinstance(thing, dict):
|
||||
return RunnableParallel(thing)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Expected a Runnable, callable or dict."
|
||||
f"Instead got an unsupported type: {type(thing)}"
|
||||
)
|
||||
|
||||
|
||||
class RunnableSeq(Runnable):
|
||||
"""A simpler version of RunnableSequence."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*steps: RunnableLike,
|
||||
name: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Create a new RunnableSequence.
|
||||
|
||||
Args:
|
||||
steps: The steps to include in the sequence.
|
||||
name: The name of the Runnable. Defaults to None.
|
||||
first: The first Runnable in the sequence. Defaults to None.
|
||||
middle: The middle Runnables in the sequence. Defaults to None.
|
||||
last: The last Runnable in the sequence. Defaults to None.
|
||||
|
||||
Raises:
|
||||
ValueError: If the sequence has less than 2 steps.
|
||||
"""
|
||||
steps_flat: list[Runnable] = []
|
||||
for step in steps:
|
||||
if isinstance(step, RunnableSequence):
|
||||
steps_flat.extend(step.steps)
|
||||
elif isinstance(step, RunnableSeq):
|
||||
steps_flat.extend(step.steps)
|
||||
else:
|
||||
steps_flat.append(coerce_to_runnable(step, name=None, trace=True))
|
||||
if len(steps_flat) < 2:
|
||||
raise ValueError(
|
||||
f"RunnableSeq must have at least 2 steps, got {len(steps_flat)}"
|
||||
)
|
||||
self.steps = steps_flat
|
||||
self.name = name
|
||||
|
||||
def __or__(
|
||||
self,
|
||||
other: Any,
|
||||
) -> Runnable:
|
||||
if isinstance(other, RunnableSequence):
|
||||
return RunnableSeq(
|
||||
*self.steps,
|
||||
other.first,
|
||||
*other.middle,
|
||||
other.last,
|
||||
name=self.name or other.name,
|
||||
)
|
||||
elif isinstance(other, RunnableSeq):
|
||||
return RunnableSeq(
|
||||
*self.steps,
|
||||
*other.steps,
|
||||
name=self.name or other.name,
|
||||
)
|
||||
else:
|
||||
return RunnableSeq(
|
||||
*self.steps,
|
||||
coerce_to_runnable(other),
|
||||
name=self.name,
|
||||
)
|
||||
|
||||
def __ror__(
|
||||
self,
|
||||
other: Any,
|
||||
) -> Runnable:
|
||||
if isinstance(other, RunnableSequence):
|
||||
return RunnableSequence(
|
||||
other.first,
|
||||
*other.middle,
|
||||
other.last,
|
||||
*self.steps,
|
||||
name=other.name or self.name,
|
||||
)
|
||||
elif isinstance(other, RunnableSeq):
|
||||
return RunnableSeq(
|
||||
*other.steps,
|
||||
*self.steps,
|
||||
name=other.name or self.name,
|
||||
)
|
||||
else:
|
||||
return RunnableSequence(
|
||||
coerce_to_runnable(other),
|
||||
*self.steps,
|
||||
name=self.name,
|
||||
)
|
||||
|
||||
def invoke(
|
||||
self, input: Input, config: Optional[RunnableConfig] = None, **kwargs: Any
|
||||
) -> Output:
|
||||
# setup callbacks and context
|
||||
config = ensure_config(config)
|
||||
callback_manager = get_callback_manager_for_config(config)
|
||||
# start the root run
|
||||
run_manager = callback_manager.on_chain_start(
|
||||
None,
|
||||
input,
|
||||
name=config.get("run_name") or self.get_name(),
|
||||
run_id=config.pop("run_id", None),
|
||||
)
|
||||
|
||||
# invoke all steps in sequence
|
||||
try:
|
||||
for i, step in enumerate(self.steps):
|
||||
# mark each step as a child run
|
||||
config = patch_config(
|
||||
config, callbacks=run_manager.get_child(f"seq:step:{i+1}")
|
||||
)
|
||||
context = copy_context()
|
||||
context.run(_set_config_context, config)
|
||||
if i == 0:
|
||||
input = context.run(step.invoke, input, config, **kwargs)
|
||||
else:
|
||||
input = context.run(step.invoke, input, config)
|
||||
# finish the root run
|
||||
except BaseException as e:
|
||||
run_manager.on_chain_error(e)
|
||||
raise
|
||||
else:
|
||||
run_manager.on_chain_end(input)
|
||||
return input
|
||||
|
||||
async def ainvoke(
|
||||
self,
|
||||
input: Input,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
**kwargs: Optional[Any],
|
||||
) -> Output:
|
||||
# setup callbacks
|
||||
config = ensure_config(config)
|
||||
callback_manager = get_async_callback_manager_for_config(config)
|
||||
# start the root run
|
||||
run_manager = await callback_manager.on_chain_start(
|
||||
None,
|
||||
input,
|
||||
name=config.get("run_name") or self.get_name(),
|
||||
run_id=config.pop("run_id", None),
|
||||
)
|
||||
|
||||
# invoke all steps in sequence
|
||||
try:
|
||||
for i, step in enumerate(self.steps):
|
||||
# mark each step as a child run
|
||||
config = patch_config(
|
||||
config, callbacks=run_manager.get_child(f"seq:step:{i+1}")
|
||||
)
|
||||
context = copy_context()
|
||||
context.run(_set_config_context, config)
|
||||
if i == 0:
|
||||
coro = step.ainvoke(input, config, **kwargs)
|
||||
else:
|
||||
coro = step.ainvoke(input, config)
|
||||
if ASYNCIO_ACCEPTS_CONTEXT:
|
||||
input = await asyncio.create_task(coro, context=context)
|
||||
else:
|
||||
input = await asyncio.create_task(coro)
|
||||
# finish the root run
|
||||
except BaseException as e:
|
||||
await run_manager.on_chain_error(e)
|
||||
raise
|
||||
else:
|
||||
await run_manager.on_chain_end(input)
|
||||
return input
|
||||
|
||||
def stream(
|
||||
self,
|
||||
input: Input,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
**kwargs: Optional[Any],
|
||||
) -> Iterator[Output]:
|
||||
# setup callbacks
|
||||
config = ensure_config(config)
|
||||
callback_manager = get_callback_manager_for_config(config)
|
||||
# start the root run
|
||||
run_manager = callback_manager.on_chain_start(
|
||||
None,
|
||||
input,
|
||||
name=config.get("run_name") or self.get_name(),
|
||||
run_id=config.pop("run_id", None),
|
||||
)
|
||||
|
||||
try:
|
||||
# stream the last steps
|
||||
# transform the input stream of each step with the next
|
||||
# steps that don't natively support transforming an input stream will
|
||||
# buffer input in memory until all available, and then start emitting output
|
||||
for idx, step in enumerate(self.steps):
|
||||
config = patch_config(
|
||||
config,
|
||||
callbacks=run_manager.get_child(f"seq:step:{idx+1}"),
|
||||
)
|
||||
if idx == 0:
|
||||
iterator = step.stream(input, config, **kwargs)
|
||||
else:
|
||||
iterator = step.transform(iterator, config)
|
||||
if stream_handler := next(
|
||||
(
|
||||
h
|
||||
for h in run_manager.handlers
|
||||
if isinstance(h, _StreamingCallbackHandler)
|
||||
),
|
||||
None,
|
||||
):
|
||||
# populates streamed_output in astream_log() output if needed
|
||||
iterator = stream_handler.tap_output_iter(run_manager.run_id, iterator)
|
||||
output: Output = None
|
||||
add_supported = False
|
||||
for chunk in iterator:
|
||||
yield chunk
|
||||
# collect final output
|
||||
if output is None:
|
||||
output = chunk
|
||||
elif add_supported:
|
||||
try:
|
||||
output = output + chunk
|
||||
except TypeError:
|
||||
output = chunk
|
||||
add_supported = False
|
||||
else:
|
||||
output = chunk
|
||||
except BaseException as e:
|
||||
run_manager.on_chain_error(e)
|
||||
raise
|
||||
else:
|
||||
run_manager.on_chain_end(output)
|
||||
|
||||
async def astream(
|
||||
self,
|
||||
input: Input,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
**kwargs: Optional[Any],
|
||||
) -> AsyncIterator[Output]:
|
||||
# setup callbacks
|
||||
config = ensure_config(config)
|
||||
callback_manager = get_async_callback_manager_for_config(config)
|
||||
# start the root run
|
||||
run_manager = await callback_manager.on_chain_start(
|
||||
None,
|
||||
input,
|
||||
name=config.get("run_name") or self.get_name(),
|
||||
run_id=config.pop("run_id", None),
|
||||
)
|
||||
|
||||
try:
|
||||
async with AsyncExitStack() as stack:
|
||||
# stream the last steps
|
||||
# transform the input stream of each step with the next
|
||||
# steps that don't natively support transforming an input stream will
|
||||
# buffer input in memory until all available, and then start emitting output
|
||||
for idx, step in enumerate(self.steps):
|
||||
config = patch_config(
|
||||
config,
|
||||
callbacks=run_manager.get_child(f"seq:step:{idx+1}"),
|
||||
)
|
||||
if idx == 0:
|
||||
aiterator = step.astream(input, config, **kwargs)
|
||||
else:
|
||||
aiterator = step.atransform(aiterator, config)
|
||||
if hasattr(aiterator, "aclose"):
|
||||
stack.push_async_callback(aiterator.aclose)
|
||||
if stream_handler := next(
|
||||
(
|
||||
h
|
||||
for h in run_manager.handlers
|
||||
if isinstance(h, _StreamingCallbackHandler)
|
||||
),
|
||||
None,
|
||||
):
|
||||
# populates streamed_output in astream_log() output if needed
|
||||
aiterator = stream_handler.tap_output_aiter(
|
||||
run_manager.run_id, aiterator
|
||||
)
|
||||
output: Output = None
|
||||
add_supported = False
|
||||
async for chunk in aiterator:
|
||||
yield chunk
|
||||
# collect final output
|
||||
if add_supported:
|
||||
try:
|
||||
output = output + chunk
|
||||
except TypeError:
|
||||
output = chunk
|
||||
add_supported = False
|
||||
else:
|
||||
output = chunk
|
||||
except BaseException as e:
|
||||
await run_manager.on_chain_error(e)
|
||||
raise
|
||||
else:
|
||||
await run_manager.on_chain_end(output)
|
||||
Generated
+25
-1264
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph"
|
||||
version = "0.2.15"
|
||||
version = "0.2.17"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -9,27 +9,22 @@ repository = "https://www.github.com/langchain-ai/langgraph"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9.0,<4.0"
|
||||
langchain-core = ">=0.2.27,<0.3"
|
||||
langchain-core = ">=0.2.38,<0.4"
|
||||
langgraph-checkpoint = "^1.0.2"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^7.3.0"
|
||||
pytest = "^8.3.2"
|
||||
pytest-cov = "^4.0.0"
|
||||
pytest-dotenv = "^0.5.2"
|
||||
pytest-asyncio = "^0.20.3"
|
||||
pytest-mock = "^3.10.0"
|
||||
syrupy = "^4.0.2"
|
||||
httpx = "^0.26.0"
|
||||
pytest-watcher = "^0.4.1"
|
||||
langchain = ">=0.1.0"
|
||||
grandalf = "^0.8"
|
||||
mypy = "^1.6.0"
|
||||
ruff = "^0.6.2"
|
||||
jupyter = "^1.0.0"
|
||||
langchainhub = "^0.1.14"
|
||||
langchain-openai = ">=0.1.2"
|
||||
langchain-anthropic = ">=0.1.8"
|
||||
pytest-xdist = {extras = ["psutil"], version = "^3.6.1"}
|
||||
pytest-repeat = "^0.9.3"
|
||||
langgraph-checkpoint = {path = "../checkpoint", develop = true}
|
||||
@@ -37,9 +32,6 @@ langgraph-checkpoint-sqlite = {path = "../checkpoint-sqlite", develop = true}
|
||||
langgraph-checkpoint-postgres = {path = "../checkpoint-postgres", develop = true}
|
||||
psycopg = {extras = ["binary"], version = ">=3.0.0"}
|
||||
|
||||
[tool.poetry.group.dev]
|
||||
optional = true
|
||||
|
||||
[tool.ruff]
|
||||
lint.select = [ "E", "F", "I" ]
|
||||
lint.ignore = [ "E501" ]
|
||||
@@ -73,7 +65,6 @@ requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
# --strict-markers will raise errors on unknown marks.
|
||||
# https://docs.pytest.org/en/7.1.x/how-to/mark.html#raising-errors-on-unknown-marks
|
||||
#
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user