mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-18 05:35:43 +02:00
Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91bca2ea1d | ||
|
|
bd206c2fcd | ||
|
|
14ec895046 | ||
|
|
fb66736ccb | ||
|
|
c3544024b9 | ||
|
|
b0d1234737 | ||
|
|
53e1a238db | ||
|
|
fcdeafd0d1 | ||
|
|
28c529feb2 | ||
|
|
91ebc8d3ed | ||
|
|
6e08f4c12e | ||
|
|
f2dc0653f1 | ||
|
|
67177a5610 | ||
|
|
b1b238c7ea | ||
|
|
598796ef86 | ||
|
|
3c7981201e | ||
|
|
109c0dfb93 | ||
|
|
d7c364c5bb | ||
|
|
746142fb07 | ||
|
|
b9c9c32c31 | ||
|
|
c89fe4c45d | ||
|
|
b0e28851a6 | ||
|
|
48fb91deda | ||
|
|
efb282a197 | ||
|
|
89ce6ea2a8 | ||
|
|
1e87312d1f |
@@ -13,7 +13,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.ref == 'refs/heads/main'
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/v0'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
|
||||
@@ -4,9 +4,11 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- v0
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- v0
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -82,9 +84,9 @@ jobs:
|
||||
run: make llms-text
|
||||
- name: Build site
|
||||
run: |
|
||||
# If this is main branch, then we want to download stats. we do this
|
||||
# If this is v0 branch, then we want to download stats. we do this
|
||||
# with the env variable DOWNLOAD_STATS=true
|
||||
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
|
||||
if [ "${{ github.ref }}" == "refs/heads/v0" ]; then
|
||||
DOWNLOAD_STATS=true make build-docs
|
||||
else
|
||||
make build-docs
|
||||
@@ -144,8 +146,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Configure GitHub Pages
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: actions/configure-pages@v4
|
||||
if: github.ref == 'refs/heads/v0'
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Upload Pages Artifact
|
||||
# if: github.ref == 'refs/heads/main'
|
||||
@@ -154,6 +156,6 @@ jobs:
|
||||
path: ./docs/site/
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
if: github.ref == 'refs/heads/main'
|
||||
if: github.ref == 'refs/heads/v0'
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
@@ -13,7 +13,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.ref == 'refs/heads/main'
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/v0'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
"""mkdocs hooks for adding custom logic to documentation pipeline.
|
||||
|
||||
Lifecycle events: https://www.mkdocs.org/dev-guide/plugins/#events
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
from typing import Any, Dict
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from mkdocs.config.defaults import MkDocsConfig
|
||||
from mkdocs.structure.files import Files, File
|
||||
from mkdocs.structure.pages import Page
|
||||
|
||||
@@ -101,8 +108,7 @@ REDIRECT_MAP = {
|
||||
"how-tos/deploy-self-hosted.md": "cloud/deployment/self_hosted_data_plane.md",
|
||||
"concepts/self_hosted.md": "concepts/langgraph_self_hosted_data_plane.md",
|
||||
# assistant redirects
|
||||
"cloud/how-tos/assistant_versioning.md": "cloud/how-tos/configuration_cloud.md"
|
||||
|
||||
"cloud/how-tos/assistant_versioning.md": "cloud/how-tos/configuration_cloud.md",
|
||||
}
|
||||
|
||||
|
||||
@@ -292,7 +298,7 @@ Redirecting...
|
||||
"""
|
||||
|
||||
|
||||
def write_html(site_dir, old_path, new_path):
|
||||
def _write_html(site_dir, old_path, new_path):
|
||||
"""Write an HTML file in the site_dir with a meta redirect to the new page"""
|
||||
# Determine all relevant paths
|
||||
old_path_abs = os.path.join(site_dir, old_path)
|
||||
@@ -308,6 +314,52 @@ def write_html(site_dir, old_path, new_path):
|
||||
f.write(content)
|
||||
|
||||
|
||||
def _inject_gtm(html: str) -> str:
|
||||
"""Inject Google Tag Manager code into the HTML.
|
||||
|
||||
Code to inject Google Tag Manager noscript tag immediately after <body>.
|
||||
|
||||
This is done via hooks rather than via a template because the MkDocs material
|
||||
theme does not seem to allow placing the code immediately after the <body> tag
|
||||
without modifying the template files directly.
|
||||
|
||||
Args:
|
||||
html: The HTML content to modify.
|
||||
|
||||
Returns:
|
||||
The modified HTML content with GTM code injected.
|
||||
"""
|
||||
# Code was copied from Google Tag Manager setup instructions.
|
||||
gtm_code = """
|
||||
<!-- Google Tag Manager (noscript) -->
|
||||
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-T35S4S46"
|
||||
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
||||
<!-- End Google Tag Manager (noscript) -->
|
||||
"""
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
body = soup.body
|
||||
if body:
|
||||
# Insert the GTM code as raw HTML at the top of <body>
|
||||
body.insert(0, BeautifulSoup(gtm_code, "html.parser"))
|
||||
return str(soup)
|
||||
else:
|
||||
return html # fallback if no <body> found
|
||||
|
||||
|
||||
def on_post_page(output: str, page: Page, config: MkDocsConfig) -> str:
|
||||
"""Inject Google Tag Manager noscript tag immediately after <body>.
|
||||
|
||||
Args:
|
||||
output: The HTML output of the page.
|
||||
page: The page instance.
|
||||
config: The MkDocs configuration object.
|
||||
|
||||
Returns:
|
||||
modified HTML output with GTM code injected.
|
||||
"""
|
||||
return _inject_gtm(output)
|
||||
|
||||
|
||||
# Create HTML files for redirects after site dir has been built
|
||||
def on_post_build(config):
|
||||
use_directory_urls = config.get("use_directory_urls")
|
||||
@@ -324,4 +376,4 @@ def on_post_build(config):
|
||||
+ hash
|
||||
+ suffix
|
||||
)
|
||||
write_html(config["site_dir"], old_html_path, new_html_path)
|
||||
_write_html(config["site_dir"], old_html_path, new_html_path)
|
||||
|
||||
@@ -62,6 +62,15 @@ Starting from the `LangGraph Platform` view...
|
||||
1. In the panel, select the `Server` tab to view server logs for the revision. Server logs are only available after a revision has been deployed.
|
||||
1. Within the `Server` tab, adjust the date/time range picker as needed. By default, the date/time range picker is set to the `Last 7 days`.
|
||||
|
||||
## View Deployment Metrics
|
||||
|
||||
Starting from the <a href="https://smith.langchain.com/" target="_blank">LangSmith UI</a>...
|
||||
|
||||
1. In the left-hand navigation panel, select `LangGraph Platform`. The `LangGraph Platform` view contains a list of existing LangGraph Platform deployments.
|
||||
1. Select an existing deployment to monitor.
|
||||
1. Select the `Monitoring` tab to view the deployment metrics. See a list of [all available metrics](../../concepts/langgraph_control_plane.md#monitoring).
|
||||
1. Within the `Monitoring` tab, use the date/time range picker as needed. By default, the date/time range picker is set to the `Last 15 minutes`.
|
||||
|
||||
## Interrupt Revision
|
||||
|
||||
Interrupting a revision will stop deployment of the revision.
|
||||
|
||||
@@ -30,18 +30,17 @@ Before deploying, review the [conceptual guide for the Self-Hosted Control Plane
|
||||
1. `LangGraphPlatform CRD`: A CRD for LangGraph Platform deployments. This contains the spec for managing an instance of a LangGraph platform deployment.
|
||||
1. `operator`: This operator handles changes to your LangGraph Platform CRDs.
|
||||
1. `host-backend`: This is the [control plane](../../concepts/langgraph_control_plane.md).
|
||||
1. Two additional images will be used by the chart.
|
||||
1. Two additional images will be used by the chart. Use the images that are specified in the latest release.
|
||||
|
||||
hostBackendImage:
|
||||
repository: "docker.io/langchain/hosted-langserve-backend"
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "0.9.80"
|
||||
operatorImage:
|
||||
repository: "docker.io/langchain/langgraph-operator"
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "aa9dff4"
|
||||
|
||||
1. In your `values.yaml` file, enable the `langgraphPlatform` option. Note that you must also have a valid ingress setup:
|
||||
1. In your config file for langsmith (usually `langsmith_config.yaml`, enable the `langgraphPlatform` option. Note that you must also have a valid ingress setup:
|
||||
|
||||
config:
|
||||
langgraphPlatform:
|
||||
enabled: true
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
!!! info "Prerequisites"
|
||||
|
||||
- [Assistants Overview](../../concepts/assistants.md)
|
||||
- [Assistants Overview](../../../concepts/assistants.md)
|
||||
|
||||
LangGraph Studio lets you view, edit, and update your assistants, and allows you to run your graph using these assistant configurations.
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Run experiments over a dataset
|
||||
|
||||
LangGraph Studio supports evaluations by allowing you to run your assistant over a pre-defined LangSmith dataset. This enables you to understand how your application performs over a variety of inputs, compare the results to reference outputs, and score the results using [evaluators](../../../agents/evals.md).
|
||||
|
||||
This guide shows you how to run an experiment end-to-end from Studio.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before running an experiment, ensure you have the following:
|
||||
|
||||
1. **A LangSmith dataset**: Your dataset should contain the inputs you want to test and optionally, reference outputs for comparison.
|
||||
|
||||
- The schema for the inputs must match the required input schema for the assistant. For more information on schemas, see [here](../../../concepts/low_level.md#schema).
|
||||
- For more on creating datasets, see [How to Manage Datasets](https://docs.smith.langchain.com/evaluation/how_to_guides/manage_datasets_in_application#set-up-your-dataset).
|
||||
|
||||
2. **(Optional) Evaluators**: You can attach evaluators (e.g., LLM-as-a-Judge, heuristics, or custom functions) to your dataset in LangSmith. These will run automatically after the graph has processed all inputs.
|
||||
|
||||
- To learn more, read about [Evaluation Concepts](https://docs.smith.langchain.com/evaluation/concepts#evaluators).
|
||||
|
||||
3. **A running application**: The experiment can be run against:
|
||||
- An application deployed on [LangGraph Platform](../../quick_start.md).
|
||||
- A locally running application started via the [langgraph-cli](../../../tutorials/langgraph-platform/local-server.md).
|
||||
|
||||
---
|
||||
|
||||
## Step-by-step guide
|
||||
|
||||
### 1. Launch the experiment
|
||||
|
||||
Click the **Run experiment** button in the top right corner of the Studio page.
|
||||
|
||||
### 2. Select your dataset
|
||||
|
||||
In the modal that appears, select the dataset (or a specific dataset split) to use for the experiment and click **Start**.
|
||||
|
||||
### 3. Monitor the progress
|
||||
|
||||
All of the inputs in the dataset will now be run against the active assistant. Monitor the experiment's progress via the badge in the top right corner.
|
||||
|
||||
You can continue to work in Studio while the experiment runs in the background. Click the arrow icon button at any time to navigate to LangSmith and view the detailed experiment results.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Run experiment" button is disabled
|
||||
|
||||
If the "Run experiment" button is disabled, check the following:
|
||||
|
||||
- **Deployed application**: If your application is deployed on LangGraph Platform, you may need to create a new revision to enable this feature.
|
||||
- **Local development server**: If you are running your application locally, make sure you have upgraded to the latest version of the `langgraph-cli` (`pip install -U langgraph-cli`). Additionally, ensure you have tracing enabled by setting the `LANGSMITH_API_KEY` in your project's `.env` file.
|
||||
|
||||
### Evaluator results are missing
|
||||
|
||||
When you run an experiment, any attached evaluators are scheduled for execution in a queue. If you don't see results immediately, it likely means they are still pending.
|
||||
@@ -43,6 +43,7 @@ The LangGraph CLI requires a JSON configuration file that follows this [schema](
|
||||
| <span style="white-space: nowrap;">`graphs`</span> | **Required**. Mapping from graph ID to path where the compiled graph or a function that makes a graph is defined. Example: <ul><li>`./your_package/your_file.py:variable`, where `variable` is an instance of `langgraph.graph.state.CompiledStateGraph`</li><li>`./your_package/your_file.py:make_graph`, where `make_graph` is a function that takes a config dictionary (`langchain_core.runnables.RunnableConfig`) and returns an instance of `langgraph.graph.state.StateGraph` or `langgraph.graph.state.CompiledStateGraph`. See [how to rebuild a graph at runtime](../../cloud/deployment/graph_rebuild.md) for more details.</li></ul> |
|
||||
| <span style="white-space: nowrap;">`auth`</span> | _(Added in v0.0.11)_ Auth configuration containing the path to your authentication handler. Example: `./your_package/auth.py:auth`, where `auth` is an instance of `langgraph_sdk.Auth`. See [authentication guide](../../concepts/auth.md) for details. |
|
||||
| <span style="white-space: nowrap;">`base_image`</span> | Optional. Base image to use for the LangGraph API server. Defaults to `langchain/langgraph-api` or `langchain/langgraphjs-api`. Use this to pin your builds to a particular version of the langgraph API, such as `"langchain/langgraph-server:0.2"`. See https://hub.docker.com/r/langchain/langgraph-server/tags for more details. (added in `langgraph-cli==0.2.8`) |
|
||||
| <span style="white-space: nowrap;">`image_distro`</span> | Optional. Linux distribution for the base image. Must be either `"debian"` or `"wolfi"`. If omitted, defaults to `"debian"`. Available in `langgraph-cli>=0.2.11`.|
|
||||
| <span style="white-space: nowrap;">`env`</span> | Path to `.env` file or a mapping from environment variable to its value. |
|
||||
| <span style="white-space: nowrap;">`store`</span> | Configuration for adding semantic search and/or time-to-live (TTL) to the BaseStore. Contains the following fields: <ul><li>`index` (optional): Configuration for semantic search indexing with fields `embed`, `dims`, and optional `fields`.</li><li>`ttl` (optional): Configuration for item expiration. An object with optional fields: `refresh_on_read` (boolean, defaults to `true`), `default_ttl` (float, lifespan in **minutes**, defaults to no expiration), and `sweep_interval_minutes` (integer, how often to check for expired items, defaults to no sweeping).</li></ul> |
|
||||
| <span style="white-space: nowrap;">`ui`</span> | Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file. (added in `langgraph-cli==0.1.84`) |
|
||||
@@ -79,6 +80,20 @@ The LangGraph CLI requires a JSON configuration file that follows this [schema](
|
||||
}
|
||||
```
|
||||
|
||||
#### Using Wolfi Base Images
|
||||
|
||||
You can specify the Linux distribution for your base image using the `image_distro` field. Valid options are `debian` or `wolfi`. Wolfi is the recommended option as it provides smaller and more secure images. This is available in `langgraph-cli>=0.2.11`.
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"chat": "./chat/graph.py:graph"
|
||||
},
|
||||
"image_distro": "wolfi"
|
||||
}
|
||||
```
|
||||
|
||||
#### Adding semantic search to the store
|
||||
|
||||
All deployments come with a DB-backed BaseStore. Adding an "index" configuration to your `langgraph.json` will enable [semantic search](../deployment/semantic_search.md) within the BaseStore of your deployment.
|
||||
|
||||
@@ -123,3 +123,12 @@ Defaults to `''`.
|
||||
Set `REDIS_CLUSTER` to `True` to enable Redis Cluster mode. When enabled, the system will connect to Redis using cluster mode. This is useful when connecting to a Redis Cluster deployment.
|
||||
|
||||
Defaults to `False`.
|
||||
|
||||
## `MOUNT_PREFIX`
|
||||
|
||||
!!! info "Only Allowed in Self-Hosted Deployments"
|
||||
The `MOUNT_PREFIX` environment variable is only allowed in Self-Hosted Deployment models, LangGraph Platform SaaS will not allow this environment variable.
|
||||
|
||||
Set `MOUNT_PREFIX` to serve the LangGraph Server under a specific path prefix. This is useful for deployments where the server is behind a reverse proxy or load balancer that requires a specific path prefix.
|
||||
|
||||
For example, if the server is to be served under `https://example.com/langgraph`, set `MOUNT_PREFIX` to `/langgraph`.
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
# Data Storage and Privacy
|
||||
|
||||
This document provides a comprehensive overview of what data is stored, collected, and processed when using LangGraph, particularly with the CLI tools like `langgraph dev`.
|
||||
|
||||
## What Data is Stored
|
||||
|
||||
### CLI Telemetry (Opt-out)
|
||||
|
||||
By default, the LangGraph CLI collects minimal analytics data to help improve the tool:
|
||||
|
||||
**Data Collected:**
|
||||
- CLI command used (e.g., `dev`, `up`, `build`)
|
||||
- CLI version
|
||||
- Operating system type and version
|
||||
- Python version
|
||||
- Anonymized parameter usage (boolean flags indicating non-default options were used)
|
||||
|
||||
**Data NOT Collected:**
|
||||
- Actual parameter values
|
||||
- File contents or paths
|
||||
- Personal information
|
||||
- Code or graph implementations
|
||||
- API keys or sensitive data
|
||||
|
||||
**How to Opt Out:**
|
||||
Set the environment variable `LANGGRAPH_CLI_NO_ANALYTICS=1` to disable all CLI analytics collection.
|
||||
|
||||
### LangSmith Integration (Opt-in)
|
||||
|
||||
When a `LANGSMITH_API_KEY` is provided (not required):
|
||||
- Metadata on number of runs executed
|
||||
- Current API version being run
|
||||
- Trace data (if tracing is enabled)
|
||||
|
||||
This data is only sent when explicitly configured with LangSmith credentials.
|
||||
|
||||
### Tracing Data (Opt-in)
|
||||
|
||||
When tracing is enabled:
|
||||
- Execution traces are logged to the configured tracing backend
|
||||
- This requires explicit configuration and is not enabled by default
|
||||
|
||||
## What Data is NOT Stored Remotely
|
||||
|
||||
- **Checkpoints**: Stored locally in your development environment
|
||||
- **Memory store data**: Persisted locally, not transmitted
|
||||
- **Graph state**: Remains in your local environment
|
||||
- **Application data**: Your actual application logic and data stay local
|
||||
|
||||
## Local Data Storage
|
||||
|
||||
### Development Mode (`langgraph dev`)
|
||||
|
||||
When using `langgraph dev`:
|
||||
- State is persisted to a local directory
|
||||
- Checkpoints are stored locally for debugging and development
|
||||
- No remote storage or transmission of your application data
|
||||
|
||||
### Checkpoints and State Persistence
|
||||
|
||||
LangGraph automatically persists:
|
||||
- **Checkpoints**: Snapshots of graph state at each execution step
|
||||
- **Thread data**: Conversation/execution history organized by thread IDs
|
||||
- **Graph state**: Node outputs, intermediate results, and execution metadata
|
||||
- **Memory/Store data**: Information that persists across multiple threads
|
||||
|
||||
**Storage Locations:**
|
||||
- **Local development**: Local directory (configurable)
|
||||
- **Docker deployment**: Local Docker volumes
|
||||
- **LangGraph Platform**: Managed database infrastructure
|
||||
|
||||
## Security and Encryption
|
||||
|
||||
### Data Encryption
|
||||
|
||||
- Checkpointers can optionally encrypt all persisted state
|
||||
- Encryption uses AES encryption via `EncryptedSerializer`
|
||||
- When `LANGGRAPH_AES_KEY` environment variable is present, encryption is automatically enabled on LangGraph Platform
|
||||
|
||||
### Data Retention
|
||||
|
||||
- **TTL (Time-to-Live)**: Configurable automatic cleanup of old data
|
||||
- **Default TTL**: Can be set in minutes for automatic expiration
|
||||
- **Automatic sweeping**: Expired data is automatically removed at configurable intervals
|
||||
|
||||
## Privacy Controls
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Key environment variables for controlling data collection and storage:
|
||||
|
||||
- `LANGGRAPH_CLI_NO_ANALYTICS=1`: Disable CLI analytics collection
|
||||
- `LANGGRAPH_AES_KEY`: Enable automatic encryption of stored data
|
||||
- `LANGSMITH_TRACING=false`: Disable tracing to LangSmith (self-hosted deployments)
|
||||
- `LANGSMITH_API_KEY`: Enable LangSmith integration (opt-in)
|
||||
|
||||
### Logging Controls
|
||||
|
||||
- `LOG_LEVEL`: Control verbosity of logs
|
||||
- `LOG_JSON`: Format logs as JSON
|
||||
- Various other logging configuration options
|
||||
|
||||
## Security Policy
|
||||
|
||||
For security vulnerabilities:
|
||||
- Report through the huntr.com bounty program
|
||||
- LangGraph is in-scope for security bounties
|
||||
- Security contact: `security@langchain.dev`
|
||||
|
||||
## Best Practices for Privacy
|
||||
|
||||
1. **Review Analytics**: Set `LANGGRAPH_CLI_NO_ANALYTICS=1` if you prefer not to share usage analytics
|
||||
2. **Enable Encryption**: Use `LANGGRAPH_AES_KEY` for sensitive data
|
||||
3. **Configure TTL**: Set appropriate data retention policies
|
||||
4. **Monitor Tracing**: Only enable tracing when needed and review what data is being sent
|
||||
5. **Environment Variables**: Audit your environment variables to ensure proper privacy controls
|
||||
|
||||
## Summary
|
||||
|
||||
LangGraph is designed with privacy in mind:
|
||||
- Minimal data collection (analytics can be disabled)
|
||||
- Local storage by default for development
|
||||
- Optional encryption for sensitive data
|
||||
- Clear opt-in requirements for external services
|
||||
- Comprehensive privacy controls through environment variables
|
||||
|
||||
Your application data, checkpoints, and state remain under your control and are not transmitted unless you explicitly configure external services like LangSmith.
|
||||
@@ -19,6 +19,7 @@ From the control plane UI, you can:
|
||||
- Update a deployment.
|
||||
- Update environment variables for a deployment.
|
||||
- View build and server logs of a deployment.
|
||||
- View deployment metrics such as CPU and memory usage.
|
||||
- Delete a deployment.
|
||||
|
||||
The Control Plane UI is embedded in [LangSmith](https://docs.smith.langchain.com/langgraph_cloud).
|
||||
@@ -88,6 +89,17 @@ Infrastructure for deployments and revisions are provisioned and deployed asynch
|
||||
|
||||
The control plane and [LangGraph Data Plane](./langgraph_data_plane.md) "listener" application coordinate to achieve asynchronous deployments.
|
||||
|
||||
### Monitoring
|
||||
|
||||
After a deployment is ready, the control plane monitors the deployment and records various metrics, such as:
|
||||
|
||||
- CPU and memory usage of the deployment.
|
||||
- Number of container restarts.
|
||||
- Number of replicas (this will increase with [autoscaling](../concepts/langgraph_data_plane.md#autoscaling)).
|
||||
- [Postgres](../concepts/langgraph_data_plane.md#postgres) CPU, memory usage, and disk usage.
|
||||
|
||||
These metrics are displayed as charts in the Control Plane UI.
|
||||
|
||||
### LangSmith Integration
|
||||
|
||||
A [LangSmith](https://docs.smith.langchain.com/) tracing project is automatically created for each deployment. The tracing project has the same name as the deployment. When creating a deployment, the `LANGCHAIN_TRACING` and `LANGSMITH_API_KEY`/`LANGCHAIN_API_KEY` environment variables do not need to be specified; they are set automatically by the control plane.
|
||||
|
||||
@@ -21,9 +21,10 @@ Key features of LangGraph Studio:
|
||||
|
||||
- Visualize your graph architecture
|
||||
- [Run and interact with your agent](../cloud/how-tos/invoke_studio.md)
|
||||
- [Manage assistants](../cloud/how-tos/studio/manage_assistants.md.md)
|
||||
- [Manage assistants](../cloud/how-tos/studio/manage_assistants.md)
|
||||
- [Manage threads](../cloud/how-tos/threads_studio.md)
|
||||
- [Iterate on prompts](../cloud/how-tos/iterate_graph_studio.md)
|
||||
- [Run experiments over a dataset](../cloud/how-tos/studio/run_evals.md)
|
||||
- Manage [long term memory](memory.md)
|
||||
- Debug agent state via [time travel](time-travel.md)
|
||||
|
||||
@@ -41,4 +42,4 @@ Chat mode is a simpler UI for iterating on and testing chat-specific agents. It
|
||||
|
||||
## Learn more
|
||||
|
||||
- See this guide on how to [get started](../cloud/how-tos/studio/quick_start.md) with LangGraph Studio.
|
||||
- See this guide on how to [get started](../cloud/how-tos/studio/quick_start.md) with LangGraph Studio.
|
||||
|
||||
@@ -197,19 +197,25 @@ In LangGraph, nodes are typically python functions (sync or async) where the **f
|
||||
Similar to `NetworkX`, you add these nodes to a graph using the [add_node][langgraph.graph.StateGraph.add_node] method:
|
||||
|
||||
```python
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.graph import StateGraph
|
||||
|
||||
builder = StateGraph(dict)
|
||||
class State(TypedDict):
|
||||
input: str
|
||||
results: str
|
||||
|
||||
builder = StateGraph(State)
|
||||
|
||||
|
||||
def my_node(state: dict, config: RunnableConfig):
|
||||
def my_node(state: State, config: RunnableConfig):
|
||||
print("In node: ", config["configurable"]["user_id"])
|
||||
return {"results": f"Hello, {state['input']}!"}
|
||||
|
||||
|
||||
# The second argument is optional
|
||||
def my_other_node(state: dict):
|
||||
def my_other_node(state: State):
|
||||
return state
|
||||
|
||||
|
||||
|
||||
@@ -470,9 +470,34 @@ If the checkpointer is used with asynchronous graph execution (i.e. executing th
|
||||
|
||||
### Serializer
|
||||
|
||||
When checkpointers save the graph state, they need to serialize the channel values in the state. This is done using serializer objects.
|
||||
When checkpointers save the graph state, they need to serialize the channel values in the state. This is done using serializer objects.
|
||||
`langgraph_checkpoint` defines [protocol][langgraph.checkpoint.serde.base.SerializerProtocol] for implementing serializers provides a default implementation ([JsonPlusSerializer][langgraph.checkpoint.serde.jsonplus.JsonPlusSerializer]) that handles a wide variety of types, including LangChain and LangGraph primitives, datetimes, enums and more.
|
||||
|
||||
#### Encryption
|
||||
|
||||
Checkpointers can optionally encrypt all persisted state. To enable this, pass an instance of [`EncryptedSerializer`][langgraph.checkpoint.serde.encrypted.EncryptedSerializer] to the `serde` argument of any `BaseCheckpointSaver` implementation. The easiest way to create an encrypted serializer is via [`from_pycryptodome_aes`][langgraph.checkpoint.serde.encrypted.EncryptedSerializer.from_pycryptodome_aes], which reads the AES key from the `LANGGRAPH_AES_KEY` environment variable (or accepts a `key` argument):
|
||||
|
||||
```python
|
||||
import sqlite3
|
||||
|
||||
from langgraph.checkpoint.serde.encrypted import EncryptedSerializer
|
||||
from langgraph.checkpoint.sqlite import SqliteSaver
|
||||
|
||||
serde = EncryptedSerializer.from_pycryptodome_aes() # reads LANGGRAPH_AES_KEY
|
||||
checkpointer = SqliteSaver(sqlite3.connect("checkpoint.db"), serde=serde)
|
||||
```
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.serde.encrypted import EncryptedSerializer
|
||||
from langgraph.checkpoint.postgres import PostgresSaver
|
||||
|
||||
serde = EncryptedSerializer.from_pycryptodome_aes()
|
||||
checkpointer = PostgresSaver.from_conn_string("postgresql://...", serde=serde)
|
||||
checkpointer.setup()
|
||||
```
|
||||
|
||||
When running on LangGraph Platform, encryption is automatically enabled whenever `LANGGRAPH_AES_KEY` is present, so you only need to provide the environment variable. Other encryption schemes can be used by implementing [`CipherProtocol`][langgraph.checkpoint.serde.base.CipherProtocol] and supplying it to `EncryptedSerializer`.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### Human-in-the-loop
|
||||
|
||||
@@ -5,7 +5,7 @@ search:
|
||||
|
||||
# LangGraph SDK
|
||||
|
||||
LangGraph Platform provides both a Python SDK for interacting with [LangGraph Server](./langgraph_server.md).
|
||||
LangGraph Platform provides both a Python and JS SDK for interacting with [LangGraph Server](./langgraph_server.md).
|
||||
|
||||
!!! tip "Python SDK reference"
|
||||
|
||||
|
||||
@@ -2950,16 +2950,6 @@
|
||||
" When you send updates from a subgraph node to a parent graph node for a key that's shared by both parent and subgraph [state schemas](../../concepts/low_level#schema), you **must** define a [reducer](../../concepts/low_level#reducers) for the key you're updating in the parent graph state. See the example below."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "6be0aeb9-e138-4adc-a1df-5d743a8eb348",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"!!! important \"State updates with `Command.PARENT`\"\n",
|
||||
"\n",
|
||||
" When you send updates from a subgraph node to a parent graph node for a key that's shared by both parent and subgraph [state schemas](../../concepts/low_level#schema), you **must** define a [reducer](../../concepts/low_level#reducers) for the key you're updating in the parent graph state."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
|
||||
@@ -12,12 +12,18 @@
|
||||
options:
|
||||
members:
|
||||
- SerializerProtocol
|
||||
- CipherProtocol
|
||||
|
||||
::: langgraph.checkpoint.serde.jsonplus
|
||||
options:
|
||||
members:
|
||||
- JsonPlusSerializer
|
||||
|
||||
::: langgraph.checkpoint.serde.encrypted
|
||||
options:
|
||||
members:
|
||||
- EncryptedSerializer
|
||||
|
||||
::: langgraph.checkpoint.memory
|
||||
|
||||
::: langgraph.checkpoint.sqlite
|
||||
@@ -32,4 +38,4 @@
|
||||
::: langgraph.checkpoint.postgres.aio
|
||||
options:
|
||||
members:
|
||||
- AsyncPostgresSaver
|
||||
- AsyncPostgresSaver
|
||||
|
||||
+2
-29
@@ -179,10 +179,12 @@ nav:
|
||||
- cloud/how-tos/studio/manage_assistants.md
|
||||
- cloud/how-tos/threads_studio.md
|
||||
- cloud/how-tos/iterate_graph_studio.md
|
||||
- cloud/how-tos/studio/run_evals.md
|
||||
- cloud/how-tos/clone_traces_studio.md
|
||||
- cloud/how-tos/datasets_studio.md
|
||||
- LangGraph SDK: concepts/sdk.md
|
||||
- Data management:
|
||||
- Data storage & privacy: concepts/data_storage_and_privacy.md
|
||||
- Add semantic search: cloud/deployment/semantic_search.md
|
||||
- Add TTLs: how-tos/ttl/configure_ttl.md
|
||||
- Authentication & access control:
|
||||
@@ -364,16 +366,6 @@ markdown_extensions:
|
||||
hooks:
|
||||
- _scripts/notebook_hooks.py
|
||||
extra:
|
||||
consent:
|
||||
title: Cookie consent
|
||||
actions:
|
||||
- accept
|
||||
- reject
|
||||
description: >-
|
||||
We use cookies to recognize your repeated visits and preferences, as well
|
||||
as to measure the effectiveness of our documentation and whether users
|
||||
find what they're searching for. <strong>Clicking "Accept" makes our
|
||||
documentation better. Thank you!</strong> ❤️
|
||||
social:
|
||||
- icon: fontawesome/brands/js
|
||||
link: https://langchain-ai.github.io/langgraphjs/
|
||||
@@ -381,25 +373,6 @@ extra:
|
||||
link: https://github.com/langchain-ai/langgraph
|
||||
- icon: fontawesome/brands/twitter
|
||||
link: https://twitter.com/LangChainAI
|
||||
analytics:
|
||||
provider: google
|
||||
property: G-G8X6ELZYE0
|
||||
feedback:
|
||||
title: Was this page helpful?
|
||||
ratings:
|
||||
- icon: material/emoticon-happy-outline
|
||||
name: This page was helpful
|
||||
data: 1
|
||||
note: >-
|
||||
Thanks for your feedback!
|
||||
- icon: material/emoticon-sad-outline
|
||||
name: This page could be improved
|
||||
data: 0
|
||||
note: >-
|
||||
Thanks for your feedback! Please help us improve this page by adding to the discussion below.
|
||||
shared_analytics:
|
||||
provider: google
|
||||
property: G-47WX3HKKY2
|
||||
validation:
|
||||
# https://www.mkdocs.org/user-guide/configuration/
|
||||
# We are still raising for omitted files because they determine the breadcrumbs for pages.
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block analytics %}
|
||||
<!-- Google Tag Manager -->
|
||||
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer','GTM-T35S4S46');</script>
|
||||
<!-- End Google Tag Manager -->
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block extrahead %}
|
||||
<meta name="algolia-site-verification" content="165B7E7C89E49946" />
|
||||
<style>
|
||||
@@ -185,7 +196,6 @@
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="notebook-links">
|
||||
{% if page.nb_url %}
|
||||
@@ -209,7 +219,6 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block announce %}
|
||||
<strong>We are growing and hiring for multiple roles for LangChain, LangGraph and LangSmith. <a href="https://www.langchain.com/careers" target="_blank" rel="noopener noreferrer"> Join our team!</a></strong>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
import functools
|
||||
import logging
|
||||
import weakref
|
||||
from dataclasses import is_dataclass
|
||||
from inspect import isclass
|
||||
from typing import (
|
||||
Annotated,
|
||||
Any,
|
||||
Callable,
|
||||
Optional,
|
||||
Union,
|
||||
get_args,
|
||||
get_origin,
|
||||
get_type_hints,
|
||||
)
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, TypeAdapter
|
||||
from typing_extensions import is_typeddict
|
||||
|
||||
__all__ = ["SchemaCoercionMapper"]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_cache: weakref.WeakKeyDictionary[type[Any], dict[int, "SchemaCoercionMapper"]] = (
|
||||
weakref.WeakKeyDictionary()
|
||||
)
|
||||
|
||||
|
||||
class SchemaCoercionMapper:
|
||||
"""Lightweight coercion of *dict* → *BaseModel* instances."""
|
||||
|
||||
def __new__(
|
||||
cls,
|
||||
schema: type[Any],
|
||||
type_hints: Optional[dict[str, Any]] = None,
|
||||
*,
|
||||
max_depth: int = 12,
|
||||
) -> "SchemaCoercionMapper":
|
||||
by_depth = _cache.setdefault(schema, {})
|
||||
if max_depth in by_depth:
|
||||
return by_depth[max_depth]
|
||||
inst = super().__new__(cls)
|
||||
by_depth[max_depth] = inst
|
||||
return inst
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
schema: type[BaseModel],
|
||||
type_hints: Optional[dict[str, Any]] = None,
|
||||
*,
|
||||
max_depth: int = 12,
|
||||
) -> None:
|
||||
if hasattr(self, "_initialised"):
|
||||
return
|
||||
self._initialised = True
|
||||
|
||||
self.schema = schema
|
||||
self.max_depth = max_depth
|
||||
|
||||
self.type_hints = (
|
||||
type_hints
|
||||
if type_hints is not None
|
||||
else get_type_hints(schema, localns={schema.__name__: schema})
|
||||
)
|
||||
|
||||
if issubclass(schema, BaseModel):
|
||||
self._fields = {
|
||||
n: self.type_hints.get(n, f.annotation)
|
||||
for n, f in schema.model_fields.items()
|
||||
}
|
||||
self._construct: Callable[..., Any] = schema.model_construct
|
||||
unhandled_attrs = ("validators", "field_validators", "root_validators")
|
||||
if (decorators := getattr(schema, "__pydantic_decorators__", None)) and any(
|
||||
getattr(decorators, attr, None) for attr in unhandled_attrs
|
||||
):
|
||||
self.coerce = lambda v, _: schema.model_validate(v)
|
||||
else:
|
||||
self.coerce = self._coerce
|
||||
else:
|
||||
raise TypeError("Schema must be a Pydantic V2 model.")
|
||||
|
||||
self._field_coercers: Optional[dict[str, Callable[[Any, int], Any]]] = None
|
||||
|
||||
def __call__(self, input_data: Any, depth: Optional[int] = None) -> Any:
|
||||
return self.coerce(input_data, depth)
|
||||
|
||||
def _coerce(self, input_data: Any, depth: Optional[int] = None) -> Any:
|
||||
if depth is None:
|
||||
depth = self.max_depth
|
||||
if not isinstance(input_data, dict) or depth <= 0:
|
||||
return input_data
|
||||
|
||||
if self._field_coercers is None:
|
||||
self._field_coercers = {
|
||||
n: self._build_coercer(t, depth - 1) for n, t in self._fields.items()
|
||||
}
|
||||
|
||||
processed: dict[str, Any] = {}
|
||||
for k, v in input_data.items():
|
||||
fn = self._field_coercers.get(k)
|
||||
processed[k] = fn(v, depth - 1) if fn else v
|
||||
return self._construct(**processed)
|
||||
|
||||
def _build_coercer(
|
||||
self, field_type: Any, depth: int, *, throw: bool = False
|
||||
) -> Callable[[Any, Any], Any]:
|
||||
if depth == 0:
|
||||
return self._passthrough
|
||||
|
||||
origin = get_origin(field_type)
|
||||
|
||||
if (field_type in _IDENTITY_TYPES) or (origin in _IDENTITY_TYPES):
|
||||
return self._passthrough
|
||||
|
||||
if origin is Annotated:
|
||||
real_type, *_ = get_args(field_type)
|
||||
sub = self._build_coercer(real_type, depth - 1)
|
||||
return lambda v, d: sub(v, d)
|
||||
|
||||
if isclass(field_type):
|
||||
# This is needed bcs. of issubclass issues on older versions of python
|
||||
try:
|
||||
is_bm_subclass = issubclass(field_type, BaseModel)
|
||||
except TypeError:
|
||||
# python < 3.11 issue.
|
||||
is_bm_subclass = False
|
||||
if is_bm_subclass:
|
||||
mapper = SchemaCoercionMapper(field_type, max_depth=depth - 1)
|
||||
return lambda v, d: mapper.coerce(v, d) if isinstance(v, dict) else v
|
||||
|
||||
if origin is list:
|
||||
args = get_args(field_type)
|
||||
if len(args) != 1:
|
||||
return self._passthrough
|
||||
sub = self._build_coercer(args[0], depth - 1)
|
||||
|
||||
def list_coercer(v: Any, d: Any) -> Any:
|
||||
if not isinstance(v, (list, tuple)):
|
||||
return v
|
||||
return [sub(x, d - 1) for x in v]
|
||||
|
||||
return list_coercer
|
||||
|
||||
if origin is set or field_type is set:
|
||||
args = get_args(field_type)
|
||||
if len(args) > 1:
|
||||
return self._passthrough
|
||||
elif len(args) == 1:
|
||||
sub = self._build_coercer(args[0], depth - 1)
|
||||
else:
|
||||
sub = None # type: ignore
|
||||
|
||||
def set_coercer(v: Any, d: Any) -> Any:
|
||||
if not isinstance(v, (list, tuple, set)):
|
||||
return v
|
||||
if sub is None:
|
||||
return set(v)
|
||||
return {sub(x, d - 1) for x in v}
|
||||
|
||||
return set_coercer
|
||||
if origin is dict or field_type is dict:
|
||||
args = get_args(field_type)
|
||||
if len(args) != 2:
|
||||
|
||||
def dict_coercer(v: Any, d: Any) -> Any:
|
||||
if not isinstance(v, dict):
|
||||
if throw:
|
||||
raise TypeError(f"Expected dict, got {type(v)}")
|
||||
return v
|
||||
|
||||
return dict_coercer
|
||||
k_sub = self._build_coercer(args[0], depth - 1)
|
||||
v_sub = self._build_coercer(args[1], depth - 1)
|
||||
|
||||
def dict_coercer(v: Any, d: Any) -> Any:
|
||||
if not isinstance(v, dict):
|
||||
if throw:
|
||||
raise TypeError(f"Expected dict, got {type(v)}")
|
||||
return v
|
||||
return {k_sub(k, d - 1): v_sub(val, d - 1) for k, val in v.items()}
|
||||
|
||||
return dict_coercer
|
||||
|
||||
if origin is tuple:
|
||||
elem_types = get_args(field_type)
|
||||
if not elem_types:
|
||||
return self._passthrough
|
||||
subs = [self._build_coercer(t, depth - 1) for t in elem_types]
|
||||
return lambda v, d: (
|
||||
tuple(
|
||||
subs[i](v[i] if i < len(v) else None, d - 1)
|
||||
for i in range(len(subs))
|
||||
)
|
||||
if isinstance(v, (list, tuple))
|
||||
else v
|
||||
)
|
||||
|
||||
if origin is Union:
|
||||
uargs = get_args(field_type)
|
||||
subs, none_in_union = [], False
|
||||
for ix, arg in enumerate(uargs):
|
||||
if arg is type(None):
|
||||
none_in_union = True
|
||||
else:
|
||||
subs.append(
|
||||
self._build_coercer(arg, depth - 1, throw=ix < len(uargs) - 1)
|
||||
)
|
||||
|
||||
def union_coercer(v: Any, d: Any) -> Any:
|
||||
if v is None and none_in_union:
|
||||
return None
|
||||
err = None
|
||||
for sp in subs:
|
||||
try:
|
||||
return sp(v, d - 1)
|
||||
except TypeError as e:
|
||||
err = e
|
||||
if err:
|
||||
raise err
|
||||
return v
|
||||
|
||||
return union_coercer
|
||||
|
||||
adapter_fn = _get_adapter(field_type)
|
||||
return lambda v, _d: adapter_fn(v)
|
||||
|
||||
@staticmethod
|
||||
def _passthrough(v: Any, _d: Any) -> Any: # noqa: D401
|
||||
return v
|
||||
|
||||
|
||||
_adapter_cache: dict[Any, Callable[[Any], Any]] = {}
|
||||
|
||||
|
||||
_IDENTITY_TYPES: tuple[type[Any], ...] = (
|
||||
int,
|
||||
float,
|
||||
str,
|
||||
bool,
|
||||
bytes,
|
||||
bytearray,
|
||||
complex,
|
||||
memoryview,
|
||||
type(None),
|
||||
)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=2048)
|
||||
def _adapter_for(tp: Any) -> Callable[[Any], Any]: # noqa: D401
|
||||
try:
|
||||
config = (
|
||||
None
|
||||
if (issubclass(tp, BaseModel) or is_dataclass(tp) or is_typeddict(tp))
|
||||
else ConfigDict(arbitrary_types_allowed=True)
|
||||
)
|
||||
except TypeError:
|
||||
config = None
|
||||
return TypeAdapter(tp, config=config).validate_python
|
||||
|
||||
|
||||
def _get_adapter(tp: Any) -> Callable[[Any], Any]:
|
||||
try:
|
||||
return _adapter_cache[tp]
|
||||
except KeyError:
|
||||
fn = _adapter_for(tp)
|
||||
_adapter_cache[tp] = fn
|
||||
return fn
|
||||
@@ -64,7 +64,6 @@ from langgraph.graph.graph import (
|
||||
Graph,
|
||||
Send,
|
||||
)
|
||||
from langgraph.graph.schema_utils import SchemaCoercionMapper
|
||||
from langgraph.managed.base import (
|
||||
ChannelKeyPlaceholder,
|
||||
ChannelTypePlaceholder,
|
||||
@@ -320,12 +319,18 @@ class StateGraph(Graph):
|
||||
|
||||
Example:
|
||||
```python
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.graph import START, StateGraph
|
||||
|
||||
def my_node(state, config):
|
||||
class State(TypedDict):
|
||||
x: int
|
||||
|
||||
def my_node(state: State, config: RunnableConfig) -> State:
|
||||
return {"x": state["x"] + 1}
|
||||
|
||||
builder = StateGraph(dict)
|
||||
builder = StateGraph(State)
|
||||
builder.add_node(my_node) # node name will be 'my_node'
|
||||
builder.add_edge(START, "my_node")
|
||||
graph = builder.compile()
|
||||
@@ -335,7 +340,7 @@ class StateGraph(Graph):
|
||||
|
||||
Example: Customize the name:
|
||||
```python
|
||||
builder = StateGraph(dict)
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("my_fair_node", my_node)
|
||||
builder.add_edge(START, "my_fair_node")
|
||||
graph = builder.compile()
|
||||
@@ -1042,11 +1047,8 @@ def _pick_mapper(
|
||||
) -> Optional[Callable[[Any], Any]]:
|
||||
if state_keys == ["__root__"]:
|
||||
return None
|
||||
if isclass(schema):
|
||||
if issubclass(schema, dict):
|
||||
return None
|
||||
if issubclass(schema, BaseModel):
|
||||
return SchemaCoercionMapper(schema, type_hints=type_hints)
|
||||
if isclass(schema) and issubclass(schema, dict):
|
||||
return None
|
||||
return partial(_coerce_state, schema)
|
||||
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ def run_with_retry(
|
||||
except ParentCommand as exc:
|
||||
ns: str = config[CONF][CONFIG_KEY_CHECKPOINT_NS]
|
||||
cmd = exc.args[0]
|
||||
if cmd.graph == ns:
|
||||
if cmd.graph in (ns, task.name):
|
||||
# this command is for the current graph, handle it
|
||||
for w in task.writers:
|
||||
w.invoke(cmd, config)
|
||||
@@ -137,7 +137,7 @@ async def arun_with_retry(
|
||||
except ParentCommand as exc:
|
||||
ns: str = config[CONF][CONFIG_KEY_CHECKPOINT_NS]
|
||||
cmd = exc.args[0]
|
||||
if cmd.graph == ns:
|
||||
if cmd.graph in (ns, task.name):
|
||||
# this command is for the current graph, handle it
|
||||
for w in task.writers:
|
||||
w.invoke(cmd, config)
|
||||
|
||||
@@ -104,7 +104,8 @@ class FuturesDict(Generic[F, E], dict[F, Optional[PregelExecutableTask]]):
|
||||
fut: F,
|
||||
) -> None:
|
||||
try:
|
||||
self.callback()(task, _exception(fut)) # type: ignore[misc]
|
||||
if cb := self.callback():
|
||||
cb(task, _exception(fut))
|
||||
finally:
|
||||
with self.lock:
|
||||
self.done.add(fut)
|
||||
@@ -431,7 +432,7 @@ class PregelRunner:
|
||||
writes.extend(resumes)
|
||||
self.put_writes()(task.id, writes) # type: ignore[misc]
|
||||
elif isinstance(exception, GraphBubbleUp):
|
||||
raise exception
|
||||
pass
|
||||
else:
|
||||
# save error to checkpointer
|
||||
task.writes.append((ERROR, exception))
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph"
|
||||
version = "0.4.7"
|
||||
version = "0.4.8"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
requires-python = ">=3.9"
|
||||
|
||||
@@ -5514,8 +5514,11 @@ def test_runnable_passthrough_node_graph() -> None:
|
||||
assert graph.get_graph(xray=True).to_json() == graph.get_graph(xray=False).to_json()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("subgraph_persist", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_parent_command(request: pytest.FixtureRequest, checkpointer_name: str) -> None:
|
||||
def test_parent_command(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, subgraph_persist: bool
|
||||
) -> None:
|
||||
from langchain_core.messages import BaseMessage
|
||||
from langchain_core.tools import tool
|
||||
|
||||
@@ -5527,7 +5530,7 @@ def test_parent_command(request: pytest.FixtureRequest, checkpointer_name: str)
|
||||
subgraph_builder = StateGraph(MessagesState)
|
||||
subgraph_builder.add_node("tool", get_user_name)
|
||||
subgraph_builder.add_edge(START, "tool")
|
||||
subgraph = subgraph_builder.compile()
|
||||
subgraph = subgraph_builder.compile(checkpointer=subgraph_persist)
|
||||
|
||||
class CustomParentState(TypedDict):
|
||||
messages: Annotated[list[BaseMessage], add_messages]
|
||||
@@ -8802,3 +8805,43 @@ def test_imp_exception(
|
||||
{"my_task": 2},
|
||||
{"my_workflow": "done"},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("subgraph_persist", [True, False])
|
||||
def test_parent_command_goto(
|
||||
sync_checkpointer: BaseCheckpointSaver, subgraph_persist: bool
|
||||
) -> None:
|
||||
class State(TypedDict):
|
||||
dialog_state: Annotated[list[str], operator.add]
|
||||
|
||||
def node_a_child(state):
|
||||
return {"dialog_state": ["a_child_state"]}
|
||||
|
||||
def node_b_child(state):
|
||||
return Command(
|
||||
graph=Command.PARENT,
|
||||
goto="node_b_parent",
|
||||
update={"dialog_state": ["b_child_state"]},
|
||||
)
|
||||
|
||||
sub_builder = StateGraph(State)
|
||||
sub_builder.add_node(node_a_child)
|
||||
sub_builder.add_node(node_b_child)
|
||||
sub_builder.add_edge(START, "node_a_child")
|
||||
sub_builder.add_edge("node_a_child", "node_b_child")
|
||||
sub_graph = sub_builder.compile(checkpointer=subgraph_persist)
|
||||
|
||||
def node_b_parent(state):
|
||||
return {"dialog_state": ["node_b_parent"]}
|
||||
|
||||
main_builder = StateGraph(State)
|
||||
main_builder.add_node(node_b_parent)
|
||||
main_builder.add_edge(START, "subgraph_node")
|
||||
main_builder.add_node("subgraph_node", sub_graph, destinations=("node_b_parent",))
|
||||
|
||||
main_graph = main_builder.compile(sync_checkpointer, name="parent")
|
||||
config = {"configurable": {"thread_id": 1}}
|
||||
|
||||
assert main_graph.invoke(input={"dialog_state": ["init_state"]}, config=config) == {
|
||||
"dialog_state": ["init_state", "b_child_state", "node_b_parent"]
|
||||
}
|
||||
|
||||
@@ -6772,8 +6772,9 @@ async def test_debug_nested_subgraphs(async_checkpointer: BaseCheckpointSaver):
|
||||
assert stream_task.get("state") == history_task.state
|
||||
|
||||
|
||||
@pytest.mark.parametrize("subgraph_persist", [True, False])
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_parent_command(checkpointer_name: str) -> None:
|
||||
async def test_parent_command(checkpointer_name: str, subgraph_persist: bool) -> None:
|
||||
from langchain_core.messages import BaseMessage
|
||||
from langchain_core.tools import tool
|
||||
|
||||
@@ -6785,7 +6786,7 @@ async def test_parent_command(checkpointer_name: str) -> None:
|
||||
subgraph_builder = StateGraph(MessagesState)
|
||||
subgraph_builder.add_node("tool", get_user_name)
|
||||
subgraph_builder.add_edge(START, "tool")
|
||||
subgraph = subgraph_builder.compile()
|
||||
subgraph = subgraph_builder.compile(checkpointer=subgraph_persist)
|
||||
|
||||
class CustomParentState(TypedDict):
|
||||
messages: Annotated[list[BaseMessage], add_messages]
|
||||
@@ -9446,3 +9447,43 @@ async def test_imp_exception(
|
||||
"parent_ids": [],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("subgraph_persist", [True, False])
|
||||
async def test_parent_command_goto(
|
||||
async_checkpointer: BaseCheckpointSaver, subgraph_persist: bool
|
||||
) -> None:
|
||||
class State(TypedDict):
|
||||
dialog_state: Annotated[list[str], operator.add]
|
||||
|
||||
async def node_a_child(state):
|
||||
return {"dialog_state": ["a_child_state"]}
|
||||
|
||||
async def node_b_child(state):
|
||||
return Command(
|
||||
graph=Command.PARENT,
|
||||
goto="node_b_parent",
|
||||
update={"dialog_state": ["b_child_state"]},
|
||||
)
|
||||
|
||||
sub_builder = StateGraph(State)
|
||||
sub_builder.add_node(node_a_child)
|
||||
sub_builder.add_node(node_b_child)
|
||||
sub_builder.add_edge(START, "node_a_child")
|
||||
sub_builder.add_edge("node_a_child", "node_b_child")
|
||||
sub_graph = sub_builder.compile(checkpointer=subgraph_persist)
|
||||
|
||||
async def node_b_parent(state):
|
||||
return {"dialog_state": ["node_b_parent"]}
|
||||
|
||||
main_builder = StateGraph(State)
|
||||
main_builder.add_node(node_b_parent)
|
||||
main_builder.add_edge(START, "subgraph_node")
|
||||
main_builder.add_node("subgraph_node", sub_graph, destinations=("node_b_parent",))
|
||||
|
||||
main_graph = main_builder.compile(async_checkpointer, name="parent")
|
||||
config = {"configurable": {"thread_id": 1}}
|
||||
|
||||
assert await main_graph.ainvoke(
|
||||
input={"dialog_state": ["init_state"]}, config=config
|
||||
) == {"dialog_state": ["init_state", "b_child_state", "node_b_parent"]}
|
||||
|
||||
Generated
+1565
-1565
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user