Compare commits

..
Author SHA1 Message Date
Vadym BardaandGitHub 46b6cd45e2 checkpoint: release 1.0.9 (#1628) 2024-09-05 17:24:18 -04:00
Vadym BardaandGitHub 8c05502e97 langgraph: update mermaid snapshots (#1627) 2024-09-05 17:11:05 -04:00
Nuno Campos 72e73e47c9 langgraph 0.2.17 2024-09-05 12:37:17 -07:00
Vadym BardaandGitHub 7bf99a5d2f langgraph: changes for compatibility pydantic v2 / langchain-core==0.3 (#1594) 2024-09-05 15:26:35 -04:00
Nuno CamposandGitHub 7461978b8c Merge pull request #1612 from langchain-ai/nc/4sep/prepare-single-task
Split out prepare_single_task from prepare_next_tasks
2024-09-05 10:21:27 -07:00
Nuno Campos 4f89cfc81d Remove legacy id 2024-09-05 09:21:07 -07:00
Isaac FranciscoandGitHub ea5cb4d100 remove self hosted docs (#1621) 2024-09-05 16:16:32 +00:00
e14c17f6b8 docs: add a how-to on subgraph transform state (#1595)
* docs: add a how-to on subgraph transform state

---------

Co-authored-by: isaac hershenson <ihershenson@hmc.edu>
Co-authored-by: Isaac Francisco <78627776+isahers1@users.noreply.github.com>
2024-09-05 12:16:11 -04:00
Isaac FranciscoandGitHub b34bfe165a docs fixes (#1620) 2024-09-05 12:03:55 -04:00
Nuno Campos ca3715b809 Split out prepare_single_task from prepare_next_tasks 2024-09-04 18:09:42 -07:00
gbaian10andGitHub 7e83cda0b5 docs: Correct the reference repo in the Cloud - Setup App (pyproject.toml) documents (#1607) 2024-09-04 21:05:10 -04:00
Nuno CamposandGitHub 6727277f8f Merge pull request #1610 from langchain-ai/an/04sept/metadata-interrupts
Return `before,after` for `__interrupt` metadata if both interrupts are configured for a node
2024-09-04 17:29:07 -07:00
Andrew Nguonly d89fb9eef0 Add before and after values to __interrupt metadata. 2024-09-04 15:36:05 -07:00
Isaac FranciscoandGitHub 1774dbb860 [docs]: adding js/curl tabs (#1558)
* jsifying

* x

* z

* x

* fix background run output

* reformatting

* z

* z

* z

* z

* z

* add tabs to stream debug

* default urls
2024-09-04 22:13:19 +00:00
Harrison ChaseandGitHub 82c989feea docs: subgraphs manage state (#1543) 2024-09-04 17:57:06 -04:00
Nuno Campos f66493f6db Lint 2024-09-04 14:37:31 -07:00
Nuno Campos eb88dfefa3 Speed improvements to local_read, emit output 2024-09-04 14:31:42 -07:00
Nuno Campos b8d8a879d8 Fix ids in debug task events 2024-09-04 14:31:20 -07:00
Nuno Campos dc10b3aa8e Add test to confirm only one root run is produced 2024-09-04 12:56:32 -07:00
Nuno CamposandGitHub af780f97a4 Merge pull request #1597 from langchain-ai/nc/3sep/perf-optimizations
Reduce cpu time spent on langchain-core utilities
2024-09-04 12:45:20 -07:00
Nuno Campos 3a1b90c882 Fix 2024-09-04 12:34:42 -07:00
Nuno Campos b9361d69b4 Try to fix test 2024-09-04 12:30:27 -07:00
Nuno Campos 26b4d9139e Avoid using json encoding when generating task ids 2024-09-04 12:19:14 -07:00
Nuno Campos 67a164ca2d Run loop tick in bg thread
- prepare_next_tasks, apply_writes do some cpu-heavy work
2024-09-04 12:18:42 -07:00
Nuno Campos b68f9211c2 Run branch.reader in bg thread 2024-09-04 12:13:38 -07:00
Nuno Campos 4efab93e2f Use pseudo rng for uuid6 generation 2024-09-04 12:07:19 -07:00
Nuno Campos cf345ef716 Store loop in async bg exec init 2024-09-04 12:07:02 -07:00
Nuno Campos 535dbd5b06 Reduce from 9s to 4s on benchmark graph
- Use a simpler version of RunnableSequence without tracing serialization
- Remove accepts_run_manager check in RunnableCallable
- Remove creation of ChannelWrite dynamically every time conditional edge runs
2024-09-04 11:02:33 -07:00
Nuno Campos f2e0dc1042 Reduce cpu time spent on langchain-core utilities
- shaves off 2s of 11s runtime of a simple graph with 1,000 subgraphs
2024-09-04 11:02:33 -07:00
Nuno CamposandGitHub 6bf367300b Merge pull request #1598 from langchain-ai/nc/3sep/runner
Move runner code to standalone class, use in Pregel.stream/astream
2024-09-04 11:01:38 -07:00
Nuno Campos e42e1a9f6b Fix test 2024-09-04 10:56:41 -07:00
Nuno Campos 0d51c8614d Move runner code to standalone class, use in Pregel.stream/astream 2024-09-04 10:50:46 -07:00
Nuno Campos b99a101b2c Add large parallel graph test 2024-09-04 10:50:05 -07:00
Nuno Campos e150193491 Avoid deepcopy 2024-09-04 10:50:05 -07:00
Nuno CamposandGitHub 7304ba683e Merge pull request #1282 from langchain-ai/isaac/nodelogging
add task_id to node logs
2024-09-03 16:30:43 -07:00
isaac hershenson ec51760f57 x 2024-09-03 13:45:22 -07:00
Isaac FranciscoandGitHub 32d07df7a4 Merge branch 'main' into isaac/nodelogging 2024-09-03 13:39:05 -07:00
Isaac FranciscoandGitHub 1ae60dccc5 test (#1592) 2024-09-03 10:12:36 -07:00
Nuno Campos 5d1ab66535 sqlite 1.0.3 2024-09-03 09:41:04 -07:00
Nuno CamposandGitHub b4944fa0a0 Merge pull request #1582 from langchain-ai/nc/2sep/rm-pydantic-base-class
Remove pydantic base class from Pregel/PregelNode
2024-09-03 09:39:03 -07:00
Nuno Campos 4ddb4ae5a0 Update 2024-09-03 09:26:45 -07:00
Nuno Campos a99b66ae3f Update 2024-09-03 09:20:17 -07:00
Nuno CamposandNuno Campos 4793b3f5e1 Use a lock for all operations on sqlite checkpointer
- Otherwise when used in multiple subgraphs in parallel separate queries can interfere w each other
2024-09-03 09:20:17 -07:00
Nuno CamposandNuno Campos 3c5913aa29 Remove pydantic base class from Pregel/PregelNode 2024-09-03 09:20:17 -07:00
Sreenivasan R SandGitHub 43d5dbf174 examples: fix missing import introduction (#1584) 2024-09-03 12:12:31 -04:00
Nuno CamposandGitHub 974b6c7bf3 Merge pull request #1549 from langchain-ai/wfh/optional_types
Support optional types from typeddict
2024-09-03 09:11:14 -07:00
Vadym BardaandGitHub d492475c61 checkpoint-sqlite: close connection in from_conn_string (#1589) 2024-09-03 11:00:37 -04:00
gbaian10andGitHub e5b09b8c20 docs: update Postgres checkpointer for persistence (#1556) 2024-09-03 11:00:14 -04:00
Jacob LeeandGitHub c1d7cbd67c Adds JS deploy guide to index (#1590) 2024-09-03 07:47:31 -07:00
gbaian10andGitHub 8c0b27a85b docs: fix Corrective RAG (CRAG) miss function (#1563)
`Corrective RAG (CRAG) using local LLMs` miss `find_tool_calls_react` function
2024-09-03 09:50:00 -04:00
322d6c8dde patch(docs): Update docs with JS cloud instructions (#1560)
* Update docs with JS cloud instructions

* Add to index

* Add md fence

* nits

* nits

* Update setup_javascript.md

---------

Co-authored-by: isaac hershenson <ihershenson@hmc.edu>
2024-09-02 18:48:50 -07:00
Nuno Campos af666a87fb sqlite 1.0.2 2024-09-02 17:58:01 -07:00
Nuno Campos 6e9f54e5ae postgres 1.0.6 2024-09-02 17:57:55 -07:00
Nuno Campos dd4a15b569 Require latest version of langgraph-checkpoint 2024-09-02 17:57:33 -07:00
Nuno CamposandGitHub 1122e5cbbb Merge pull request #1579 from langchain-ai/nc/2sep/async-checkpoint-test-setup
test: Ensure that async checkpointers in tests are setup in same event loop as they are used in
2024-09-02 17:57:05 -07:00
Nuno CamposandGitHub 29311f054d Merge pull request #1580 from langchain-ai/nc/2sep/async-checkpointers-implement-async
checkpoint: Implement sync methods in async checkpointers
2024-09-02 17:51:52 -07:00
Nuno Campos 3b2a237deb Lint 2024-09-02 17:45:57 -07:00
Nuno Campos 0cc3799298 checkpoint: Implement sync methods in async checkpointers
- These are needed when subgraphs are invoked synchronously in nodes
2024-09-02 17:43:34 -07:00
Nuno Campos ddeb9d7314 Disable test in old versions of py 2024-09-02 17:42:37 -07:00
Nuno Campos d65d0ec665 test: Ensure that async checkpointers in tests are setup in same event loop as they are used in
- This more closely remembers the environment they're used in, so it's what we should be testing
- Remove unnecessary pytest-asyncio dependency, use anyio pytest plugin instead
- Convert remaining async tests using only memory checkpointer to use all existing ones
2024-09-02 17:24:54 -07:00
Nuno CamposandGitHub a937754132 lib 0.2.16 2024-09-01 12:15:20 -07:00
Reuben MandGitHub 8c698fc362 Return replan response in dict. Add Replan example (#1566) 2024-08-31 23:41:48 +00:00
gbaian10andGitHub 3a07fe2efb docs: fix agent_supervisor.ipynb (#1562)
fix #1561
2024-08-31 16:35:59 -07:00
William Fu-Hinthorn 1191dc1b48 Handle py3.9 2024-08-30 23:52:41 -07:00
isaac hershenson ee5801511b fmt 2024-08-30 18:25:02 -07:00
isaac hershenson d14477c891 x 2024-08-30 18:22:22 -07:00
isaac hershenson 01765fbda4 x 2024-08-30 17:39:45 -07:00
Isaac FranciscoandGitHub 54bcd490ef Merge branch 'main' into isaac/nodelogging 2024-08-30 16:58:31 -07:00
William Fu-Hinthorn 85e827ae85 Merge branch 'main' into wfh/optional_types 2024-08-30 16:40:56 -07:00
William Fu-Hinthorn 55263c640f Return a default value instead 2024-08-30 16:28:01 -07:00
William Fu-Hinthorn 51f58279ba Update snapshot 2024-08-30 16:20:22 -07:00
Nuno CamposandGitHub 7d6a302858 Merge pull request #1514 from vermapratyush/patch-1
[docs]: Fix typo
2024-08-30 16:20:18 -07:00
Nuno CamposandGitHub d225ef074f Merge pull request #1487 from langchain-ai/isaac/fixcustomersupportbot
customer support bot doc nit
2024-08-30 16:14:50 -07:00
William Fu-Hinthorn a07ec9d8c8 Handle total=false 2024-08-30 10:21:57 -07:00
William Fu-Hinthorn 65b64c1331 Support optional types from typeddict
In the input schema
2024-08-30 09:42:53 -07:00
Pratyush VermaandGitHub 8e6019a6b8 Merge branch 'main' into patch-1 2024-08-28 15:46:39 +01:00
Pratyush VermaandGitHub 666fb3f31d Fix typo 2024-08-28 15:45:02 +01:00
isaac hershenson 86c047eb31 changes 2024-08-26 18:05:17 -07:00
isaac hershenson eee91d2129 wip 2024-08-08 15:29:40 -07:00
113 changed files with 12959 additions and 8747 deletions
+10 -2
View File
@@ -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
+2
View File
@@ -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",
+3 -3
View File
@@ -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
-35
View File
@@ -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
+6 -2
View File
@@ -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();
```
+13 -5
View File
@@ -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:
+443
View File
@@ -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}]}}
+12 -12
View File
@@ -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
+184
View File
@@ -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>
```
+87 -21
View File
@@ -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:
================================ Human Message =================================
================================ Human Message =================================
what's the weather in sf?
================================== Ai Message ==================================
================================== 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
================================= Tool Message =================================
================================= 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."}]
================================== Ai Message ==================================
================================== 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.
================================ Human Message =================================
================================ Human Message =================================
what's the weather in nyc?
================================== Ai Message ==================================
================================== 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
================================= Tool Message =================================
================================= 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}}"}]
================================== Ai Message ==================================
================================== 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);
+7 -7
View File
@@ -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)
+86 -20
View File
@@ -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:
================================ Human Message =================================
================================ Human Message =================================
what's the weather in sf?
================================== Ai Message ==================================
================================== 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
================================= Tool Message =================================
================================= 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 ..."}]
================================ Human Message =================================
================================ Human Message =================================
what's the weather in nyc?
================================== Ai Message ==================================
================================== 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
================================= Tool Message =================================
================================= 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."}]
================================== Ai Message ==================================
================================== 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:
+85 -17
View File
@@ -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:
================================ Human Message =================================
================================ Human Message =================================
what's the weather in sf?
================================== Ai Message ==================================
================================== 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
================================= Tool Message =================================
================================= 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."}]
================================== Ai Message ==================================
================================== Ai Message ==================================
According to the search results from Tavily, the current weather in San Francisco is:
+83 -17
View File
@@ -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:
================================ Human Message =================================
================================ Human Message =================================
what's the weather in nyc?
================================== Ai Message ==================================
================================== 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
================================= Tool Message =================================
================================= 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}}"}]
================================== Ai Message ==================================
================================== 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.
+310
View File
@@ -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}}]}}
+180
View File
@@ -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
}
]
}
+71 -17
View File
@@ -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...
+27 -23
View File
@@ -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"
+24 -16
View File
@@ -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}`);
+23 -17
View File
@@ -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
+19 -15
View File
@@ -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");
}
```
+14 -11
View File
@@ -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");
}
```
+125
View File
@@ -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
View File
@@ -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}]}}
+6 -1
View File
@@ -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
View File
@@ -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
}
-132
View File
@@ -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

-192
View File
@@ -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
}
-72
View File
@@ -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": [
"![Webhook response](./img/webhook_results.png)"
]
}
],
"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",
-7
View File
@@ -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,
+2 -5
View File
@@ -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",
+42 -1
View File
@@ -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",
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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)"
]
},
{
+5 -9
View File
@@ -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",
+1 -1
View File
@@ -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",
+8
View File
@@ -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",
+1 -1
View File
@@ -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",
-7
View File
@@ -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": {
+277
View File
@@ -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
View File
@@ -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",
"![Screenshot 2024-07-23 at 12.39.27 PM.png](attachment:5e0fb308-080e-4621-ba20-55a655bb981e.png)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8eb46884-08aa-4ab4-8bb9-f72277c3b35b",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
@@ -1266,14 +1266,6 @@
"\n",
"![Screenshot 2024-06-23 at 1.32.31 PM.png](attachment:953411a8-f352-4c8f-a923-d3ff171c6080.png)"
]
},
{
"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()
+3 -4
View File
@@ -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"
+2 -2
View File
@@ -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 (?, ?, ?, ?, ?, ?, ?, ?)",
[
+3 -4
View File
@@ -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"
+2 -2
View File
@@ -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
+6 -5
View File
@@ -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"
+2 -2
View File
@@ -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"
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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")
+1 -1
View File
@@ -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
+2
View File
@@ -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,
+24 -11
View File
@@ -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"
+27 -33
View File
@@ -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"],
+104 -289
View File
@@ -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}")
+224 -150
View File
@@ -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]}"
-34
View File
@@ -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
+16 -32
View File
@@ -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(
+3 -2
View File
@@ -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
+6 -5
View File
@@ -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)
+80 -45
View File
@@ -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()
+4 -3
View File
@@ -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():
+95 -51
View File
@@ -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
+193
View File
@@ -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")
+39 -57
View File
@@ -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:
-185
View File
@@ -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)}"
)
+152
View File
@@ -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
+101
View File
@@ -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 ...
+519
View File
@@ -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)
+25 -1264
View File
File diff suppressed because it is too large Load Diff
+3 -12
View File
@@ -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