Merge branch 'main' into v1
@@ -11,6 +11,9 @@ on:
|
||||
env:
|
||||
PYTHON_VERSION: "3.10"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.ref == 'refs/heads/main'
|
||||
|
||||
@@ -24,7 +24,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
python: ${{ steps.filter.outputs.python }}
|
||||
sdk-js: ${{ steps.filter.outputs.sdk-js }}
|
||||
deps: ${{ steps.filter.outputs.deps }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -40,8 +39,6 @@ jobs:
|
||||
- 'libs/checkpoint-sqlite/**'
|
||||
- 'libs/checkpoint-postgres/**'
|
||||
- 'libs/prebuilt/**'
|
||||
sdk-js:
|
||||
- 'libs/sdk-js/**'
|
||||
deps:
|
||||
- '**/pyproject.toml'
|
||||
- '**/uv.lock'
|
||||
@@ -152,68 +149,16 @@ jobs:
|
||||
uses: ./.github/workflows/_integration_test.yml
|
||||
secrets: inherit
|
||||
|
||||
lint-js:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.sdk-js == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
working-directory:
|
||||
- "libs/sdk-js"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ matrix.working-directory }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js (LTS)
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "yarn"
|
||||
cache-dependency-path: ${{ matrix.working-directory }}/yarn.lock
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Run lint
|
||||
run: yarn lint
|
||||
- name: Build
|
||||
run: yarn build
|
||||
|
||||
test-js:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.sdk-js == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
working-directory:
|
||||
- "libs/sdk-js"
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ matrix.working-directory }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js (LTS)
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "yarn"
|
||||
cache-dependency-path: ${{ matrix.working-directory }}/yarn.lock
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Run tests
|
||||
run: yarn test
|
||||
|
||||
ci_success:
|
||||
name: "CI Success"
|
||||
needs:
|
||||
[
|
||||
lint,
|
||||
lint-js,
|
||||
test,
|
||||
test-langgraph,
|
||||
check-sdk-methods,
|
||||
check-schema,
|
||||
integration-test,
|
||||
test-js,
|
||||
]
|
||||
if: |
|
||||
always()
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
name: JS Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
# Disallow publishing from branches that aren't `main`.
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
working-directory:
|
||||
- "libs/sdk-js"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ matrix.working-directory }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# JS Build
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "yarn"
|
||||
cache-dependency-path: ${{ matrix.working-directory }}/yarn.lock
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Build
|
||||
run: yarn build
|
||||
- name: Publish package to NPM
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc
|
||||
npm publish
|
||||
@@ -1,4 +1,3 @@
|
||||
site/
|
||||
docs/cloud/reference/sdk/js_ts_sdk_ref.md
|
||||
|
||||
.vercel
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
.PHONY: lint-docs format-docs build-docs serve-docs serve-clean-docs clean-docs codespell build-typedoc llms-text build-prebuilt tests
|
||||
|
||||
build-typedoc:
|
||||
cd ../libs/sdk-js && yarn install --include-dev && yarn typedoc
|
||||
cd ../libs/sdk-js && yarn --silent concat-md --decrease-title-levels --ignore=js_ts_sdk_ref.md --start-title-level-at 2 docs > ../../docs/docs/cloud/reference/sdk/js_ts_sdk_ref.md 2>/dev/null
|
||||
# Add links to the monorepo
|
||||
sed -e '1,10s|@langchain/langgraph-sdk|[@langchain/langgraph-sdk](https://github.com/langchain-ai/langgraph/tree/main/libs/sdk-js)|g' docs/cloud/reference/sdk/js_ts_sdk_ref.md > temp_file && mv temp_file docs/cloud/reference/sdk/js_ts_sdk_ref.md
|
||||
.PHONY: lint-docs format-docs build-docs serve-docs serve-clean-docs clean-docs codespell llms-text build-prebuilt tests
|
||||
|
||||
build-prebuilt:
|
||||
# Use to create an update to date prebuilt page.
|
||||
@@ -21,7 +15,7 @@ build-prebuilt:
|
||||
fi
|
||||
uv run python -m _scripts.third_party_page.create_third_party_page stats.yml docs/agents/prebuilt.md --language python
|
||||
|
||||
build-docs: build-typedoc build-prebuilt
|
||||
build-docs: build-prebuilt
|
||||
uv run python -m mkdocs build --clean -f mkdocs.yml --strict
|
||||
|
||||
llms-text:
|
||||
@@ -45,7 +39,7 @@ vercel-build-docs: install-vercel-deps
|
||||
serve-clean-docs: clean-docs
|
||||
uv run python -m mkdocs serve -c -f mkdocs.yml --strict -w ../libs/langgraph
|
||||
|
||||
serve-docs: build-typedoc
|
||||
serve-docs:
|
||||
uv run python -m mkdocs serve -f mkdocs.yml -w ../libs/langgraph -w ../libs/checkpoint -w ../libs/sdk-py --dirty
|
||||
|
||||
clean-docs:
|
||||
|
||||
@@ -34,20 +34,20 @@ REDIRECT_MAP = {
|
||||
"how-tos/streaming-from-final-node.ipynb": "how-tos/streaming-specific-nodes.ipynb",
|
||||
"how-tos/streaming-events-from-within-tools-without-langchain.ipynb": "how-tos/streaming-events-from-within-tools.ipynb#example-without-langchain",
|
||||
# graph-api
|
||||
"how-tos/state-reducers.ipynb": "how-tos/graph-api#define-and-update-state",
|
||||
"how-tos/sequence.ipynb": "how-tos/graph-api#create-a-sequence-of-steps",
|
||||
"how-tos/branching.ipynb": "how-tos/graph-api#create-branches",
|
||||
"how-tos/recursion-limit.ipynb": "how-tos/graph-api#create-and-control-loops",
|
||||
"how-tos/visualization.ipynb": "how-tos/graph-api#visualize-your-graph",
|
||||
"how-tos/input_output_schema.ipynb": "how-tos/graph-api#define-input-and-output-schemas",
|
||||
"how-tos/pass_private_state.ipynb": "how-tos/graph-api#pass-private-state-between-nodes",
|
||||
"how-tos/state-model.ipynb": "how-tos/graph-api#use-pydantic-models-for-graph-state",
|
||||
"how-tos/map-reduce.ipynb": "how-tos/graph-api/#map-reduce-and-the-send-api",
|
||||
"how-tos/command.ipynb": "how-tos/graph-api/#combine-control-flow-and-state-updates-with-command",
|
||||
"how-tos/configuration.ipynb": "how-tos/graph-api/#add-runtime-configuration",
|
||||
"how-tos/node-retries.ipynb": "how-tos/graph-api/#add-retry-policies",
|
||||
"how-tos/return-when-recursion-limit-hits.ipynb": "how-tos/graph-api/#impose-a-recursion-limit",
|
||||
"how-tos/async.ipynb": "how-tos/graph-api/#async",
|
||||
"how-tos/state-reducers.ipynb": "how-tos/graph-api.md#define-and-update-state",
|
||||
"how-tos/sequence.ipynb": "how-tos/graph-api.md#create-a-sequence-of-steps",
|
||||
"how-tos/branching.ipynb": "how-tos/graph-api.md#create-branches",
|
||||
"how-tos/recursion-limit.ipynb": "how-tos/graph-api.md#create-and-control-loops",
|
||||
"how-tos/visualization.ipynb": "how-tos/graph-api.md#visualize-your-graph",
|
||||
"how-tos/input_output_schema.ipynb": "how-tos/graph-api.md#define-input-and-output-schemas",
|
||||
"how-tos/pass_private_state.ipynb": "how-tos/graph-api.md#pass-private-state-between-nodes",
|
||||
"how-tos/state-model.ipynb": "how-tos/graph-api.md#use-pydantic-models-for-graph-state",
|
||||
"how-tos/map-reduce.ipynb": "how-tos/graph-api.md#map-reduce-and-the-send-api",
|
||||
"how-tos/command.ipynb": "how-tos/graph-api.md#combine-control-flow-and-state-updates-with-command",
|
||||
"how-tos/configuration.ipynb": "how-tos/graph-api.md#add-runtime-configuration",
|
||||
"how-tos/node-retries.ipynb": "how-tos/graph-api.md#add-retry-policies",
|
||||
"how-tos/return-when-recursion-limit-hits.ipynb": "how-tos/graph-api.md#impose-a-recursion-limit",
|
||||
"how-tos/async.ipynb": "how-tos/graph-api.md#async",
|
||||
# memory how-tos
|
||||
"how-tos/memory/manage-conversation-history.ipynb": "how-tos/memory/add-memory.md",
|
||||
"how-tos/memory/delete-messages.ipynb": "how-tos/memory/add-memory.md#delete-messages",
|
||||
@@ -55,8 +55,8 @@ REDIRECT_MAP = {
|
||||
"how-tos/memory.ipynb": "how-tos/memory/add-memory.md",
|
||||
"agents/memory.ipynb": "how-tos/memory/add-memory.md",
|
||||
# subgraph how-tos
|
||||
"how-tos/subgraph-transform-state.ipynb": "how-tos/subgraph.ipynb#different-state-schemas",
|
||||
"how-tos/subgraphs-manage-state.ipynb": "how-tos/subgraph.ipynb#add-persistence",
|
||||
"how-tos/subgraph-transform-state.ipynb": "how-tos/subgraph.md#different-state-schemas",
|
||||
"how-tos/subgraphs-manage-state.ipynb": "how-tos/subgraph.md#add-persistence",
|
||||
# persistence how-tos
|
||||
"how-tos/persistence_postgres.ipynb": "how-tos/memory/add-memory.md#use-in-production",
|
||||
"how-tos/persistence_mongodb.ipynb": "how-tos/memory/add-memory.md#use-in-production",
|
||||
@@ -73,9 +73,9 @@ REDIRECT_MAP = {
|
||||
"how-tos/pass-run-time-values-to-tools.ipynb": "how-tos/tool-calling.ipynb#read-state",
|
||||
"how-tos/update-state-from-tools.ipynb": "how-tos/tool-calling.ipynb#update-state",
|
||||
# multi-agent how-tos
|
||||
"how-tos/agent-handoffs.ipynb": "how-tos/multi_agent.ipynb#handoffs",
|
||||
"how-tos/multi-agent-network.ipynb": "how-tos/multi_agent.ipynb#use-in-a-multi-agent-system",
|
||||
"how-tos/multi-agent-multi-turn-convo.ipynb": "how-tos/multi_agent.ipynb#multi-turn-conversation",
|
||||
"how-tos/agent-handoffs.ipynb": "how-tos/multi_agent.md#handoffs",
|
||||
"how-tos/multi-agent-network.ipynb": "how-tos/multi_agent.md#use-in-a-multi-agent-system",
|
||||
"how-tos/multi-agent-multi-turn-convo.ipynb": "how-tos/multi_agent.md#multi-turn-conversation",
|
||||
# cloud redirects
|
||||
"cloud/index.md": "index.md",
|
||||
"cloud/how-tos/index.md": "concepts/langgraph_platform",
|
||||
|
||||
@@ -9,21 +9,12 @@ hide:
|
||||
|
||||
# Use MCP
|
||||
|
||||
[Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) is an open protocol that standardizes how applications provide tools and context to language models. LangGraph agents can use tools defined on MCP servers through the `langchain-mcp-adapters` library.
|
||||
|
||||

|
||||
|
||||
Install the `langchain-mcp-adapters` library to use MCP tools in LangGraph:
|
||||
|
||||
```bash
|
||||
pip install langchain-mcp-adapters
|
||||
```
|
||||
The Model Context Protocol (MCP) is an open protocol that standardizes how applications provide tools and context to language models. LangGraph agents can use tools defined on MCP servers through the `langchain-mcp-adapters` library.
|
||||
|
||||
## Use MCP tools
|
||||
|
||||
The `langchain-mcp-adapters` package enables agents to use tools defined across one or more MCP servers.
|
||||
|
||||
|
||||
=== "In an agent"
|
||||
|
||||
```python title="Agent using tools defined on MCP servers"
|
||||
|
||||
@@ -30,7 +30,7 @@ LangGraph includes several capabilities essential for building robust, productio
|
||||
- [**Memory integration**](../how-tos/memory/add-memory.md): Native support for *short-term* (session-based) and *long-term* (persistent across sessions) memory, enabling stateful behaviors in chatbots and assistants.
|
||||
- [**Human-in-the-loop control**](../concepts/human_in_the_loop.md): Execution can pause *indefinitely* to await human feedback—unlike websocket-based solutions limited to real-time interaction. This enables asynchronous approval, correction, or intervention at any point in the workflow.
|
||||
- [**Streaming support**](../how-tos/streaming.md): Real-time streaming of agent state, model tokens, tool outputs, or combined streams.
|
||||
- [**Deployment tooling**](./deployment.md): Includes infrastructure-free deployment tools. [**LangGraph Platform**](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) supports testing, debugging, and deployment.
|
||||
- [**Deployment tooling**](../tutorials/langgraph-platform/local-server.md): Includes infrastructure-free deployment tools. [**LangGraph Platform**](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) supports testing, debugging, and deployment.
|
||||
- **[Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/)**: A visual IDE for inspecting and debugging workflows.
|
||||
- Supports multiple [**deployment options**](https://langchain-ai.github.io/langgraph/concepts/deployment_options.md) for production.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ You can use a prebuilt chat UI for interacting with any LangGraph agent through
|
||||
|
||||
## Run agent in UI
|
||||
|
||||
First, set up LangGraph API server [locally](./deployment.md#launch-langgraph-server-locally) or deploy your agent on [LangGraph Platform](https://langchain-ai.github.io/langgraph/cloud/quick_start/).
|
||||
First, set up LangGraph API server [locally](../tutorials/langgraph-platform/local-server.md) or deploy your agent on [LangGraph Platform](https://langchain-ai.github.io/langgraph/cloud/quick_start/).
|
||||
|
||||
Then, navigate to [Agent Chat UI](https://agentchat.vercel.app), or clone the repository and [run the dev server locally](https://github.com/langchain-ai/agent-chat-ui?tab=readme-ov-file#setup):
|
||||
|
||||
@@ -25,7 +25,7 @@ Then, navigate to [Agent Chat UI](https://agentchat.vercel.app), or clone the re
|
||||
|
||||
## Add human-in-the-loop
|
||||
|
||||
Agent Chat UI has full support for [human-in-the-loop](../concepts/human_in_the_loop.md) workflows. To try it out, replace the agent code in `src/agent/graph.py` (from the [deployment](./deployment.md) guide) with this [agent implementation](../how-tos/human_in_the_loop/add-human-in-the-loop.md#add-interrupts-to-any-tool):
|
||||
Agent Chat UI has full support for [human-in-the-loop](../concepts/human_in_the_loop.md) workflows. To try it out, replace the agent code in `src/agent/graph.py` (from the [deployment](../tutorials/langgraph-platform/local-server.md) guide) with this [agent implementation](../how-tos/human_in_the_loop/add-human-in-the-loop.md#add-interrupts-to-any-tool):
|
||||
|
||||
<video controls src="../assets/interrupt-chat-ui.mp4" type="video/mp4"></video>
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# API Reference
|
||||
# LangGraph Server API Reference
|
||||
|
||||
The LangGraph Platform API reference is available with each deployment at the `/docs` URL path (e.g. `http://localhost:8124/docs`).
|
||||
The LangGraph Server API reference is available within each deployment at the `/docs` endpoint (e.g. `http://localhost:8124/docs`).
|
||||
|
||||
Click <a href="/langgraph/cloud/reference/api/api_ref.html" target="_blank">here</a> to view the API reference.
|
||||
|
||||
## Authentication
|
||||
|
||||
For deployments to LangGraph Platform, authentication is required. Pass the `X-Api-Key` header with each request to the LangGraph Platform API. The value of the header should be set to a valid LangSmith API key for the organization where the API is deployed.
|
||||
For deployments to LangGraph Platform, authentication is required. Pass the `X-Api-Key` header with each request to the LangGraph Server. The value of the header should be set to a valid LangSmith API key for the organization where the LangGraph Server is deployed.
|
||||
|
||||
Example `curl` command:
|
||||
```shell
|
||||
@@ -18,5 +18,5 @@ curl --request POST \
|
||||
"metadata": {},
|
||||
"limit": 10,
|
||||
"offset": 0
|
||||
}'
|
||||
}'
|
||||
```
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
# LangGraph Control Plane API Reference
|
||||
|
||||
The LangGraph Control Plane API is used to programmatically create and manage LangGraph Server deployments. For example, the APIs can be orchestrated to create custom CI/CD workflows.
|
||||
|
||||
Click <a href="https://api.host.langchain.com/docs" target="_blank">here</a> to view the API reference.
|
||||
|
||||
## Host
|
||||
|
||||
LangGraph Control Plane hosts for Cloud SaaS data regions:
|
||||
|
||||
| US | EU |
|
||||
|----|----|
|
||||
| `https://api.host.langchain.com` | `https://eu.api.host.langchain.com` |
|
||||
|
||||
**Note**: Self-hosted deployments of LangGraph Platform will have a custom host for the LangGraph Control Plane.
|
||||
|
||||
## Authentication
|
||||
|
||||
To authenticate with the LangGraph Control Plane API, set the `X-Api-Key` header to a valid LangSmith API key.
|
||||
|
||||
Example `curl` command:
|
||||
```shell
|
||||
curl --request GET \
|
||||
--url http://localhost:8124/v2/deployments \
|
||||
--header 'X-Api-Key: LANGSMITH_API_KEY'
|
||||
```
|
||||
|
||||
## Versioning
|
||||
|
||||
Each endpoint path is prefixed with a version (e.g. `v1`, `v2`).
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Call `POST /v2/deployments` to create a new Deployment. The response body contains the Deployment ID (`id`) and the ID of the latest (and first) revision (`latest_revision_id`).
|
||||
1. Call `GET /v2/deployments/{deployment_id}` to retrieve the Deployment. Set `deployment_id` in the URL to the value of Deployment ID (`id`).
|
||||
1. Poll for revision `status` until `status` is `DEPLOYED` by calling `GET /v2/deployments/{deployment_id}/revisions/{latest_revision_id}`.
|
||||
1. Call `PATCH /v2/deployments/{deployment_id}` to update the deployment.
|
||||
|
||||
## Example Code
|
||||
Below is example Python code that demonstrates how to orchestrate the LangGraph Control Plane APIs to create a deployment, update the deployment, and delete the deployment.
|
||||
```python
|
||||
import os
|
||||
import time
|
||||
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# required environment variables
|
||||
CONTROL_PLANE_HOST = os.getenv("CONTROL_PLANE_HOST")
|
||||
LANGSMITH_API_KEY = os.getenv("LANGSMITH_API_KEY")
|
||||
INTEGRATION_ID = os.getenv("INTEGRATION_ID")
|
||||
MAX_WAIT_TIME = 1800 # 30 mins
|
||||
|
||||
|
||||
def get_headers() -> dict:
|
||||
"""Return common headers for requests to LangGraph Control Plane API."""
|
||||
return {
|
||||
"X-Api-Key": LANGSMITH_API_KEY,
|
||||
}
|
||||
|
||||
|
||||
def create_deployment() -> str:
|
||||
"""Create deployment. Return deployment ID."""
|
||||
headers = get_headers()
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
deployment_name = "my_deployment"
|
||||
|
||||
request_body = {
|
||||
"name": deployment_name,
|
||||
"source": "github",
|
||||
"source_config": {
|
||||
"integration_id": INTEGRATION_ID,
|
||||
"repo_url": "https://github.com/langchain-ai/langgraph-example",
|
||||
"deployment_type": "dev",
|
||||
"build_on_push": False,
|
||||
"custom_url": None,
|
||||
"resource_spec": None,
|
||||
},
|
||||
"source_revision_config": {
|
||||
"repo_ref": "main",
|
||||
"langgraph_config_path": "langgraph.json",
|
||||
"image_uri": None,
|
||||
},
|
||||
"secrets": [
|
||||
{
|
||||
"name": "OPENAI_API_KEY",
|
||||
"value": "test_openai_api_key",
|
||||
},
|
||||
{
|
||||
"name": "ANTHROPIC_API_KEY",
|
||||
"value": "test_anthropic_api_key",
|
||||
},
|
||||
{
|
||||
"name": "TAVILY_API_KEY",
|
||||
"value": "test_tavily_api_key",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
url=f"{CONTROL_PLANE_HOST}/v2/deployments",
|
||||
headers=headers,
|
||||
json=request_body,
|
||||
)
|
||||
|
||||
if response.status_code != 201:
|
||||
raise Exception(f"Failed to create deployment: {response.text}")
|
||||
|
||||
deployment_id = response.json()["id"]
|
||||
print(f"Created deployment {deployment_name} ({deployment_id})")
|
||||
return deployment_id
|
||||
|
||||
|
||||
def get_deployment(deployment_id: str) -> dict:
|
||||
"""Get deployment."""
|
||||
response = requests.get(
|
||||
url=f"{CONTROL_PLANE_HOST}/v2/deployments/{deployment_id}",
|
||||
headers=get_headers(),
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to get deployment ID {deployment_id}: {response.text}")
|
||||
|
||||
return response.json()
|
||||
|
||||
|
||||
def list_revisions(deployment_id: str) -> list[dict]:
|
||||
"""List revisions.
|
||||
|
||||
Return list is sorted by created_at in descending order (latest first).
|
||||
"""
|
||||
response = requests.get(
|
||||
url=f"{CONTROL_PLANE_HOST}/v2/deployments/{deployment_id}/revisions",
|
||||
headers=get_headers(),
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(
|
||||
f"Failed to list revisions for deployment ID {deployment_id}: {response.text}"
|
||||
)
|
||||
|
||||
return response.json()
|
||||
|
||||
|
||||
def get_revision(
|
||||
deployment_id: str,
|
||||
revision_id: str,
|
||||
) -> dict:
|
||||
"""Get revision."""
|
||||
response = requests.get(
|
||||
url=f"{CONTROL_PLANE_HOST}/v2/deployments/{deployment_id}/revisions/{revision_id}",
|
||||
headers=get_headers(),
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to get revision ID {revision_id}: {response.text}")
|
||||
|
||||
return response.json()
|
||||
|
||||
|
||||
def patch_deployment(deployment_id: str) -> None:
|
||||
"""Patch deployment."""
|
||||
headers = get_headers()
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
response = requests.patch(
|
||||
url=f"{CONTROL_PLANE_HOST}/v2/deployments/{deployment_id}",
|
||||
headers=headers,
|
||||
json={
|
||||
"source_config": {
|
||||
"build_on_push": True,
|
||||
},
|
||||
"source_revision_config": {
|
||||
"repo_ref": "main",
|
||||
"langgraph_config_path": "langgraph.json",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Failed to patch deployment: {response.text}")
|
||||
|
||||
print(f"Patched deployment ID {deployment_id}")
|
||||
|
||||
|
||||
def wait_for_deployment(deployment_id: str, revision_id: str) -> None:
|
||||
"""Wait for revision status to be DEPLOYED."""
|
||||
start_time = time.time()
|
||||
revision, status = None, None
|
||||
while time.time() - start_time < MAX_WAIT_TIME:
|
||||
revision = get_revision(deployment_id, revision_id)
|
||||
status = revision["status"]
|
||||
if status == "DEPLOYED":
|
||||
break
|
||||
elif "FAILED" in status:
|
||||
raise Exception(f"Revision ID {revision_id} failed: {revision}")
|
||||
|
||||
print(f"Waiting for revision ID {revision_id} to be DEPLOYED...")
|
||||
time.sleep(60)
|
||||
|
||||
if status != "DEPLOYED":
|
||||
raise Exception(
|
||||
f"Timeout waiting for revision ID {revision_id} to be DEPLOYED: {revision}"
|
||||
)
|
||||
|
||||
|
||||
def delete_deployment(deployment_id: str) -> None:
|
||||
"""Delete deployment."""
|
||||
response = requests.delete(
|
||||
url=f"{CONTROL_PLANE_HOST}/v2/deployments/{deployment_id}",
|
||||
headers=get_headers(),
|
||||
)
|
||||
|
||||
if response.status_code != 204:
|
||||
raise Exception(
|
||||
f"Failed to delete deployment ID {deployment_id}: {response.text}"
|
||||
)
|
||||
|
||||
print(f"Deployment ID {deployment_id} deleted")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# create deployment and get the latest revision
|
||||
deployment_id = create_deployment()
|
||||
revisions = list_revisions(deployment_id)
|
||||
latest_revision = revisions["resources"][0]
|
||||
latest_revision_id = latest_revision["id"]
|
||||
|
||||
# wait for latest revision to be DEPLOYED
|
||||
wait_for_deployment(deployment_id, latest_revision_id)
|
||||
|
||||
# patch the deployment and get the latest revision
|
||||
patch_deployment(deployment_id)
|
||||
revisions = list_revisions(deployment_id)
|
||||
latest_revision = revisions["resources"][0]
|
||||
latest_revision_id = latest_revision["id"]
|
||||
|
||||
# wait for latest revision to be DEPLOYED
|
||||
wait_for_deployment(deployment_id, latest_revision_id)
|
||||
|
||||
# delete the deployment
|
||||
delete_deployment(deployment_id)
|
||||
```
|
||||
@@ -53,7 +53,7 @@ The LangGraph CLI requires a JSON configuration file that follows this [schema](
|
||||
| <span style="white-space: nowrap;">`pip_installer`</span> | _(Added in v0.3)_ Optional. Python package installer selector. It can be set to `"auto"`, `"pip"`, or `"uv"`. From version 0.3 onward the default strategy is to run `uv pip`, which typically delivers faster builds while remaining a drop-in replacement. In the uncommon situation where `uv` cannot handle your dependency graph or the structure of your `pyproject.toml`, specify `"pip"` here to revert to the earlier behaviour. |
|
||||
| <span style="white-space: nowrap;">`dockerfile_lines`</span> | Array of additional lines to add to Dockerfile following the import from parent image. |
|
||||
| <span style="white-space: nowrap;">`checkpointer`</span> | Configuration for the checkpointer. Contains a `ttl` field which is an object with the following keys: <ul><li>`strategy`: How to handle expired checkpoints (e.g., `"delete"`).</li><li>`sweep_interval_minutes`: How often to check for expired checkpoints (integer).</li><li>`default_ttl`: Default time-to-live for checkpoints in **minutes** (integer). Defines how long checkpoints are kept before the specified strategy is applied.</li></ul> |
|
||||
| <span style="white-space: nowrap;">`http`</span> | HTTP server configuration with the following fields: <ul><li>`app`: Path to custom Starlette/FastAPI app (e.g., `"./src/agent/webapp.py:app"`). See [custom routes guide](../../how-tos/http/custom_routes.md).</li><li>`disable_assistants`: Disable `/assistants` routes</li><li>`disable_threads`: Disable `/threads` routes</li><li>`disable_runs`: Disable `/runs` routes</li><li>`disable_store`: Disable `/store` routes</li><li>`disable_meta`: Disable `/ok`, `/info`, `/metrics`, and `/docs` routes</li><li>`disable_mcp`: Disable `/mcp` routes</li><li>`cors`: CORS configuration with fields for `allow_origins`, `allow_methods`, `allow_headers`, etc.</li><li>`configurable_headers`: Define which request headers to exclude or include as a run's configurable values.</li></ul> |
|
||||
| <span style="white-space: nowrap;">`http`</span> | HTTP server configuration with the following fields: <ul><li>`app`: Path to custom Starlette/FastAPI app (e.g., `"./src/agent/webapp.py:app"`). See [custom routes guide](../../how-tos/http/custom_routes.md).</li><li>`cors`: CORS configuration with fields for `allow_origins`, `allow_methods`, `allow_headers`, etc.</li><li>`configurable_headers`: Define which request headers to exclude or include as a run's configurable values.</li><li>`disable_assistants`: Disable `/assistants` routes</li><li>`disable_mcp`: Disable `/mcp` routes</li><li>`disable_meta`: Disable `/ok`, `/info`, `/metrics`, and `/docs` routes</li><li>`disable_runs`: Disable `/runs` routes</li><li>`disable_store`: Disable `/store` routes</li><li>`disable_threads`: Disable `/threads` routes</li><li>`disable_ui`: Disable `/ui` routes</li><li>`disable_webhooks`: Disable webhooks calls on run completion in all routes</li><li>`mount_prefix`: Prefix for mounted routes (e.g., "/my-deployment/api")</li></ul> |
|
||||
|
||||
=== "JS"
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ Parallel processing is vital for efficient multi-agent systems and complex tasks
|
||||
- Implementation of map-reduce-like operations
|
||||
- Efficient handling of independent subtasks
|
||||
|
||||
For practical implementation, see our [map-reduce tutorial](../how-tos/graph-api.ipynb#map-reduce-and-the-send-api)
|
||||
For practical implementation, see our [map-reduce tutorial](../how-tos/graph-api.md#map-reduce-and-the-send-api)
|
||||
|
||||
### Subgraphs
|
||||
|
||||
@@ -107,7 +107,7 @@ For practical implementation, see our [map-reduce tutorial](../how-tos/graph-api
|
||||
- Hierarchical organization of agent teams
|
||||
- Controlled communication between agents and the main system
|
||||
|
||||
Subgraphs communicate with the parent graph through overlapping keys in the state schema. This enables flexible, modular agent design. For implementation details, refer to our [subgraph how-to guide](../how-tos/subgraph.ipynb).
|
||||
Subgraphs communicate with the parent graph through overlapping keys in the state schema. This enables flexible, modular agent design. For implementation details, refer to our [subgraph how-to guide](../how-tos/subgraph.md).
|
||||
|
||||
### Reflection
|
||||
|
||||
|
||||
@@ -143,6 +143,54 @@ The returned user information is available:
|
||||
In many of our tutorials, we will just show the "authorization" parameter to be concise, but you can opt to accept more information as needed
|
||||
to implement your custom authentication scheme.
|
||||
|
||||
### Agent authentication
|
||||
|
||||
Custom authentication permits delegated access. The values you return in `@auth.authenticate` are added to the run context, giving agents user-scoped credentials lets them access resources on the user’s behalf.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
%% Actors
|
||||
participant ClientApp as Client
|
||||
participant AuthProv as Auth Provider
|
||||
participant LangGraph as LangGraph Backend
|
||||
participant SecretStore as Secret Store
|
||||
participant ExternalService as External Service
|
||||
|
||||
%% Platform login / AuthN
|
||||
ClientApp ->> AuthProv: 1. Login (username / password)
|
||||
AuthProv -->> ClientApp: 2. Return token
|
||||
ClientApp ->> LangGraph: 3. Request with token
|
||||
|
||||
Note over LangGraph: 4. Validate token (@auth.authenticate)
|
||||
LangGraph -->> AuthProv: 5. Fetch user info
|
||||
AuthProv -->> LangGraph: 6. Confirm validity
|
||||
|
||||
%% Fetch user tokens from secret store
|
||||
LangGraph ->> SecretStore: 6a. Fetch user tokens
|
||||
SecretStore -->> LangGraph: 6b. Return tokens
|
||||
|
||||
Note over LangGraph: 7. Apply access control (@auth.on.*)
|
||||
|
||||
%% External Service round-trip
|
||||
LangGraph ->> ExternalService: 8. Call external service (with header)
|
||||
Note over ExternalService: 9. External service validates header and executes action
|
||||
ExternalService -->> LangGraph: 10. Service response
|
||||
|
||||
%% Return to caller
|
||||
LangGraph -->> ClientApp: 11. Return resources
|
||||
```
|
||||
|
||||
After authentication, the platform creates a special configuration object that is passed to your graph and all nodes via the configurable context.
|
||||
This object contains information about the current user, including any custom fields you return from your [`@auth.authenticate`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.authenticate) handler.
|
||||
|
||||
To enable an agent to act on behalf of the user, use [custom authentication middleware](../how-tos/auth/custom_auth.md). This will allow the agent to interact with external systems like MCP servers, external databases, and even other agents on behalf of the user.
|
||||
|
||||
For more information, see the [Use custom auth](../how-tos/auth/custom_auth.md#enable-agent-authentication) guide.
|
||||
|
||||
### Agent authentication with MCP
|
||||
|
||||
For information on how to authenticate an agent to an MCP server, see the [MCP conceptual guide](../concepts/mcp.md).
|
||||
|
||||
## Authorization
|
||||
|
||||
After authentication, LangGraph calls your [`@auth.on`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.on) handlers to control access to specific resources (e.g., threads, assistants, crons). These handlers can:
|
||||
|
||||
@@ -26,7 +26,7 @@ The Control Plane UI is embedded in [LangSmith](https://docs.smith.langchain.com
|
||||
|
||||
## Control Plane API
|
||||
|
||||
This section describes data model of the control plane API. The API is used to create, update, and delete deployments. However, they are not publicly accessible.
|
||||
This section describes the data model of the control plane API. The API is used to create, update, and delete deployments. See the [control plane API reference](../cloud/reference/api/api_ref_control_plane.md) for more details.
|
||||
|
||||
### Deployment
|
||||
|
||||
@@ -34,11 +34,7 @@ A deployment is an instance of a LangGraph Server. A single deployment can have
|
||||
|
||||
### Revision
|
||||
|
||||
A revision is an iteration of a deployment. When a new deployment is created, an initial revision is automatically created. To deploy code changes or update environment variables for a deployment, a new revision must be created.
|
||||
|
||||
### Environment Variable
|
||||
|
||||
Environment variables are set for a deployment. All environment variables are stored as secrets (i.e. saved in a secrets store).
|
||||
A revision is an iteration of a deployment. When a new deployment is created, an initial revision is automatically created. To deploy code changes or update secrets for a deployment, a new revision must be created.
|
||||
|
||||
## Control Plane Features
|
||||
|
||||
@@ -50,21 +46,40 @@ For simplicity, the control plane offers two deployment types with different res
|
||||
|
||||
| **Deployment Type** | **CPU/Memory** | **Scaling** | **Database** |
|
||||
|---------------------|-----------------|---------------------|----------------------------------------------------------------------------------|
|
||||
| Development | 1 CPU, 1 GB RAM | Up to 1 container | 10 GB disk, no backups |
|
||||
| Production | 2 CPU, 2 GB RAM | Up to 10 containers | Autoscaling disk, automatic backups, highly available (multi-zone configuration) |
|
||||
| Development | 1 CPU, 1 GB RAM | Up to 1 replica | 10 GB disk, no backups |
|
||||
| Production | 2 CPU, 2 GB RAM | Up to 10 replicas | Autoscaling disk, automatic backups, highly available (multi-zone configuration) |
|
||||
|
||||
CPU and memory resources are per container.
|
||||
CPU and memory resources are per replica.
|
||||
|
||||
!!! warning "Immutable Deployment Type"
|
||||
|
||||
Once a deployment is created, the deployment type cannot be changed.
|
||||
|
||||
!!! info "Resource Customization"
|
||||
For `Production` type deployments, resources can be manually increased on a case-by-case basis depending on use case and capacity constraints. Contact support@langchain.dev to request an increase in resources.
|
||||
!!! info "Self-Hosted Deployment"
|
||||
Resources for [Self-Hosted Data Plane](../concepts/langgraph_self_hosted_data_plane.md) and [Self-Hosted Control Plane](../concepts/langgraph_self_hosted_control_plane.md) deployments can be fully customized. Deployment types are only applicable for [Cloud SaaS](../concepts/langgraph_cloud.md) deployments.
|
||||
|
||||
For `Development` types deployments, database disk size can be manually increased on a case-by-case basis depending on use case and capacity constraints. For most use cases, [TTLs](../how-tos/ttl/configure_ttl.md) should be configured to manage disk usage. Contact support@langchain.dev to request an increase in resources.
|
||||
#### Production
|
||||
|
||||
Resources for [Self-Hosted Data Plane](../concepts/langgraph_self_hosted_data_plane.md) and [Self-Hosted Control Plane](../concepts/langgraph_self_hosted_control_plane.md) deployments can be fully customized.
|
||||
`Production` type deployments are suitable for "production" workloads. For example, select `Production` for customer-facing applications in the critical path.
|
||||
|
||||
Resources for `Production` type deployments can be manually increased on a case-by-case basis depending on use case and capacity constraints. Contact support@langchain.dev to request an increase in resources.
|
||||
|
||||
#### Development
|
||||
|
||||
`Development` type deployments are suitable development and testing. For example, select `Development` for internal testing environments. `Development` type deployments are not suitable for "production" workloads.
|
||||
|
||||
!!! danger "Preemptible Compute Infrastructure"
|
||||
`Development` type deployments (API server, queue server, and database) are provisioned on preemptible compute infrastructure. This means the compute infrastructure **may be terminated at any time without notice**. This may result in intermittent...
|
||||
|
||||
- Redis connection timeouts/errors
|
||||
- Postgres connection timeouts/errors
|
||||
- Failed or retrying background runs
|
||||
|
||||
This behavior is expected. Preemptible compute infrastructure **significantly reduces the cost to provision a `Development` type deployment**. By design, LangGraph Server is fault-tolerant. The implementation will automatically attempt to recover from Redis/Postgres connection errors and retry failed background runs.
|
||||
|
||||
`Production` type deployments are provisioned on durable compute infrastructure, not preemptible compute infrastructure.
|
||||
|
||||
Database disk size for `Development` type deployments can be manually increased on a case-by-case basis depending on use case and capacity constraints. For most use cases, [TTLs](../how-tos/ttl/configure_ttl.md) should be configured to manage disk usage. Contact support@langchain.dev to request an increase in resources.
|
||||
|
||||
### Database Provisioning
|
||||
|
||||
|
||||
@@ -45,9 +45,9 @@ The first thing you do when you define a graph is define the `State` of the grap
|
||||
|
||||
### Schema
|
||||
|
||||
The main documented way to specify the schema of a graph is by using `TypedDict`. However, we also support [using a Pydantic BaseModel](../how-tos/graph-api.ipynb#use-pydantic-models-for-graph-state) as your graph state to add **default values** and additional data validation.
|
||||
The main documented way to specify the schema of a graph is by using `TypedDict`. However, we also support [using a Pydantic BaseModel](../how-tos/graph-api.md#use-pydantic-models-for-graph-state) as your graph state to add **default values** and additional data validation.
|
||||
|
||||
By default, the graph will have the same input and output schemas. If you want to change this, you can also specify explicit input and output schemas directly. This is useful when you have a lot of keys, and some are explicitly for input and others for output. See the [guide here](../how-tos/graph-api.ipynb#define-input-and-output-schemas) for how to use.
|
||||
By default, the graph will have the same input and output schemas. If you want to change this, you can also specify explicit input and output schemas directly. This is useful when you have a lot of keys, and some are explicitly for input and others for output. See the [guide here](../how-tos/graph-api.md#define-input-and-output-schemas) for how to use.
|
||||
|
||||
#### Multiple schemas
|
||||
|
||||
@@ -56,9 +56,9 @@ Typically, all graph nodes communicate with a single schema. This means that the
|
||||
- Internal nodes can pass information that is not required in the graph's input / output.
|
||||
- We may also want to use different input / output schemas for the graph. The output might, for example, only contain a single relevant output key.
|
||||
|
||||
It is possible to have nodes write to private state channels inside the graph for internal node communication. We can simply define a private schema, `PrivateState`. See [this guide](../how-tos/graph-api.ipynb#pass-private-state-between-nodes) for more detail.
|
||||
It is possible to have nodes write to private state channels inside the graph for internal node communication. We can simply define a private schema, `PrivateState`. See [this guide](../how-tos/graph-api.md#pass-private-state-between-nodes) for more detail.
|
||||
|
||||
It is also possible to define explicit input and output schemas for a graph. In these cases, we define an "internal" schema that contains _all_ keys relevant to graph operations. But, we also define `input` and `output` schemas that are sub-sets of the "internal" schema to constrain the input and output of the graph. See [this guide](../how-tos/graph-api.ipynb#define-input-and-output-schemas) for more detail.
|
||||
It is also possible to define explicit input and output schemas for a graph. In these cases, we define an "internal" schema that contains _all_ keys relevant to graph operations. But, we also define `input` and `output` schemas that are sub-sets of the "internal" schema to constrain the input and output of the graph. See [this guide](../how-tos/graph-api.md#define-input-and-output-schemas) for more detail.
|
||||
|
||||
Let's look at an example:
|
||||
|
||||
@@ -406,7 +406,7 @@ def my_node(state: State) -> Command[Literal["my_other_node"]]:
|
||||
|
||||
When returning `Command` in your node functions, you must add return type annotations with the list of node names the node is routing to, e.g. `Command[Literal["my_other_node"]]`. This is necessary for the graph rendering and tells LangGraph that `my_node` can navigate to `my_other_node`.
|
||||
|
||||
Check out this [how-to guide](../how-tos/graph-api.ipynb#combine-control-flow-and-state-updates-with-command) for an end-to-end example of how to use `Command`.
|
||||
Check out this [how-to guide](../how-tos/graph-api.md#combine-control-flow-and-state-updates-with-command) for an end-to-end example of how to use `Command`.
|
||||
|
||||
### When should I use Command instead of conditional edges?
|
||||
|
||||
@@ -433,17 +433,17 @@ def my_node(state: State) -> Command[Literal["other_subgraph"]]:
|
||||
|
||||
!!! important "State updates with `Command.PARENT`"
|
||||
|
||||
When you send updates from a subgraph node to a parent graph node for a key that's shared by both parent and subgraph [state schemas](#schema), you **must** define a [reducer](#reducers) for the key you're updating in the parent graph state. See this [example](../how-tos/graph-api.ipynb#navigate-to-a-node-in-a-parent-graph).
|
||||
When you send updates from a subgraph node to a parent graph node for a key that's shared by both parent and subgraph [state schemas](#schema), you **must** define a [reducer](#reducers) for the key you're updating in the parent graph state. See this [example](../how-tos/graph-api.md#navigate-to-a-node-in-a-parent-graph).
|
||||
|
||||
This is particularly useful when implementing [multi-agent handoffs](./multi_agent.md#handoffs).
|
||||
|
||||
Check out [this guide](../how-tos/graph-api.ipynb#navigate-to-a-node-in-a-parent-graph) for detail.
|
||||
Check out [this guide](../how-tos/graph-api.md#navigate-to-a-node-in-a-parent-graph) for detail.
|
||||
|
||||
### Using inside tools
|
||||
|
||||
A common use case is updating graph state from inside a tool. For example, in a customer support application you might want to look up customer information based on their account number or ID in the beginning of the conversation.
|
||||
|
||||
Refer to [this guide](../how-tos/graph-api.ipynb#use-inside-tools) for detail.
|
||||
Refer to [this guide](../how-tos/graph-api.md#use-inside-tools) for detail.
|
||||
|
||||
### Human-in-the-loop
|
||||
|
||||
@@ -489,7 +489,7 @@ def node_a(state, config):
|
||||
...
|
||||
```
|
||||
|
||||
See [this guide](../how-tos/graph-api.ipynb#add-runtime-configuration) for a full breakdown on configuration.
|
||||
See [this guide](../how-tos/graph-api.md#add-runtime-configuration) for a full breakdown on configuration.
|
||||
|
||||
### Recursion Limit
|
||||
|
||||
@@ -503,4 +503,4 @@ Read [this how-to](https://langchain-ai.github.io/langgraph/how-tos/recursion-li
|
||||
|
||||
## Visualization
|
||||
|
||||
It's often nice to be able to visualize graphs, especially as they get more complex. LangGraph comes with several built-in ways to visualize graphs. See [this how-to guide](../how-tos/graph-api.ipynb#visualize-your-graph) for more info.
|
||||
It's often nice to be able to visualize graphs, especially as they get more complex. LangGraph comes with several built-in ways to visualize graphs. See [this how-to guide](../how-tos/graph-api.md#visualize-your-graph) for more info.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# MCP
|
||||
|
||||
[Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) is an open protocol that standardizes how applications provide tools and context to language models. LangGraph agents can use tools defined on MCP servers through the `langchain-mcp-adapters` library.
|
||||
|
||||

|
||||
|
||||
Install the `langchain-mcp-adapters` library to use MCP tools in LangGraph:
|
||||
|
||||
```bash
|
||||
pip install langchain-mcp-adapters
|
||||
```
|
||||
|
||||
## Authenticate to an MCP server
|
||||
|
||||
You can set up [custom authentication middleware](../how-tos/auth/custom_auth.md) to authenticate a user with an MCP server to get access to user-scoped tools within your LangGraph Platform deployment.
|
||||
|
||||
!!! note
|
||||
Custom authentication is a LangGraph Platform feature.
|
||||
|
||||
An example architecture for this flow:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
%% Actors
|
||||
participant ClientApp as Client
|
||||
participant AuthProv as Auth Provider
|
||||
participant LangGraph as LangGraph Backend
|
||||
participant SecretStore as Secret Store
|
||||
participant MCPServer as MCP Server
|
||||
|
||||
%% Platform login / AuthN
|
||||
ClientApp ->> AuthProv: 1. Login (username / password)
|
||||
AuthProv -->> ClientApp: 2. Return token
|
||||
ClientApp ->> LangGraph: 3. Request with token
|
||||
|
||||
Note over LangGraph: 4. Validate token (@auth.authenticate)
|
||||
LangGraph -->> AuthProv: 5. Fetch user info
|
||||
AuthProv -->> LangGraph: 6. Confirm validity
|
||||
|
||||
%% Fetch user tokens from secret store
|
||||
LangGraph ->> SecretStore: 6a. Fetch user tokens
|
||||
SecretStore -->> LangGraph: 6b. Return tokens
|
||||
|
||||
Note over LangGraph: 7. Apply access control (@auth.on.*)
|
||||
|
||||
%% MCP round-trip
|
||||
Note over LangGraph: 8. Build MCP client with user token
|
||||
LangGraph ->> MCPServer: 9. Call MCP tool (with header)
|
||||
Note over MCPServer: 10. MCP validates header and runs tool
|
||||
MCPServer -->> LangGraph: 11. Tool response
|
||||
|
||||
%% Return to caller
|
||||
LangGraph -->> ClientApp: 12. Return resources / tool output
|
||||
```
|
||||
|
||||
For more information, see [MCP endpoint in LangGraph Server](../concepts/server-mcp.md#use-user-scoped-mcp-tools-in-your-deployment).
|
||||
|
||||
@@ -166,7 +166,7 @@ network = builder.compile()
|
||||
|
||||
### Supervisor
|
||||
|
||||
In this architecture, we define agents as nodes and add a supervisor node (LLM) that decides which agent nodes should be called next. We use [`Command`](./low_level.md#command) to route execution to the appropriate agent node based on supervisor's decision. This architecture also lends itself well to running multiple agents in parallel or using [map-reduce](../how-tos/graph-api.ipynb#map-reduce-and-the-send-api) pattern.
|
||||
In this architecture, we define agents as nodes and add a supervisor node (LLM) that decides which agent nodes should be called next. We use [`Command`](./low_level.md#command) to route execution to the appropriate agent node based on supervisor's decision. This architecture also lends itself well to running multiple agents in parallel or using [map-reduce](../how-tos/graph-api.md#map-reduce-and-the-send-api) pattern.
|
||||
|
||||
```python
|
||||
from typing import Literal
|
||||
@@ -414,5 +414,5 @@ There are two high-level approaches to achieve that:
|
||||
|
||||
An agent might need to have a different state schema from the rest of the agents. For example, a search agent might only need to keep track of queries and retrieved documents. There are two ways to achieve this in LangGraph:
|
||||
|
||||
- Define [subgraph](./subgraphs.md) agents with a separate state schema. If there are no shared state keys (channels) between the subgraph and the parent graph, it’s important to [add input / output transformations](../how-tos/subgraph.ipynb#different-state-schemas) so that the parent graph knows how to communicate with the subgraphs.
|
||||
- Define agent node functions with a [private input state schema](../how-tos/graph-api.ipynb/#pass-private-state-between-nodes) that is distinct from the overall graph state schema. This allows passing information that is only needed for executing that particular agent.
|
||||
- Define [subgraph](./subgraphs.md) agents with a separate state schema. If there are no shared state keys (channels) between the subgraph and the parent graph, it’s important to [add input / output transformations](../how-tos/subgraph.md#different-state-schemas) so that the parent graph knows how to communicate with the subgraphs.
|
||||
- Define agent node functions with a [private input state schema](../how-tos/graph-api.md/#pass-private-state-between-nodes) that is distinct from the overall graph state schema. This allows passing information that is only needed for executing that particular agent.
|
||||
|
||||
@@ -8,8 +8,7 @@ hide:
|
||||
|
||||
# MCP endpoint in LangGraph Server
|
||||
|
||||
The **Model Context Protocol (MCP)** is an open protocol for describing tools and data sources in a model-agnostic format, enabling LLMs to discover
|
||||
and use them via a structured API.
|
||||
The [Model Context Protocol (MCP)](./mcp.md) is an open protocol for describing tools and data sources in a model-agnostic format, enabling LLMs to discover and use them via a structured API.
|
||||
|
||||
[LangGraph Server](./langgraph_server.md) implements MCP using the [Streamable HTTP transport](https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/transports/#streamable-http). This allows LangGraph **agents** to be exposed as **MCP tools**, making them usable with any MCP-compliant client supporting Streamable HTTP.
|
||||
|
||||
@@ -28,79 +27,6 @@ Install them with:
|
||||
pip install "langgraph-api>=0.2.3" "langgraph-sdk>=0.1.61"
|
||||
```
|
||||
|
||||
## Exposing an agent as MCP tool
|
||||
|
||||
|
||||
When deployed, your agent will appear as a tool in the MCP endpoint
|
||||
with this configuration:
|
||||
|
||||
- **Tool name**: The agent's name.
|
||||
- **Tool description**: The agent's description.
|
||||
- **Tool input schema**: The agent's input schema.
|
||||
|
||||
### Setting name and description
|
||||
|
||||
You can set the name and description of your agent in `langgraph.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"graphs": {
|
||||
"my_agent": {
|
||||
"path": "./my_agent/agent.py:graph",
|
||||
"description": "A description of what the agent does"
|
||||
}
|
||||
},
|
||||
"env": ".env"
|
||||
}
|
||||
```
|
||||
|
||||
After deployment, you can update the name and description using the LangGraph SDK.
|
||||
|
||||
### Schema
|
||||
|
||||
Define clear, minimal input and output schemas to avoid exposing unnecessary internal complexity to the LLM.
|
||||
|
||||
The default [MessagesState](./low_level.md#messagesstate) uses `AnyMessage`, which supports many message types but is too general for direct LLM exposure.
|
||||
|
||||
Instead, define **custom agents or workflows** that use explicitly typed input and output structures.
|
||||
|
||||
For example, a workflow answering documentation questions might look like this:
|
||||
|
||||
```python
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
# Define input schema
|
||||
class InputState(TypedDict):
|
||||
question: str
|
||||
|
||||
# Define output schema
|
||||
class OutputState(TypedDict):
|
||||
answer: str
|
||||
|
||||
# Combine input and output
|
||||
class OverallState(InputState, OutputState):
|
||||
pass
|
||||
|
||||
# Define the processing node
|
||||
def answer_node(state: InputState):
|
||||
# Replace with actual logic and do something useful
|
||||
return {"answer": "bye", "question": state["question"]}
|
||||
|
||||
# Build the graph with explicit schemas
|
||||
builder = StateGraph(OverallState, input_schema=InputState, output_schema=OutputState)
|
||||
builder.add_node(answer_node)
|
||||
builder.add_edge(START, "answer_node")
|
||||
builder.add_edge("answer_node", END)
|
||||
graph = builder.compile()
|
||||
|
||||
# Run the graph
|
||||
print(graph.invoke({"question": "hi"}))
|
||||
```
|
||||
|
||||
For more details, see the [low-level concepts guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#state).
|
||||
|
||||
|
||||
## Usage overview
|
||||
|
||||
To enable MCP:
|
||||
@@ -201,6 +127,109 @@ Use an MCP-compliant client to connect to the LangGraph server. The following ex
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Expose an agent as MCP tool
|
||||
|
||||
When deployed, your agent will appear as a tool in the MCP endpoint
|
||||
with this configuration:
|
||||
|
||||
- **Tool name**: The agent's name.
|
||||
- **Tool description**: The agent's description.
|
||||
- **Tool input schema**: The agent's input schema.
|
||||
|
||||
### Setting name and description
|
||||
|
||||
You can set the name and description of your agent in `langgraph.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"graphs": {
|
||||
"my_agent": {
|
||||
"path": "./my_agent/agent.py:graph",
|
||||
"description": "A description of what the agent does"
|
||||
}
|
||||
},
|
||||
"env": ".env"
|
||||
}
|
||||
```
|
||||
|
||||
After deployment, you can update the name and description using the LangGraph SDK.
|
||||
|
||||
### Schema
|
||||
|
||||
Define clear, minimal input and output schemas to avoid exposing unnecessary internal complexity to the LLM.
|
||||
|
||||
The default [MessagesState](./low_level.md#messagesstate) uses `AnyMessage`, which supports many message types but is too general for direct LLM exposure.
|
||||
|
||||
Instead, define **custom agents or workflows** that use explicitly typed input and output structures.
|
||||
|
||||
For example, a workflow answering documentation questions might look like this:
|
||||
|
||||
```python
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
# Define input schema
|
||||
class InputState(TypedDict):
|
||||
question: str
|
||||
|
||||
# Define output schema
|
||||
class OutputState(TypedDict):
|
||||
answer: str
|
||||
|
||||
# Combine input and output
|
||||
class OverallState(InputState, OutputState):
|
||||
pass
|
||||
|
||||
# Define the processing node
|
||||
def answer_node(state: InputState):
|
||||
# Replace with actual logic and do something useful
|
||||
return {"answer": "bye", "question": state["question"]}
|
||||
|
||||
# Build the graph with explicit schemas
|
||||
builder = StateGraph(OverallState, input_schema=InputState, output_schema=OutputState)
|
||||
builder.add_node(answer_node)
|
||||
builder.add_edge(START, "answer_node")
|
||||
builder.add_edge("answer_node", END)
|
||||
graph = builder.compile()
|
||||
|
||||
# Run the graph
|
||||
print(graph.invoke({"question": "hi"}))
|
||||
```
|
||||
|
||||
For more details, see the [low-level concepts guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#state).
|
||||
|
||||
## Use user-scoped MCP tools in your deployment
|
||||
|
||||
!!! tip "Prerequisites"
|
||||
|
||||
You have added your own [custom auth middleware](https://langchain-ai.github.io/langgraph/how-tos/auth/custom_auth/) that populates the `langgraph_auth_user` object, making it accessible through configurable context for every node in your graph.
|
||||
|
||||
To make user-scoped tools available to your LangGraph Platform deployment, start with implementing a snippet like the following:
|
||||
|
||||
```python
|
||||
from langchain_mcp_adapters.client import MultiServerMCPClient
|
||||
|
||||
def get_mcp_tools_node(state, config):
|
||||
user = config["configurable"].get("langgraph_auth_user")
|
||||
# e.g., user["github_token"], user["email"], etc.
|
||||
|
||||
client = MultiServerMCPClient({
|
||||
"github": {
|
||||
"transport": "streamable_http", # (1)
|
||||
"url": "https://my-github-mcp-server/mcp", # (2)
|
||||
"headers": {
|
||||
"Authorization": f"Bearer {user['github_token']}"
|
||||
}
|
||||
}
|
||||
})
|
||||
tools = await client.get_tools() # (3)
|
||||
return {"tools": tools}
|
||||
|
||||
```
|
||||
|
||||
1. MCP only supports adding headers to requests made to `streamable_http` and `sse` `transport` servers.
|
||||
2. Your MCP server URL.
|
||||
3. Get available tools from your MCP server.
|
||||
|
||||
## Session behavior
|
||||
|
||||
@@ -210,7 +239,7 @@ The current LangGraph MCP implementation does not support sessions. Each `/mcp`
|
||||
|
||||
The `/mcp` endpoint uses the same authentication as the rest of the LangGraph API. Refer to the [authentication guide](./auth.md) for setup details.
|
||||
|
||||
## Disabling MCP
|
||||
## Disable MCP
|
||||
|
||||
To disable the MCP endpoint, set `disable_mcp` to `true` in your `langgraph.json` configuration file:
|
||||
|
||||
@@ -222,4 +251,4 @@ To disable the MCP endpoint, set `disable_mcp` to `true` in your `langgraph.json
|
||||
}
|
||||
```
|
||||
|
||||
This will prevent the server from exposing the `/mcp` endpoint.
|
||||
This will prevent the server from exposing the `/mcp` endpoint.
|
||||
|
||||
@@ -12,7 +12,7 @@ Some reasons for using subgraphs are:
|
||||
|
||||
The main question when adding subgraphs is how the parent graph and subgraph communicate, i.e. how they pass the [state](./low_level.md#state) between each other during the graph execution. There are two scenarios:
|
||||
|
||||
* parent and subgraph have **shared state keys** in their state [schemas](./low_level.md#state). In this case, you can [include the subgraph as a node in the parent graph](../how-tos/subgraph.ipynb#shared-state-schemas)
|
||||
* parent and subgraph have **shared state keys** in their state [schemas](./low_level.md#state). In this case, you can [include the subgraph as a node in the parent graph](../how-tos/subgraph.md#shared-state-schemas)
|
||||
|
||||
```python
|
||||
from langgraph.graph import StateGraph, MessagesState, START
|
||||
@@ -40,7 +40,7 @@ The main question when adding subgraphs is how the parent graph and subgraph com
|
||||
graph.invoke({"messages": [{"role": "user", "content": "hi!"}]})
|
||||
```
|
||||
|
||||
* parent graph and subgraph have **different schemas** (no shared state keys in their state [schemas](./low_level.md#state)). In this case, you have to [call the subgraph from inside a node in the parent graph](../how-tos/subgraph.ipynb#different-state-schemas): this is useful when the parent graph and the subgraph have different state schemas and you need to transform state before or after calling the subgraph
|
||||
* parent graph and subgraph have **different schemas** (no shared state keys in their state [schemas](./low_level.md#state)). In this case, you have to [call the subgraph from inside a node in the parent graph](../how-tos/subgraph.md#different-state-schemas): this is useful when the parent graph and the subgraph have different state schemas and you need to transform state before or after calling the subgraph
|
||||
|
||||
```python
|
||||
from typing_extensions import TypedDict, Annotated
|
||||
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
@@ -1,138 +1,147 @@
|
||||
# Add custom authentication
|
||||
|
||||
!!! tip "Prerequisites"
|
||||
|
||||
This guide assumes familiarity with the following concepts:
|
||||
|
||||
* [**Authentication & Access Control**](../../concepts/auth.md)
|
||||
* [**LangGraph Platform**](../../concepts/langgraph_platform.md)
|
||||
|
||||
For a more guided walkthrough, see [**setting up custom authentication**](../../tutorials/auth/getting_started.md) tutorial.
|
||||
|
||||
???+ note "Support by deployment type"
|
||||
|
||||
Custom auth is supported for all deployments in the **managed LangGraph Platform**, as well as **Enterprise** self-hosted plans. It is not supported for **Lite** self-hosted plans.
|
||||
|
||||
This guide shows how to add custom authentication to your LangGraph Platform application. This guide applies to both LangGraph Platform and self-hosted deployments. It does not apply to isolated usage of the LangGraph open source library in your own custom server.
|
||||
|
||||
## 1. Implement authentication
|
||||
!!! note
|
||||
|
||||
Custom auth is supported for all **managed LangGraph Platform** deployments, as well as **Enterprise** self-hosted plans. It is not supported for **Lite** self-hosted plans.
|
||||
|
||||
## Add custom authentication to your deployment
|
||||
|
||||
To leverage custom authentication and access user-level metadata in your deployments, set up custom authentication to automatically populate the `config["configurable"]["langgraph_auth_user"]` object through a custom authentication handler. You can then access this object in your graph with the `langgraph_auth_user` key to [allow an agent to perform authenticated actions on behalf of the user](#enable-agent-authentication).
|
||||
|
||||
1. Implement authentication:
|
||||
|
||||
!!! note
|
||||
|
||||
Without a custom `@auth.authenticate` handler, LangGraph sees only the API-key owner (usually the developer), so requests aren’t scoped to individual end-users. To propagate custom tokens, you must implement your own handler.
|
||||
|
||||
```python
|
||||
from langgraph_sdk import Auth
|
||||
import requests
|
||||
|
||||
auth = Auth()
|
||||
|
||||
def is_valid_key(api_key: str) -> bool:
|
||||
is_valid = # your API key validation logic
|
||||
return is_valid
|
||||
|
||||
@auth.authenticate # (1)!
|
||||
async def authenticate(headers: dict) -> Auth.types.MinimalUserDict:
|
||||
api_key = headers.get("x-api-key")
|
||||
if not api_key or not is_valid_key(api_key):
|
||||
raise Auth.exceptions.HTTPException(status_code=401, detail="Invalid API key")
|
||||
|
||||
# Fetch user-specific tokens from your secret store
|
||||
user_tokens = await fetch_user_tokens(api_key)
|
||||
|
||||
return { # (2)!
|
||||
"identity": api_key, # fetch user ID from LangSmith
|
||||
"github_token" : user_tokens.github_token
|
||||
"jira_token" : user_tokens.jira_token
|
||||
# ... custom fields/secrets here
|
||||
}
|
||||
```
|
||||
|
||||
1. This handler receives the request (headers, etc.), validates the user, and returns a dictionary with at least an identity field.
|
||||
2. You can add any custom fields you want (e.g., OAuth tokens, roles, org IDs, etc.).
|
||||
|
||||
2. In your `langgraph.json`, add the path to your auth file:
|
||||
|
||||
```json hl_lines="7-9"
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"agent": "./agent.py:graph"
|
||||
},
|
||||
"env": ".env",
|
||||
"auth": {
|
||||
"path": "./auth.py:my_auth"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Once you've set up authentication in your server, requests must include the required authorization information based on your chosen scheme. Assuming you are using JWT token authentication, you could access your deployments using any of the following methods:
|
||||
|
||||
=== "Python Client"
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
my_token = "your-token" # In practice, you would generate a signed token with your auth provider
|
||||
client = get_client(
|
||||
url="http://localhost:2024",
|
||||
headers={"Authorization": f"Bearer {my_token}"}
|
||||
)
|
||||
threads = await client.threads.search()
|
||||
```
|
||||
|
||||
=== "Python RemoteGraph"
|
||||
|
||||
```python
|
||||
from langgraph.pregel.remote import RemoteGraph
|
||||
|
||||
my_token = "your-token" # In practice, you would generate a signed token with your auth provider
|
||||
remote_graph = RemoteGraph(
|
||||
"agent",
|
||||
url="http://localhost:2024",
|
||||
headers={"Authorization": f"Bearer {my_token}"}
|
||||
)
|
||||
threads = await remote_graph.ainvoke(...)
|
||||
```
|
||||
|
||||
=== "JavaScript Client"
|
||||
|
||||
```javascript
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const my_token = "your-token"; // In practice, you would generate a signed token with your auth provider
|
||||
const client = new Client({
|
||||
apiUrl: "http://localhost:2024",
|
||||
defaultHeaders: { Authorization: `Bearer ${my_token}` },
|
||||
});
|
||||
const threads = await client.threads.search();
|
||||
```
|
||||
|
||||
=== "JavaScript RemoteGraph"
|
||||
|
||||
```javascript
|
||||
import { RemoteGraph } from "@langchain/langgraph/remote";
|
||||
|
||||
const my_token = "your-token"; // In practice, you would generate a signed token with your auth provider
|
||||
const remoteGraph = new RemoteGraph({
|
||||
graphId: "agent",
|
||||
url: "http://localhost:2024",
|
||||
headers: { Authorization: `Bearer ${my_token}` },
|
||||
});
|
||||
const threads = await remoteGraph.invoke(...);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer ${your-token}" http://localhost:2024/threads
|
||||
```
|
||||
|
||||
## Enable agent authentication
|
||||
|
||||
After [authentication](#add-custom-authentication-to-your-deployment), the platform creates a special configuration object (`config`) that is passed to LangGraph Platform deployment. This object contains information about the current user, including any custom fields you return from your `@auth.authenticate` handler.
|
||||
|
||||
To allow an agent to perform authenticated actions on behalf of the user, access this object in your graph with the `langgraph_auth_user` key:
|
||||
|
||||
```python
|
||||
from langgraph_sdk import Auth
|
||||
|
||||
my_auth = Auth()
|
||||
|
||||
@my_auth.authenticate
|
||||
async def authenticate(authorization: str) -> str:
|
||||
token = authorization.split(" ", 1)[-1] # "Bearer <token>"
|
||||
try:
|
||||
# Verify token with your auth provider
|
||||
user_id = await verify_token(token)
|
||||
return user_id
|
||||
except Exception:
|
||||
raise Auth.exceptions.HTTPException(
|
||||
status_code=401,
|
||||
detail="Invalid token"
|
||||
)
|
||||
|
||||
# Add authorization rules to actually control access to resources
|
||||
@my_auth.on
|
||||
async def add_owner(
|
||||
ctx: Auth.types.AuthContext,
|
||||
value: dict,
|
||||
):
|
||||
"""Add owner to resource metadata and filter by owner."""
|
||||
filters = {"owner": ctx.user.identity}
|
||||
metadata = value.setdefault("metadata", {})
|
||||
metadata.update(filters)
|
||||
return filters
|
||||
|
||||
# Assumes you organize information in store like (user_id, resource_type, resource_id)
|
||||
@my_auth.on.store()
|
||||
async def authorize_store(ctx: Auth.types.AuthContext, value: dict):
|
||||
namespace: tuple = value["namespace"]
|
||||
assert namespace[0] == ctx.user.identity, "Not authorized"
|
||||
|
||||
def my_node(state, config):
|
||||
user_config = config["configurable"].get("langgraph_auth_user")
|
||||
# token was resolved during the @auth.authenticate function
|
||||
token = user_config.get("github_token","")
|
||||
...
|
||||
```
|
||||
|
||||
## 2. Update configuration
|
||||
!!! note
|
||||
Fetch user credentials from a secure secret store. Storing secrets in graph state is not recommended.
|
||||
|
||||
In your `langgraph.json`, add the path to your auth file:
|
||||
## Learn more
|
||||
|
||||
```json hl_lines="7-9"
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"agent": "./agent.py:graph"
|
||||
},
|
||||
"env": ".env",
|
||||
"auth": {
|
||||
"path": "./auth.py:my_auth"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Connect from the client
|
||||
|
||||
Once you've set up authentication in your server, requests must include the required authorization information based on your chosen scheme.
|
||||
Assuming you are using JWT token authentication, you could access your deployments using any of the following methods:
|
||||
|
||||
=== "Python Client"
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
my_token = "your-token" # In practice, you would generate a signed token with your auth provider
|
||||
client = get_client(
|
||||
url="http://localhost:2024",
|
||||
headers={"Authorization": f"Bearer {my_token}"}
|
||||
)
|
||||
threads = await client.threads.search()
|
||||
```
|
||||
|
||||
=== "Python RemoteGraph"
|
||||
|
||||
```python
|
||||
from langgraph.pregel.remote import RemoteGraph
|
||||
|
||||
my_token = "your-token" # In practice, you would generate a signed token with your auth provider
|
||||
remote_graph = RemoteGraph(
|
||||
"agent",
|
||||
url="http://localhost:2024",
|
||||
headers={"Authorization": f"Bearer {my_token}"}
|
||||
)
|
||||
threads = await remote_graph.ainvoke(...)
|
||||
```
|
||||
|
||||
=== "JavaScript Client"
|
||||
|
||||
```javascript
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const my_token = "your-token"; // In practice, you would generate a signed token with your auth provider
|
||||
const client = new Client({
|
||||
apiUrl: "http://localhost:2024",
|
||||
defaultHeaders: { Authorization: `Bearer ${my_token}` },
|
||||
});
|
||||
const threads = await client.threads.search();
|
||||
```
|
||||
|
||||
=== "JavaScript RemoteGraph"
|
||||
|
||||
```javascript
|
||||
import { RemoteGraph } from "@langchain/langgraph/remote";
|
||||
|
||||
const my_token = "your-token"; // In practice, you would generate a signed token with your auth provider
|
||||
const remoteGraph = new RemoteGraph({
|
||||
graphId: "agent",
|
||||
url: "http://localhost:2024",
|
||||
headers: { Authorization: `Bearer ${my_token}` },
|
||||
});
|
||||
const threads = await remoteGraph.invoke(...);
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer ${your-token}" http://localhost:2024/threads
|
||||
```
|
||||
* [Authentication & Access Control](../../concepts/auth.md)
|
||||
* [LangGraph Platform](../../concepts/langgraph_platform.md)
|
||||
* [Setting up custom authentication tutorial](../../tutorials/auth/getting_started.md)
|
||||
|
||||
@@ -0,0 +1,580 @@
|
||||
# Build multi-agent systems
|
||||
|
||||
A single agent might struggle if it needs to specialize in multiple domains or manage many tools. To tackle this, you can break your agent into smaller, independent agents and composing them into a [multi-agent system](../concepts/multi_agent.md).
|
||||
|
||||
In multi-agent systems, agents need to communicate between each other. They do so via [handoffs](#handoffs) — a primitive that describes which agent to hand control to and the payload to send to that agent.
|
||||
|
||||
This guide covers the following:
|
||||
|
||||
* implementing [handoffs](#handoffs) between agents
|
||||
* using handoffs and the prebuilt [agent](../agents/agents.md) to [build a custom multi-agent system](#build-a-multi-agent-system)
|
||||
|
||||
To get started with building multi-agent systems, check out LangGraph [prebuilt implementations](#prebuilt-implementations) of two of the most popular multi-agent architectures — [supervisor](../agents/multi-agent.md#supervisor) and [swarm](../agents/multi-agent.md#swarm).
|
||||
|
||||
## Handoffs
|
||||
|
||||
To set up communication between the agents in a multi-agent system you can use [**handoffs**](../concepts/multi_agent.md#handoffs) — a pattern where one agent *hands off* control to another. Handoffs allow you to specify:
|
||||
|
||||
- **destination**: target agent to navigate to (e.g., name of the LangGraph node to go to)
|
||||
- **payload**: information to pass to that agent (e.g., state update)
|
||||
|
||||
### Create handoffs
|
||||
|
||||
To implement handoffs, you can return `Command` objects from your agent nodes or tools:
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
from langchain_core.tools import tool, InjectedToolCallId
|
||||
from langgraph.prebuilt import create_react_agent, InjectedState
|
||||
from langgraph.graph import StateGraph, START, MessagesState
|
||||
from langgraph.types import Command
|
||||
|
||||
def create_handoff_tool(*, agent_name: str, description: str | None = None):
|
||||
name = f"transfer_to_{agent_name}"
|
||||
description = description or f"Transfer to {agent_name}"
|
||||
|
||||
@tool(name, description=description)
|
||||
def handoff_tool(
|
||||
# highlight-next-line
|
||||
state: Annotated[MessagesState, InjectedState], # (1)!
|
||||
# highlight-next-line
|
||||
tool_call_id: Annotated[str, InjectedToolCallId],
|
||||
) -> Command:
|
||||
tool_message = {
|
||||
"role": "tool",
|
||||
"content": f"Successfully transferred to {agent_name}",
|
||||
"name": name,
|
||||
"tool_call_id": tool_call_id,
|
||||
}
|
||||
return Command( # (2)!
|
||||
# highlight-next-line
|
||||
goto=agent_name, # (3)!
|
||||
# highlight-next-line
|
||||
update={"messages": state["messages"] + [tool_message]}, # (4)!
|
||||
# highlight-next-line
|
||||
graph=Command.PARENT, # (5)!
|
||||
)
|
||||
return handoff_tool
|
||||
```
|
||||
|
||||
1. Access the [state](../concepts/low_level.md#state) of the agent that is calling the handoff tool using the [InjectedState][langgraph.prebuilt.InjectedState] annotation.
|
||||
2. The `Command` primitive allows specifying a state update and a node transition as a single operation, making it useful for implementing handoffs.
|
||||
3. Name of the agent or node to hand off to.
|
||||
4. Take the agent's messages and **add** them to the parent's **state** as part of the handoff. The next agent will see the parent state.
|
||||
5. Indicate to LangGraph that we need to navigate to agent node in a **parent** multi-agent graph.
|
||||
|
||||
!!! tip
|
||||
|
||||
If you want to use tools that return `Command`, you can either use prebuilt [`create_react_agent`][langgraph.prebuilt.chat_agent_executor.create_react_agent] / [`ToolNode`][langgraph.prebuilt.tool_node.ToolNode] components, or implement your own tool-executing node that collects `Command` objects returned by the tools and returns a list of them, e.g.:
|
||||
|
||||
```python
|
||||
def call_tools(state):
|
||||
...
|
||||
commands = [tools_by_name[tool_call["name"]].invoke(tool_call) for tool_call in tool_calls]
|
||||
return commands
|
||||
```
|
||||
|
||||
!!! Important
|
||||
|
||||
This handoff implementation assumes that:
|
||||
|
||||
- each agent receives overall message history (across all agents) in the multi-agent system as its input. If you want more control over agent inputs, see [this section](#control-agent-inputs)
|
||||
- each agent outputs its internal messages history to the overall message history of the multi-agent system. If you want more control over **how agent outputs are added**, wrap the agent in a separate node function:
|
||||
|
||||
```python
|
||||
def call_hotel_assistant(state):
|
||||
# return agent's final response,
|
||||
# excluding inner monologue
|
||||
response = hotel_assistant.invoke(state)
|
||||
# highlight-next-line
|
||||
return {"messages": response["messages"][-1]}
|
||||
```
|
||||
|
||||
### Control agent inputs
|
||||
|
||||
You can use the [`Send()`][langgraph.types.Send] primitive to directly send data to the worker agents during the handoff. For example, you can request that the calling agent populate a task description for the next agent:
|
||||
|
||||
```python
|
||||
|
||||
from typing import Annotated
|
||||
from langchain_core.tools import tool, InjectedToolCallId
|
||||
from langgraph.prebuilt import InjectedState
|
||||
from langgraph.graph import StateGraph, START, MessagesState
|
||||
# highlight-next-line
|
||||
from langgraph.types import Command, Send
|
||||
|
||||
def create_task_description_handoff_tool(
|
||||
*, agent_name: str, description: str | None = None
|
||||
):
|
||||
name = f"transfer_to_{agent_name}"
|
||||
description = description or f"Ask {agent_name} for help."
|
||||
|
||||
@tool(name, description=description)
|
||||
def handoff_tool(
|
||||
# this is populated by the calling agent
|
||||
task_description: Annotated[
|
||||
str,
|
||||
"Description of what the next agent should do, including all of the relevant context.",
|
||||
],
|
||||
# these parameters are ignored by the LLM
|
||||
state: Annotated[MessagesState, InjectedState],
|
||||
) -> Command:
|
||||
task_description_message = {"role": "user", "content": task_description}
|
||||
agent_input = {**state, "messages": [task_description_message]}
|
||||
return Command(
|
||||
# highlight-next-line
|
||||
goto=[Send(agent_name, agent_input)],
|
||||
graph=Command.PARENT,
|
||||
)
|
||||
|
||||
return handoff_tool
|
||||
```
|
||||
|
||||
See the multi-agent [supervisor](../tutorials/multi_agent/agent_supervisor.ipynb#4-create-delegation-tasks) example for a full example of using [`Send()`][langgraph.types.Send] in handoffs.
|
||||
|
||||
## Build a multi-agent system
|
||||
|
||||
You can use handoffs in any agents built with LangGraph. We recommend using the prebuilt [agent](../agents/overview.md) or [`ToolNode`](./tool-calling.md#toolnode), as they natively support handoffs tools returning `Command`. Below is an example of how you can implement a multi-agent system for booking travel using handoffs:
|
||||
|
||||
```python
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
from langgraph.graph import StateGraph, START, MessagesState
|
||||
|
||||
def create_handoff_tool(*, agent_name: str, description: str | None = None):
|
||||
# same implementation as above
|
||||
...
|
||||
return Command(...)
|
||||
|
||||
# Handoffs
|
||||
transfer_to_hotel_assistant = create_handoff_tool(agent_name="hotel_assistant")
|
||||
transfer_to_flight_assistant = create_handoff_tool(agent_name="flight_assistant")
|
||||
|
||||
# Define agents
|
||||
flight_assistant = create_react_agent(
|
||||
model="anthropic:claude-3-5-sonnet-latest",
|
||||
# highlight-next-line
|
||||
tools=[..., transfer_to_hotel_assistant],
|
||||
# highlight-next-line
|
||||
name="flight_assistant"
|
||||
)
|
||||
hotel_assistant = create_react_agent(
|
||||
model="anthropic:claude-3-5-sonnet-latest",
|
||||
# highlight-next-line
|
||||
tools=[..., transfer_to_flight_assistant],
|
||||
# highlight-next-line
|
||||
name="hotel_assistant"
|
||||
)
|
||||
|
||||
# Define multi-agent graph
|
||||
multi_agent_graph = (
|
||||
StateGraph(MessagesState)
|
||||
# highlight-next-line
|
||||
.add_node(flight_assistant)
|
||||
# highlight-next-line
|
||||
.add_node(hotel_assistant)
|
||||
.add_edge(START, "flight_assistant")
|
||||
.compile()
|
||||
)
|
||||
```
|
||||
|
||||
??? example "Full example: Multi-agent system for booking travel"
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
from langchain_core.messages import convert_to_messages
|
||||
from langchain_core.tools import tool, InjectedToolCallId
|
||||
from langgraph.prebuilt import create_react_agent, InjectedState
|
||||
from langgraph.graph import StateGraph, START, MessagesState
|
||||
from langgraph.types import Command
|
||||
|
||||
# We'll use `pretty_print_messages` helper to render the streamed agent outputs nicely
|
||||
|
||||
def pretty_print_message(message, indent=False):
|
||||
pretty_message = message.pretty_repr(html=True)
|
||||
if not indent:
|
||||
print(pretty_message)
|
||||
return
|
||||
|
||||
indented = "\n".join("\t" + c for c in pretty_message.split("\n"))
|
||||
print(indented)
|
||||
|
||||
|
||||
def pretty_print_messages(update, last_message=False):
|
||||
is_subgraph = False
|
||||
if isinstance(update, tuple):
|
||||
ns, update = update
|
||||
# skip parent graph updates in the printouts
|
||||
if len(ns) == 0:
|
||||
return
|
||||
|
||||
graph_id = ns[-1].split(":")[0]
|
||||
print(f"Update from subgraph {graph_id}:")
|
||||
print("\n")
|
||||
is_subgraph = True
|
||||
|
||||
for node_name, node_update in update.items():
|
||||
update_label = f"Update from node {node_name}:"
|
||||
if is_subgraph:
|
||||
update_label = "\t" + update_label
|
||||
|
||||
print(update_label)
|
||||
print("\n")
|
||||
|
||||
messages = convert_to_messages(node_update["messages"])
|
||||
if last_message:
|
||||
messages = messages[-1:]
|
||||
|
||||
for m in messages:
|
||||
pretty_print_message(m, indent=is_subgraph)
|
||||
print("\n")
|
||||
|
||||
|
||||
def create_handoff_tool(*, agent_name: str, description: str | None = None):
|
||||
name = f"transfer_to_{agent_name}"
|
||||
description = description or f"Transfer to {agent_name}"
|
||||
|
||||
@tool(name, description=description)
|
||||
def handoff_tool(
|
||||
# highlight-next-line
|
||||
state: Annotated[MessagesState, InjectedState], # (1)!
|
||||
# highlight-next-line
|
||||
tool_call_id: Annotated[str, InjectedToolCallId],
|
||||
) -> Command:
|
||||
tool_message = {
|
||||
"role": "tool",
|
||||
"content": f"Successfully transferred to {agent_name}",
|
||||
"name": name,
|
||||
"tool_call_id": tool_call_id,
|
||||
}
|
||||
return Command( # (2)!
|
||||
# highlight-next-line
|
||||
goto=agent_name, # (3)!
|
||||
# highlight-next-line
|
||||
update={"messages": state["messages"] + [tool_message]}, # (4)!
|
||||
# highlight-next-line
|
||||
graph=Command.PARENT, # (5)!
|
||||
)
|
||||
return handoff_tool
|
||||
|
||||
# Handoffs
|
||||
transfer_to_hotel_assistant = create_handoff_tool(
|
||||
agent_name="hotel_assistant",
|
||||
description="Transfer user to the hotel-booking assistant.",
|
||||
)
|
||||
transfer_to_flight_assistant = create_handoff_tool(
|
||||
agent_name="flight_assistant",
|
||||
description="Transfer user to the flight-booking assistant.",
|
||||
)
|
||||
|
||||
# Simple agent tools
|
||||
def book_hotel(hotel_name: str):
|
||||
"""Book a hotel"""
|
||||
return f"Successfully booked a stay at {hotel_name}."
|
||||
|
||||
def book_flight(from_airport: str, to_airport: str):
|
||||
"""Book a flight"""
|
||||
return f"Successfully booked a flight from {from_airport} to {to_airport}."
|
||||
|
||||
# Define agents
|
||||
flight_assistant = create_react_agent(
|
||||
model="anthropic:claude-3-5-sonnet-latest",
|
||||
# highlight-next-line
|
||||
tools=[book_flight, transfer_to_hotel_assistant],
|
||||
prompt="You are a flight booking assistant",
|
||||
# highlight-next-line
|
||||
name="flight_assistant"
|
||||
)
|
||||
hotel_assistant = create_react_agent(
|
||||
model="anthropic:claude-3-5-sonnet-latest",
|
||||
# highlight-next-line
|
||||
tools=[book_hotel, transfer_to_flight_assistant],
|
||||
prompt="You are a hotel booking assistant",
|
||||
# highlight-next-line
|
||||
name="hotel_assistant"
|
||||
)
|
||||
|
||||
# Define multi-agent graph
|
||||
multi_agent_graph = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node(flight_assistant)
|
||||
.add_node(hotel_assistant)
|
||||
.add_edge(START, "flight_assistant")
|
||||
.compile()
|
||||
)
|
||||
|
||||
# Run the multi-agent graph
|
||||
for chunk in multi_agent_graph.stream(
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "book a flight from BOS to JFK and a stay at McKittrick Hotel"
|
||||
}
|
||||
]
|
||||
},
|
||||
# highlight-next-line
|
||||
subgraphs=True
|
||||
):
|
||||
pretty_print_messages(chunk)
|
||||
```
|
||||
|
||||
1. Access agent's state
|
||||
2. The `Command` primitive allows specifying a state update and a node transition as a single operation, making it useful for implementing handoffs.
|
||||
3. Name of the agent or node to hand off to.
|
||||
4. Take the agent's messages and **add** them to the parent's **state** as part of the handoff. The next agent will see the parent state.
|
||||
5. Indicate to LangGraph that we need to navigate to agent node in a **parent** multi-agent graph.
|
||||
|
||||
## Multi-turn conversation
|
||||
|
||||
Users might want to engage in a *multi-turn conversation* with one or more agents. To build a system that can handle this, you can create a node that uses an [`interrupt`][langgraph.types.interrupt] to collect user input and routes back to the **active** agent.
|
||||
|
||||
The agents can then be implemented as nodes in a graph that executes agent steps and determines the next action:
|
||||
|
||||
1. **Wait for user input** to continue the conversation, or
|
||||
2. **Route to another agent** (or back to itself, such as in a loop) via a [handoff](#handoffs)
|
||||
|
||||
```python
|
||||
def human(state) -> Command[Literal["agent", "another_agent"]]:
|
||||
"""A node for collecting user input."""
|
||||
user_input = interrupt(value="Ready for user input.")
|
||||
|
||||
# Determine the active agent.
|
||||
active_agent = ...
|
||||
|
||||
...
|
||||
return Command(
|
||||
update={
|
||||
"messages": [{
|
||||
"role": "human",
|
||||
"content": user_input,
|
||||
}]
|
||||
},
|
||||
goto=active_agent
|
||||
)
|
||||
|
||||
def agent(state) -> Command[Literal["agent", "another_agent", "human"]]:
|
||||
# The condition for routing/halting can be anything, e.g. LLM tool call / structured output, etc.
|
||||
goto = get_next_agent(...) # 'agent' / 'another_agent'
|
||||
if goto:
|
||||
return Command(goto=goto, update={"my_state_key": "my_state_value"})
|
||||
else:
|
||||
return Command(goto="human") # Go to human node
|
||||
```
|
||||
|
||||
??? example "Full example: multi-agent system for travel recommendations"
|
||||
|
||||
In this example, we will build a team of travel assistant agents that can communicate with each other via handoffs.
|
||||
|
||||
We will create 2 agents:
|
||||
|
||||
* travel_advisor: can help with travel destination recommendations. Can ask hotel_advisor for help.
|
||||
* hotel_advisor: can help with hotel recommendations. Can ask travel_advisor for help.
|
||||
|
||||
```python
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langgraph.graph import MessagesState, StateGraph, START
|
||||
from langgraph.prebuilt import create_react_agent, InjectedState
|
||||
from langgraph.types import Command, interrupt
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
|
||||
model = ChatAnthropic(model="claude-3-5-sonnet-latest")
|
||||
|
||||
class MultiAgentState(MessagesState):
|
||||
last_active_agent: str
|
||||
|
||||
|
||||
# Define travel advisor tools and ReAct agent
|
||||
travel_advisor_tools = [
|
||||
get_travel_recommendations,
|
||||
make_handoff_tool(agent_name="hotel_advisor"),
|
||||
]
|
||||
travel_advisor = create_react_agent(
|
||||
model,
|
||||
travel_advisor_tools,
|
||||
prompt=(
|
||||
"You are a general travel expert that can recommend travel destinations (e.g. countries, cities, etc). "
|
||||
"If you need hotel recommendations, ask 'hotel_advisor' for help. "
|
||||
"You MUST include human-readable response before transferring to another agent."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def call_travel_advisor(
|
||||
state: MultiAgentState,
|
||||
) -> Command[Literal["hotel_advisor", "human"]]:
|
||||
# You can also add additional logic like changing the input to the agent / output from the agent, etc.
|
||||
# NOTE: we're invoking the ReAct agent with the full history of messages in the state
|
||||
response = travel_advisor.invoke(state)
|
||||
update = {**response, "last_active_agent": "travel_advisor"}
|
||||
return Command(update=update, goto="human")
|
||||
|
||||
|
||||
# Define hotel advisor tools and ReAct agent
|
||||
hotel_advisor_tools = [
|
||||
get_hotel_recommendations,
|
||||
make_handoff_tool(agent_name="travel_advisor"),
|
||||
]
|
||||
hotel_advisor = create_react_agent(
|
||||
model,
|
||||
hotel_advisor_tools,
|
||||
prompt=(
|
||||
"You are a hotel expert that can provide hotel recommendations for a given destination. "
|
||||
"If you need help picking travel destinations, ask 'travel_advisor' for help."
|
||||
"You MUST include human-readable response before transferring to another agent."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def call_hotel_advisor(
|
||||
state: MultiAgentState,
|
||||
) -> Command[Literal["travel_advisor", "human"]]:
|
||||
response = hotel_advisor.invoke(state)
|
||||
update = {**response, "last_active_agent": "hotel_advisor"}
|
||||
return Command(update=update, goto="human")
|
||||
|
||||
|
||||
def human_node(
|
||||
state: MultiAgentState, config
|
||||
) -> Command[Literal["hotel_advisor", "travel_advisor", "human"]]:
|
||||
"""A node for collecting user input."""
|
||||
|
||||
user_input = interrupt(value="Ready for user input.")
|
||||
active_agent = state["last_active_agent"]
|
||||
|
||||
return Command(
|
||||
update={
|
||||
"messages": [
|
||||
{
|
||||
"role": "human",
|
||||
"content": user_input,
|
||||
}
|
||||
]
|
||||
},
|
||||
goto=active_agent,
|
||||
)
|
||||
|
||||
|
||||
builder = StateGraph(MultiAgentState)
|
||||
builder.add_node("travel_advisor", call_travel_advisor)
|
||||
builder.add_node("hotel_advisor", call_hotel_advisor)
|
||||
|
||||
# This adds a node to collect human input, which will route
|
||||
# back to the active agent.
|
||||
builder.add_node("human", human_node)
|
||||
|
||||
# We'll always start with a general travel advisor.
|
||||
builder.add_edge(START, "travel_advisor")
|
||||
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
```
|
||||
|
||||
Let's test a multi turn conversation with this application.
|
||||
|
||||
```python
|
||||
import uuid
|
||||
|
||||
thread_config = {"configurable": {"thread_id": str(uuid.uuid4())}}
|
||||
|
||||
inputs = [
|
||||
# 1st round of conversation,
|
||||
{
|
||||
"messages": [
|
||||
{"role": "user", "content": "i wanna go somewhere warm in the caribbean"}
|
||||
]
|
||||
},
|
||||
# Since we're using `interrupt`, we'll need to resume using the Command primitive.
|
||||
# 2nd round of conversation,
|
||||
Command(
|
||||
resume="could you recommend a nice hotel in one of the areas and tell me which area it is."
|
||||
),
|
||||
# 3rd round of conversation,
|
||||
Command(
|
||||
resume="i like the first one. could you recommend something to do near the hotel?"
|
||||
),
|
||||
]
|
||||
|
||||
for idx, user_input in enumerate(inputs):
|
||||
print()
|
||||
print(f"--- Conversation Turn {idx + 1} ---")
|
||||
print()
|
||||
print(f"User: {user_input}")
|
||||
print()
|
||||
for update in graph.stream(
|
||||
user_input,
|
||||
config=thread_config,
|
||||
stream_mode="updates",
|
||||
):
|
||||
for node_id, value in update.items():
|
||||
if isinstance(value, dict) and value.get("messages", []):
|
||||
last_message = value["messages"][-1]
|
||||
if isinstance(last_message, dict) or last_message.type != "ai":
|
||||
continue
|
||||
print(f"{node_id}: {last_message.content}")
|
||||
```
|
||||
|
||||
```
|
||||
--- Conversation Turn 1 ---
|
||||
|
||||
User: {'messages': [{'role': 'user', 'content': 'i wanna go somewhere warm in the caribbean'}]}
|
||||
|
||||
travel_advisor: Based on the recommendations, Aruba would be an excellent choice for your Caribbean getaway! Aruba is known as "One Happy Island" and offers:
|
||||
- Year-round warm weather with consistent temperatures around 82°F (28°C)
|
||||
- Beautiful white sand beaches like Eagle Beach and Palm Beach
|
||||
- Clear turquoise waters perfect for swimming and snorkeling
|
||||
- Minimal rainfall and location outside the hurricane belt
|
||||
- A blend of Caribbean and Dutch culture
|
||||
- Great dining options and nightlife
|
||||
- Various water sports and activities
|
||||
|
||||
Would you like me to get some specific hotel recommendations in Aruba for your stay? I can transfer you to our hotel advisor who can help with accommodations.
|
||||
|
||||
--- Conversation Turn 2 ---
|
||||
|
||||
User: Command(resume='could you recommend a nice hotel in one of the areas and tell me which area it is.')
|
||||
|
||||
hotel_advisor: Based on the recommendations, I can suggest two excellent options:
|
||||
|
||||
1. The Ritz-Carlton, Aruba - Located in Palm Beach
|
||||
- This luxury resort is situated in the vibrant Palm Beach area
|
||||
- Known for its exceptional service and amenities
|
||||
- Perfect if you want to be close to dining, shopping, and entertainment
|
||||
- Features multiple restaurants, a casino, and a world-class spa
|
||||
- Located on a pristine stretch of Palm Beach
|
||||
|
||||
2. Bucuti & Tara Beach Resort - Located in Eagle Beach
|
||||
- An adults-only boutique resort on Eagle Beach
|
||||
- Known for being more intimate and peaceful
|
||||
- Award-winning for its sustainability practices
|
||||
- Perfect for a romantic getaway or peaceful vacation
|
||||
- Located on one of the most beautiful beaches in the Caribbean
|
||||
|
||||
Would you like more specific information about either of these properties or their locations?
|
||||
|
||||
--- Conversation Turn 3 ---
|
||||
|
||||
User: Command(resume='i like the first one. could you recommend something to do near the hotel?')
|
||||
|
||||
travel_advisor: Near the Ritz-Carlton in Palm Beach, here are some highly recommended activities:
|
||||
|
||||
1. Visit the Palm Beach Plaza Mall - Just a short walk from the hotel, featuring shopping, dining, and entertainment
|
||||
2. Try your luck at the Stellaris Casino - It's right in the Ritz-Carlton
|
||||
3. Take a sunset sailing cruise - Many depart from the nearby pier
|
||||
4. Visit the California Lighthouse - A scenic landmark just north of Palm Beach
|
||||
5. Enjoy water sports at Palm Beach:
|
||||
- Jet skiing
|
||||
- Parasailing
|
||||
- Snorkeling
|
||||
- Stand-up paddleboarding
|
||||
|
||||
Would you like more specific information about any of these activities or would you like to know about other options in the area?
|
||||
```
|
||||
|
||||
## Prebuilt implementations
|
||||
|
||||
LangGraph comes with prebuilt implementations of two of the most popular multi-agent architectures:
|
||||
|
||||
- [supervisor](../agents/multi-agent.md#supervisor) — individual agents are coordinated by a central supervisor agent. The supervisor controls all communication flow and task delegation, making decisions about which agent to invoke based on the current context and task requirements. You can use [`langgraph-supervisor`](https://github.com/langchain-ai/langgraph-supervisor-py) library to create a supervisor multi-agent systems.
|
||||
- [swarm](../agents/multi-agent.md#supervisor) — agents dynamically hand off control to one another based on their specializations. The system remembers which agent was last active, ensuring that on subsequent interactions, the conversation resumes with that agent. You can use [`langgraph-swarm`](https://github.com/langchain-ai/langgraph-swarm-py) library to create a swarm multi-agent systems.
|
||||
@@ -1,559 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Use subgraphs\n",
|
||||
"\n",
|
||||
"This guide explains the mechanics of using [subgraphs](../../concepts/subgraphs). A common application of subgraphs is to build [multi-agent](../../concepts/multi_agent) systems.\n",
|
||||
"\n",
|
||||
"When adding subgraphs, you need to define how the parent graph and the subgraph communicate:\n",
|
||||
"\n",
|
||||
"* [Shared state schemas](#shared-state-schemas) — parent and subgraph have **shared state keys** in their state [schemas](../../concepts/low_level#state)\n",
|
||||
"* [Different state schemas](#different-state-schemas) — **no shared state keys** in parent and subgraph [schemas](../../concepts/low_level#state)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div class=\"admonition tip\">\n",
|
||||
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
|
||||
" <p style=\"padding-top: 5px;\">\n",
|
||||
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
|
||||
" </p>\n",
|
||||
"</div>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Shared state schemas\n",
|
||||
"\n",
|
||||
"A common case is for the parent graph and subgraph to communicate over a shared state key (channel) in the [schema](../../concepts/low_level#state). For example, in [multi-agent](../../concepts/multi_agent) systems, the agents often communicate over a shared [messages](https://langchain-ai.github.io/langgraph/concepts/low_level/#why-use-messages) key.\n",
|
||||
"\n",
|
||||
"If your subgraph shares state keys with the parent graph, you can follow these steps to add it to your graph:\n",
|
||||
"\n",
|
||||
"1. Define the subgraph workflow (`subgraph_builder` in the example below) and compile it\n",
|
||||
"2. Pass compiled subgraph to the `.add_node` method when defining the parent graph workflow\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph.state import StateGraph, START\n",
|
||||
"\n",
|
||||
"class State(TypedDict):\n",
|
||||
" foo: str\n",
|
||||
"\n",
|
||||
"# Subgraph\n",
|
||||
"\n",
|
||||
"def subgraph_node_1(state: State):\n",
|
||||
" return {\"foo\": \"hi! \" + state[\"foo\"]}\n",
|
||||
"\n",
|
||||
"subgraph_builder = StateGraph(State)\n",
|
||||
"subgraph_builder.add_node(subgraph_node_1)\n",
|
||||
"subgraph_builder.add_edge(START, \"subgraph_node_1\")\n",
|
||||
"# highlight-next-line\n",
|
||||
"subgraph = subgraph_builder.compile()\n",
|
||||
"\n",
|
||||
"# Parent graph\n",
|
||||
"\n",
|
||||
"builder = StateGraph(State)\n",
|
||||
"# highlight-next-line\n",
|
||||
"builder.add_node(\"node_1\", subgraph)\n",
|
||||
"builder.add_edge(START, \"node_1\")\n",
|
||||
"graph = builder.compile()\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"??? example \"Full example: shared state schemas\"\n",
|
||||
"\n",
|
||||
" ```python\n",
|
||||
" from typing_extensions import TypedDict\n",
|
||||
" from langgraph.graph.state import StateGraph, START\n",
|
||||
"\n",
|
||||
" # Define subgraph\n",
|
||||
" class SubgraphState(TypedDict):\n",
|
||||
" foo: str # (1)! \n",
|
||||
" bar: str # (2)!\n",
|
||||
" \n",
|
||||
" def subgraph_node_1(state: SubgraphState):\n",
|
||||
" return {\"bar\": \"bar\"}\n",
|
||||
" \n",
|
||||
" def subgraph_node_2(state: SubgraphState):\n",
|
||||
" # note that this node is using a state key ('bar') that is only available in the subgraph\n",
|
||||
" # and is sending update on the shared state key ('foo')\n",
|
||||
" return {\"foo\": state[\"foo\"] + state[\"bar\"]}\n",
|
||||
" \n",
|
||||
" subgraph_builder = StateGraph(SubgraphState)\n",
|
||||
" subgraph_builder.add_node(subgraph_node_1)\n",
|
||||
" subgraph_builder.add_node(subgraph_node_2)\n",
|
||||
" subgraph_builder.add_edge(START, \"subgraph_node_1\")\n",
|
||||
" subgraph_builder.add_edge(\"subgraph_node_1\", \"subgraph_node_2\")\n",
|
||||
" subgraph = subgraph_builder.compile()\n",
|
||||
" \n",
|
||||
" # Define parent graph\n",
|
||||
" class ParentState(TypedDict):\n",
|
||||
" foo: str\n",
|
||||
" \n",
|
||||
" def node_1(state: ParentState):\n",
|
||||
" return {\"foo\": \"hi! \" + state[\"foo\"]}\n",
|
||||
" \n",
|
||||
" builder = StateGraph(ParentState)\n",
|
||||
" builder.add_node(\"node_1\", node_1)\n",
|
||||
" # highlight-next-line\n",
|
||||
" builder.add_node(\"node_2\", subgraph)\n",
|
||||
" builder.add_edge(START, \"node_1\")\n",
|
||||
" builder.add_edge(\"node_1\", \"node_2\")\n",
|
||||
" graph = builder.compile()\n",
|
||||
" \n",
|
||||
" for chunk in graph.stream({\"foo\": \"foo\"}):\n",
|
||||
" print(chunk)\n",
|
||||
" ```\n",
|
||||
"\n",
|
||||
" 1. This key is shared with the parent graph state\n",
|
||||
" 2. This key is private to the `SubgraphState` and is not visible to the parent graph\n",
|
||||
" \n",
|
||||
" ```\n",
|
||||
" {'node_1': {'foo': 'hi! foo'}}\n",
|
||||
" {'node_2': {'foo': 'hi! foobar'}}\n",
|
||||
" ```\n",
|
||||
"\n",
|
||||
" ```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Different state schemas\n",
|
||||
"\n",
|
||||
"For more complex systems you might want to define subgraphs that have a **completely different schema** from the parent graph (no shared keys). For example, you might want to keep a private message history for each of the agents in a [multi-agent](../concepts/multi_agent.md) system.\n",
|
||||
"\n",
|
||||
"If that's the case for your application, you need to define a node **function that invokes the subgraph**. This function needs to transform the input (parent) state to the subgraph state before invoking the subgraph, and transform the results back to the parent state before returning the state update from the node.\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"from langgraph.graph.state import StateGraph, START\n",
|
||||
"\n",
|
||||
"class SubgraphState(TypedDict):\n",
|
||||
" bar: str\n",
|
||||
"\n",
|
||||
"# Subgraph\n",
|
||||
"\n",
|
||||
"def subgraph_node_1(state: SubgraphState):\n",
|
||||
" return {\"bar\": \"hi! \" + state[\"bar\"]}\n",
|
||||
"\n",
|
||||
"subgraph_builder = StateGraph(SubgraphState)\n",
|
||||
"subgraph_builder.add_node(subgraph_node_1)\n",
|
||||
"subgraph_builder.add_edge(START, \"subgraph_node_1\")\n",
|
||||
"# highlight-next-line\n",
|
||||
"subgraph = subgraph_builder.compile()\n",
|
||||
"\n",
|
||||
"# Parent graph\n",
|
||||
"\n",
|
||||
"class State(TypedDict):\n",
|
||||
" foo: str\n",
|
||||
"\n",
|
||||
"def call_subgraph(state: State):\n",
|
||||
" # highlight-next-line\n",
|
||||
" subgraph_output = subgraph.invoke({\"bar\": state[\"foo\"]}) # (1)!\n",
|
||||
" # highlight-next-line\n",
|
||||
" return {\"foo\": subgraph_output[\"bar\"]} # (2)!\n",
|
||||
"\n",
|
||||
"builder = StateGraph(State)\n",
|
||||
"# highlight-next-line\n",
|
||||
"builder.add_node(\"node_1\", call_subgraph)\n",
|
||||
"builder.add_edge(START, \"node_1\")\n",
|
||||
"graph = builder.compile()\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"1. Transform the state to the subgraph state\n",
|
||||
"2. Transform response back to the parent state\n",
|
||||
"\n",
|
||||
"??? example \"Full example: different state schemas\"\n",
|
||||
"\n",
|
||||
" ```python\n",
|
||||
" from typing_extensions import TypedDict\n",
|
||||
" from langgraph.graph.state import StateGraph, START\n",
|
||||
"\n",
|
||||
" # Define subgraph\n",
|
||||
" class SubgraphState(TypedDict):\n",
|
||||
" # note that none of these keys are shared with the parent graph state\n",
|
||||
" bar: str\n",
|
||||
" baz: str\n",
|
||||
" \n",
|
||||
" def subgraph_node_1(state: SubgraphState):\n",
|
||||
" return {\"baz\": \"baz\"}\n",
|
||||
" \n",
|
||||
" def subgraph_node_2(state: SubgraphState):\n",
|
||||
" return {\"bar\": state[\"bar\"] + state[\"baz\"]}\n",
|
||||
" \n",
|
||||
" subgraph_builder = StateGraph(SubgraphState)\n",
|
||||
" subgraph_builder.add_node(subgraph_node_1)\n",
|
||||
" subgraph_builder.add_node(subgraph_node_2)\n",
|
||||
" subgraph_builder.add_edge(START, \"subgraph_node_1\")\n",
|
||||
" subgraph_builder.add_edge(\"subgraph_node_1\", \"subgraph_node_2\")\n",
|
||||
" subgraph = subgraph_builder.compile()\n",
|
||||
" \n",
|
||||
" # Define parent graph\n",
|
||||
" class ParentState(TypedDict):\n",
|
||||
" foo: str\n",
|
||||
" \n",
|
||||
" def node_1(state: ParentState):\n",
|
||||
" return {\"foo\": \"hi! \" + state[\"foo\"]}\n",
|
||||
" \n",
|
||||
" def node_2(state: ParentState):\n",
|
||||
" # highlight-next-line\n",
|
||||
" response = subgraph.invoke({\"bar\": state[\"foo\"]}) # (1)!\n",
|
||||
" # highlight-next-line\n",
|
||||
" return {\"foo\": response[\"bar\"]} # (2)!\n",
|
||||
" \n",
|
||||
" \n",
|
||||
" builder = StateGraph(ParentState)\n",
|
||||
" builder.add_node(\"node_1\", node_1)\n",
|
||||
" # highlight-next-line\n",
|
||||
" builder.add_node(\"node_2\", node_2)\n",
|
||||
" builder.add_edge(START, \"node_1\")\n",
|
||||
" builder.add_edge(\"node_1\", \"node_2\")\n",
|
||||
" graph = builder.compile()\n",
|
||||
" \n",
|
||||
" for chunk in graph.stream({\"foo\": \"foo\"}, subgraphs=True):\n",
|
||||
" print(chunk)\n",
|
||||
" ```\n",
|
||||
"\n",
|
||||
" 1. Transform the state to the subgraph state\n",
|
||||
" 2. Transform response back to the parent state\n",
|
||||
"\n",
|
||||
" ```\n",
|
||||
" ((), {'node_1': {'foo': 'hi! foo'}})\n",
|
||||
" (('node_2:9c36dd0f-151a-cb42-cbad-fa2f851f9ab7',), {'subgraph_node_1': {'baz': 'baz'}})\n",
|
||||
" (('node_2:9c36dd0f-151a-cb42-cbad-fa2f851f9ab7',), {'subgraph_node_2': {'bar': 'hi! foobaz'}})\n",
|
||||
" ((), {'node_2': {'foo': 'hi! foobaz'}})\n",
|
||||
" ```\n",
|
||||
"\n",
|
||||
"??? example \"Full example: different state schemas (two levels of subgraphs)\"\n",
|
||||
"\n",
|
||||
" This is an example with two levels of subgraphs: parent -> child -> grandchild.\n",
|
||||
"\n",
|
||||
" ```python\n",
|
||||
" # Grandchild graph\n",
|
||||
" from typing_extensions import TypedDict\n",
|
||||
" from langgraph.graph.state import StateGraph, START, END\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",
|
||||
" \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()\n",
|
||||
" \n",
|
||||
" # Child graph\n",
|
||||
" class ChildState(TypedDict):\n",
|
||||
" my_child_key: str\n",
|
||||
" \n",
|
||||
" def call_grandchild_graph(state: ChildState) -> ChildState:\n",
|
||||
" # NOTE: parent or grandchild keys won't be accessible here\n",
|
||||
" grandchild_graph_input = {\"my_grandchild_key\": state[\"my_child_key\"]} # (1)!\n",
|
||||
" # highlight-next-line\n",
|
||||
" grandchild_graph_output = grandchild_graph.invoke(grandchild_graph_input)\n",
|
||||
" return {\"my_child_key\": grandchild_graph_output[\"my_grandchild_key\"] + \" today?\"} # (2)!\n",
|
||||
" \n",
|
||||
" child = StateGraph(ChildState)\n",
|
||||
" # highlight-next-line\n",
|
||||
" child.add_node(\"child_1\", call_grandchild_graph) # (3)!\n",
|
||||
" child.add_edge(START, \"child_1\")\n",
|
||||
" child.add_edge(\"child_1\", END)\n",
|
||||
" child_graph = child.compile()\n",
|
||||
" \n",
|
||||
" # Parent graph\n",
|
||||
" 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",
|
||||
" def call_child_graph(state: ParentState) -> ParentState:\n",
|
||||
" child_graph_input = {\"my_child_key\": state[\"my_key\"]} # (4)!\n",
|
||||
" # highlight-next-line\n",
|
||||
" child_graph_output = child_graph.invoke(child_graph_input)\n",
|
||||
" return {\"my_key\": child_graph_output[\"my_child_key\"]} # (5)!\n",
|
||||
" \n",
|
||||
" parent = StateGraph(ParentState)\n",
|
||||
" parent.add_node(\"parent_1\", parent_1)\n",
|
||||
" # highlight-next-line\n",
|
||||
" parent.add_node(\"child\", call_child_graph) # (6)!\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()\n",
|
||||
" \n",
|
||||
" for chunk in parent_graph.stream({\"my_key\": \"Bob\"}, subgraphs=True):\n",
|
||||
" print(chunk)\n",
|
||||
" ```\n",
|
||||
"\n",
|
||||
" 1. We're transforming the state from the child state channels (`my_child_key`) to the child state channels (`my_grandchild_key`)\n",
|
||||
" 2. We're transforming the state from the grandchild state channels (`my_grandchild_key`) back to the child state channels (`my_child_key`)\n",
|
||||
" 3. We're passing a function here instead of just compiled graph (`grandchild_graph`)\n",
|
||||
" 4. We're transforming the state from the parent state channels (`my_key`) to the child state channels (`my_child_key`)\n",
|
||||
" 5. We're transforming the state from the child state channels (`my_child_key`) back to the parent state channels (`my_key`)\n",
|
||||
" 6. We're passing a function here instead of just a compiled graph (`child_graph`)\n",
|
||||
"\n",
|
||||
" ```\n",
|
||||
" ((), {'parent_1': {'my_key': 'hi Bob'}})\n",
|
||||
" (('child:2e26e9ce-602f-862c-aa66-1ea5a4655e3b', 'child_1:781bb3b1-3971-84ce-810b-acf819a03f9c'), {'grandchild_1': {'my_grandchild_key': 'hi Bob, how are you'}})\n",
|
||||
" (('child:2e26e9ce-602f-862c-aa66-1ea5a4655e3b',), {'child_1': {'my_child_key': 'hi Bob, how are you today?'}})\n",
|
||||
" ((), {'child': {'my_key': 'hi Bob, how are you today?'}})\n",
|
||||
" ((), {'parent_2': {'my_key': 'hi Bob, how are you today? bye!'}})\n",
|
||||
" ```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Add persistence \n",
|
||||
"\n",
|
||||
"You only need to **provide the checkpointer when compiling the parent graph**. LangGraph will automatically propagate the checkpointer to the child subgraphs.\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"from langgraph.graph import START, StateGraph\n",
|
||||
"from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"class State(TypedDict):\n",
|
||||
" foo: str\n",
|
||||
"\n",
|
||||
"# Subgraph\n",
|
||||
"\n",
|
||||
"def subgraph_node_1(state: State):\n",
|
||||
" return {\"foo\": state[\"foo\"] + \"bar\"}\n",
|
||||
"\n",
|
||||
"subgraph_builder = StateGraph(State)\n",
|
||||
"subgraph_builder.add_node(subgraph_node_1)\n",
|
||||
"subgraph_builder.add_edge(START, \"subgraph_node_1\")\n",
|
||||
"# highlight-next-line\n",
|
||||
"subgraph = subgraph_builder.compile()\n",
|
||||
"\n",
|
||||
"# Parent graph\n",
|
||||
"\n",
|
||||
"builder = StateGraph(State)\n",
|
||||
"# highlight-next-line\n",
|
||||
"builder.add_node(\"node_1\", subgraph)\n",
|
||||
"builder.add_edge(START, \"node_1\")\n",
|
||||
"\n",
|
||||
"checkpointer = InMemorySaver()\n",
|
||||
"# highlight-next-line\n",
|
||||
"graph = builder.compile(checkpointer=checkpointer)\n",
|
||||
"``` \n",
|
||||
"\n",
|
||||
"If you want the subgraph to **have its own memory**, you can compile it `with checkpointer=True`. This is useful in [multi-agent](../../concepts/multi_agent) systems, if you want agents to keep track of their internal message histories:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"subgraph_builder = StateGraph(...)\n",
|
||||
"# highlight-next-line\n",
|
||||
"subgraph = subgraph_builder.compile(checkpointer=True)\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## View subgraph state\n",
|
||||
"\n",
|
||||
"When you enable [persistence](../persistence), you can [inspect the graph state](../persistence#manage-checkpoints) (checkpoint) via `graph.get_state(config)`. To view the subgraph state, you can use `graph.get_state(config, subgraphs=True)`.\n",
|
||||
"\n",
|
||||
"!!! important \"Available **only** when interrupted\"\n",
|
||||
"\n",
|
||||
" Subgraph state can only be viewed **when the subgraph is interrupted**. Once you resume the graph, you won't be able to access the subgraph state.\n",
|
||||
"\n",
|
||||
"??? example \"View interrupted subgraph state\"\n",
|
||||
"\n",
|
||||
" ```python\n",
|
||||
" from langgraph.graph import START, StateGraph\n",
|
||||
" from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
" from langgraph.types import interrupt, Command\n",
|
||||
" from typing_extensions import TypedDict\n",
|
||||
" \n",
|
||||
" class State(TypedDict):\n",
|
||||
" foo: str\n",
|
||||
" \n",
|
||||
" # Subgraph\n",
|
||||
" \n",
|
||||
" def subgraph_node_1(state: State):\n",
|
||||
" # highlight-next-line\n",
|
||||
" value = interrupt(\"Provide value:\")\n",
|
||||
" return {\"foo\": state[\"foo\"] + value}\n",
|
||||
" \n",
|
||||
" subgraph_builder = StateGraph(State)\n",
|
||||
" subgraph_builder.add_node(subgraph_node_1)\n",
|
||||
" subgraph_builder.add_edge(START, \"subgraph_node_1\")\n",
|
||||
" \n",
|
||||
" subgraph = subgraph_builder.compile()\n",
|
||||
" \n",
|
||||
" # Parent graph\n",
|
||||
" \n",
|
||||
" builder = StateGraph(State)\n",
|
||||
" # highlight-next-line\n",
|
||||
" builder.add_node(\"node_1\", subgraph)\n",
|
||||
" builder.add_edge(START, \"node_1\")\n",
|
||||
" \n",
|
||||
" checkpointer = InMemorySaver()\n",
|
||||
" # highlight-next-line\n",
|
||||
" graph = builder.compile(checkpointer=checkpointer)\n",
|
||||
" \n",
|
||||
" config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
|
||||
" \n",
|
||||
" graph.invoke({\"foo\": \"\"}, config)\n",
|
||||
" parent_state = graph.get_state(config)\n",
|
||||
" # highlight-next-line\n",
|
||||
" subgraph_state = graph.get_state(config, subgraphs=True).tasks[0].state # (1)!\n",
|
||||
" \n",
|
||||
" # resume the subgraph\n",
|
||||
" graph.invoke(Command(resume=\"bar\"), config)\n",
|
||||
" ```\n",
|
||||
" \n",
|
||||
" 1. This will be available only when the subgraph is interrupted. Once you resume the graph, you won't be able to access the subgraph state."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Stream subgraph outputs\n",
|
||||
"\n",
|
||||
"To include outputs from [subgraphs](../concepts/low_level.md#subgraphs) in the streamed outputs, you can set `subgraphs=True` in the `.stream()` method of the parent graph. This will stream outputs from both the parent graph and any subgraphs.\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"for chunk in graph.stream(\n",
|
||||
" {\"foo\": \"foo\"},\n",
|
||||
" # highlight-next-line\n",
|
||||
" subgraphs=True, # (1)!\n",
|
||||
" stream_mode=\"updates\",\n",
|
||||
"):\n",
|
||||
" print(chunk)\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"1. Set `subgraphs=True` to stream outputs from subgraphs.\n",
|
||||
"\n",
|
||||
"??? example \"Stream from subgraphs\"\n",
|
||||
"\n",
|
||||
" ```python\n",
|
||||
" from typing_extensions import TypedDict\n",
|
||||
" from langgraph.graph.state import StateGraph, START\n",
|
||||
"\n",
|
||||
" # Define subgraph\n",
|
||||
" class SubgraphState(TypedDict):\n",
|
||||
" foo: str\n",
|
||||
" bar: str\n",
|
||||
" \n",
|
||||
" def subgraph_node_1(state: SubgraphState):\n",
|
||||
" return {\"bar\": \"bar\"}\n",
|
||||
" \n",
|
||||
" def subgraph_node_2(state: SubgraphState):\n",
|
||||
" # note that this node is using a state key ('bar') that is only available in the subgraph\n",
|
||||
" # and is sending update on the shared state key ('foo')\n",
|
||||
" return {\"foo\": state[\"foo\"] + state[\"bar\"]}\n",
|
||||
" \n",
|
||||
" subgraph_builder = StateGraph(SubgraphState)\n",
|
||||
" subgraph_builder.add_node(subgraph_node_1)\n",
|
||||
" subgraph_builder.add_node(subgraph_node_2)\n",
|
||||
" subgraph_builder.add_edge(START, \"subgraph_node_1\")\n",
|
||||
" subgraph_builder.add_edge(\"subgraph_node_1\", \"subgraph_node_2\")\n",
|
||||
" subgraph = subgraph_builder.compile()\n",
|
||||
" \n",
|
||||
" # Define parent graph\n",
|
||||
" class ParentState(TypedDict):\n",
|
||||
" foo: str\n",
|
||||
" \n",
|
||||
" def node_1(state: ParentState):\n",
|
||||
" return {\"foo\": \"hi! \" + state[\"foo\"]}\n",
|
||||
" \n",
|
||||
" builder = StateGraph(ParentState)\n",
|
||||
" builder.add_node(\"node_1\", node_1)\n",
|
||||
" # highlight-next-line\n",
|
||||
" builder.add_node(\"node_2\", subgraph)\n",
|
||||
" builder.add_edge(START, \"node_1\")\n",
|
||||
" builder.add_edge(\"node_1\", \"node_2\")\n",
|
||||
" graph = builder.compile()\n",
|
||||
"\n",
|
||||
" for chunk in graph.stream(\n",
|
||||
" {\"foo\": \"foo\"},\n",
|
||||
" stream_mode=\"updates\",\n",
|
||||
" # highlight-next-line\n",
|
||||
" subgraphs=True, # (1)!\n",
|
||||
" ):\n",
|
||||
" print(chunk)\n",
|
||||
" ```\n",
|
||||
" \n",
|
||||
" 1. Set `subgraphs=True` to stream outputs from subgraphs.\n",
|
||||
"\n",
|
||||
" ```\n",
|
||||
" ((), {'node_1': {'foo': 'hi! foo'}})\n",
|
||||
" (('node_2:e58e5673-a661-ebb0-70d4-e298a7fc28b7',), {'subgraph_node_1': {'bar': 'bar'}})\n",
|
||||
" (('node_2:e58e5673-a661-ebb0-70d4-e298a7fc28b7',), {'subgraph_node_2': {'foo': 'hi! foobar'}})\n",
|
||||
" ((), {'node_2': {'foo': 'hi! foobar'}})\n",
|
||||
" ```"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
# Use subgraphs
|
||||
|
||||
This guide explains the mechanics of using [subgraphs](../concepts/subgraphs.md). A common application of subgraphs is to build [multi-agent](../concepts/multi_agent.md) systems.
|
||||
|
||||
When adding subgraphs, you need to define how the parent graph and the subgraph communicate:
|
||||
|
||||
* [Shared state schemas](#shared-state-schemas) — parent and subgraph have **shared state keys** in their state [schemas](../concepts/low_level.md#state)
|
||||
* [Different state schemas](#different-state-schemas) — **no shared state keys** in parent and subgraph [schemas](../concepts/low_level.md#state)
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
pip install -U langgraph
|
||||
```
|
||||
|
||||
!!! tip "Set up LangSmith for LangGraph development"
|
||||
Sign up for [LangSmith](https://smith.langchain.com) to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started [here](https://docs.smith.langchain.com).
|
||||
|
||||
## Shared state schemas
|
||||
|
||||
A common case is for the parent graph and subgraph to communicate over a shared state key (channel) in the [schema](../concepts/low_level.md#state). For example, in [multi-agent](../concepts/multi_agent.md) systems, the agents often communicate over a shared [messages](https://langchain-ai.github.io/langgraph/concepts/low_level.md#why-use-messages) key.
|
||||
|
||||
If your subgraph shares state keys with the parent graph, you can follow these steps to add it to your graph:
|
||||
|
||||
1. Define the subgraph workflow (`subgraph_builder` in the example below) and compile it
|
||||
2. Pass compiled subgraph to the `.add_node` method when defining the parent graph workflow
|
||||
|
||||
```python
|
||||
from typing_extensions import TypedDict
|
||||
from langgraph.graph.state import StateGraph, START
|
||||
|
||||
class State(TypedDict):
|
||||
foo: str
|
||||
|
||||
# Subgraph
|
||||
|
||||
def subgraph_node_1(state: State):
|
||||
return {"foo": "hi! " + state["foo"]}
|
||||
|
||||
subgraph_builder = StateGraph(State)
|
||||
subgraph_builder.add_node(subgraph_node_1)
|
||||
subgraph_builder.add_edge(START, "subgraph_node_1")
|
||||
subgraph = subgraph_builder.compile()
|
||||
|
||||
# Parent graph
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("node_1", subgraph)
|
||||
builder.add_edge(START, "node_1")
|
||||
graph = builder.compile()
|
||||
```
|
||||
|
||||
??? example "Full example: shared state schemas"
|
||||
|
||||
```python
|
||||
from typing_extensions import TypedDict
|
||||
from langgraph.graph.state import StateGraph, START
|
||||
|
||||
# Define subgraph
|
||||
class SubgraphState(TypedDict):
|
||||
foo: str # (1)!
|
||||
bar: str # (2)!
|
||||
|
||||
def subgraph_node_1(state: SubgraphState):
|
||||
return {"bar": "bar"}
|
||||
|
||||
def subgraph_node_2(state: SubgraphState):
|
||||
# note that this node is using a state key ('bar') that is only available in the subgraph
|
||||
# and is sending update on the shared state key ('foo')
|
||||
return {"foo": state["foo"] + state["bar"]}
|
||||
|
||||
subgraph_builder = StateGraph(SubgraphState)
|
||||
subgraph_builder.add_node(subgraph_node_1)
|
||||
subgraph_builder.add_node(subgraph_node_2)
|
||||
subgraph_builder.add_edge(START, "subgraph_node_1")
|
||||
subgraph_builder.add_edge("subgraph_node_1", "subgraph_node_2")
|
||||
subgraph = subgraph_builder.compile()
|
||||
|
||||
# Define parent graph
|
||||
class ParentState(TypedDict):
|
||||
foo: str
|
||||
|
||||
def node_1(state: ParentState):
|
||||
return {"foo": "hi! " + state["foo"]}
|
||||
|
||||
builder = StateGraph(ParentState)
|
||||
builder.add_node("node_1", node_1)
|
||||
builder.add_node("node_2", subgraph)
|
||||
builder.add_edge(START, "node_1")
|
||||
builder.add_edge("node_1", "node_2")
|
||||
graph = builder.compile()
|
||||
|
||||
for chunk in graph.stream({"foo": "foo"}):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
1. This key is shared with the parent graph state
|
||||
2. This key is private to the `SubgraphState` and is not visible to the parent graph
|
||||
|
||||
```
|
||||
{'node_1': {'foo': 'hi! foo'}}
|
||||
{'node_2': {'foo': 'hi! foobar'}}
|
||||
```
|
||||
|
||||
## Different state schemas
|
||||
|
||||
For more complex systems you might want to define subgraphs that have a **completely different schema** from the parent graph (no shared keys). For example, you might want to keep a private message history for each of the agents in a [multi-agent](../concepts/multi_agent.md) system.
|
||||
|
||||
If that's the case for your application, you need to define a node **function that invokes the subgraph**. This function needs to transform the input (parent) state to the subgraph state before invoking the subgraph, and transform the results back to the parent state before returning the state update from the node.
|
||||
|
||||
```python
|
||||
from typing_extensions import TypedDict
|
||||
from langgraph.graph.state import StateGraph, START
|
||||
|
||||
class SubgraphState(TypedDict):
|
||||
bar: str
|
||||
|
||||
# Subgraph
|
||||
|
||||
def subgraph_node_1(state: SubgraphState):
|
||||
return {"bar": "hi! " + state["bar"]}
|
||||
|
||||
subgraph_builder = StateGraph(SubgraphState)
|
||||
subgraph_builder.add_node(subgraph_node_1)
|
||||
subgraph_builder.add_edge(START, "subgraph_node_1")
|
||||
subgraph = subgraph_builder.compile()
|
||||
|
||||
# Parent graph
|
||||
|
||||
class State(TypedDict):
|
||||
foo: str
|
||||
|
||||
def call_subgraph(state: State):
|
||||
subgraph_output = subgraph.invoke({"bar": state["foo"]}) # (1)!
|
||||
return {"foo": subgraph_output["bar"]} # (2)!
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("node_1", call_subgraph)
|
||||
builder.add_edge(START, "node_1")
|
||||
graph = builder.compile()
|
||||
```
|
||||
|
||||
1. Transform the state to the subgraph state
|
||||
2. Transform response back to the parent state
|
||||
|
||||
??? example "Full example: different state schemas"
|
||||
|
||||
```python
|
||||
from typing_extensions import TypedDict
|
||||
from langgraph.graph.state import StateGraph, START
|
||||
|
||||
# Define subgraph
|
||||
class SubgraphState(TypedDict):
|
||||
# note that none of these keys are shared with the parent graph state
|
||||
bar: str
|
||||
baz: str
|
||||
|
||||
def subgraph_node_1(state: SubgraphState):
|
||||
return {"baz": "baz"}
|
||||
|
||||
def subgraph_node_2(state: SubgraphState):
|
||||
return {"bar": state["bar"] + state["baz"]}
|
||||
|
||||
subgraph_builder = StateGraph(SubgraphState)
|
||||
subgraph_builder.add_node(subgraph_node_1)
|
||||
subgraph_builder.add_node(subgraph_node_2)
|
||||
subgraph_builder.add_edge(START, "subgraph_node_1")
|
||||
subgraph_builder.add_edge("subgraph_node_1", "subgraph_node_2")
|
||||
subgraph = subgraph_builder.compile()
|
||||
|
||||
# Define parent graph
|
||||
class ParentState(TypedDict):
|
||||
foo: str
|
||||
|
||||
def node_1(state: ParentState):
|
||||
return {"foo": "hi! " + state["foo"]}
|
||||
|
||||
def node_2(state: ParentState):
|
||||
response = subgraph.invoke({"bar": state["foo"]}) # (1)!
|
||||
return {"foo": response["bar"]} # (2)!
|
||||
|
||||
|
||||
builder = StateGraph(ParentState)
|
||||
builder.add_node("node_1", node_1)
|
||||
builder.add_node("node_2", node_2)
|
||||
builder.add_edge(START, "node_1")
|
||||
builder.add_edge("node_1", "node_2")
|
||||
graph = builder.compile()
|
||||
|
||||
for chunk in graph.stream({"foo": "foo"}, subgraphs=True):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
1. Transform the state to the subgraph state
|
||||
2. Transform response back to the parent state
|
||||
|
||||
```
|
||||
((), {'node_1': {'foo': 'hi! foo'}})
|
||||
(('node_2:9c36dd0f-151a-cb42-cbad-fa2f851f9ab7',), {'grandchild_1': {'my_grandchild_key': 'hi Bob, how are you'}})
|
||||
(('node_2:9c36dd0f-151a-cb42-cbad-fa2f851f9ab7',), {'grandchild_2': {'bar': 'hi! foobaz'}})
|
||||
((), {'node_2': {'foo': 'hi! foobaz'}})
|
||||
```
|
||||
|
||||
??? example "Full example: different state schemas (two levels of subgraphs)"
|
||||
|
||||
This is an example with two levels of subgraphs: parent -> child -> grandchild.
|
||||
|
||||
```python
|
||||
# Grandchild graph
|
||||
from typing_extensions import TypedDict
|
||||
from langgraph.graph.state import StateGraph, START, END
|
||||
|
||||
class GrandChildState(TypedDict):
|
||||
my_grandchild_key: str
|
||||
|
||||
def grandchild_1(state: GrandChildState) -> GrandChildState:
|
||||
# NOTE: child or parent keys will not be accessible here
|
||||
return {"my_grandchild_key": state["my_grandchild_key"] + ", how are you"}
|
||||
|
||||
|
||||
grandchild = StateGraph(GrandChildState)
|
||||
grandchild.add_node("grandchild_1", grandchild_1)
|
||||
|
||||
grandchild.add_edge(START, "grandchild_1")
|
||||
grandchild.add_edge("grandchild_1", END)
|
||||
|
||||
grandchild_graph = grandchild.compile()
|
||||
|
||||
# Child graph
|
||||
class ChildState(TypedDict):
|
||||
my_child_key: str
|
||||
|
||||
def call_grandchild_graph(state: ChildState) -> ChildState:
|
||||
# NOTE: parent or grandchild keys won't be accessible here
|
||||
grandchild_graph_input = {"my_grandchild_key": state["my_child_key"]} # (1)!
|
||||
grandchild_graph_output = grandchild_graph.invoke(grandchild_graph_input)
|
||||
return {"my_child_key": grandchild_graph_output["my_grandchild_key"] + " today?"} # (2)!
|
||||
|
||||
child = StateGraph(ChildState)
|
||||
child.add_node("child_1", call_grandchild_graph) # (3)!
|
||||
child.add_edge(START, "child_1")
|
||||
child.add_edge("child_1", END)
|
||||
child_graph = child.compile()
|
||||
|
||||
# Parent graph
|
||||
class ParentState(TypedDict):
|
||||
my_key: str
|
||||
|
||||
def parent_1(state: ParentState) -> ParentState:
|
||||
# NOTE: child or grandchild keys won't be accessible here
|
||||
return {"my_key": "hi " + state["my_key"]}
|
||||
|
||||
def parent_2(state: ParentState) -> ParentState:
|
||||
return {"my_key": state["my_key"] + " bye!"}
|
||||
|
||||
def call_child_graph(state: ParentState) -> ParentState:
|
||||
child_graph_input = {"my_child_key": state["my_key"]} # (4)!
|
||||
child_graph_output = child_graph.invoke(child_graph_input)
|
||||
return {"my_key": child_graph_output["my_child_key"]} # (5)!
|
||||
|
||||
parent = StateGraph(ParentState)
|
||||
parent.add_node("parent_1", parent_1)
|
||||
parent.add_node("child", call_child_graph) # (6)!
|
||||
parent.add_node("parent_2", parent_2)
|
||||
|
||||
parent.add_edge(START, "parent_1")
|
||||
parent.add_edge("parent_1", "child")
|
||||
parent.add_edge("child", "parent_2")
|
||||
parent.add_edge("parent_2", END)
|
||||
|
||||
parent_graph = parent.compile()
|
||||
|
||||
for chunk in parent_graph.stream({"my_key": "Bob"}, subgraphs=True):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
1. We're transforming the state from the child state channels (`my_child_key`) to the child state channels (`my_grandchild_key`)
|
||||
2. We're transforming the state from the grandchild state channels (`my_grandchild_key`) back to the child state channels (`my_child_key`)
|
||||
3. We're passing a function here instead of just compiled graph (`grandchild_graph`)
|
||||
4. We're transforming the state from the parent state channels (`my_key`) to the child state channels (`my_child_key`)
|
||||
5. We're transforming the state from the child state channels (`my_child_key`) back to the parent state channels (`my_key`)
|
||||
6. We're passing a function here instead of just a compiled graph (`child_graph`)
|
||||
|
||||
```
|
||||
((), {'parent_1': {'my_key': 'hi Bob'}})
|
||||
(('child:2e26e9ce-602f-862c-aa66-1ea5a4655e3b', 'child_1:781bb3b1-3971-84ce-810b-acf819a03f9c'), {'grandchild_1': {'my_grandchild_key': 'hi Bob, how are you'}})
|
||||
(('child:2e26e9ce-602f-862c-aa66-1ea5a4655e3b',), {'child_1': {'my_child_key': 'hi Bob, how are you today?'}})
|
||||
((), {'child': {'my_key': 'hi Bob, how are you today?'}})
|
||||
((), {'parent_2': {'my_key': 'hi Bob, how are you today? bye!'}})
|
||||
```
|
||||
|
||||
## Add persistence
|
||||
|
||||
You only need to **provide the checkpointer when compiling the parent graph**. LangGraph will automatically propagate the checkpointer to the child subgraphs.
|
||||
|
||||
```python
|
||||
from langgraph.graph import START, StateGraph
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
class State(TypedDict):
|
||||
foo: str
|
||||
|
||||
# Subgraph
|
||||
|
||||
def subgraph_node_1(state: State):
|
||||
return {"foo": state["foo"] + "bar"}
|
||||
|
||||
subgraph_builder = StateGraph(State)
|
||||
subgraph_builder.add_node(subgraph_node_1)
|
||||
subgraph_builder.add_edge(START, "subgraph_node_1")
|
||||
subgraph = subgraph_builder.compile()
|
||||
|
||||
# Parent graph
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("node_1", subgraph)
|
||||
builder.add_edge(START, "node_1")
|
||||
|
||||
checkpointer = InMemorySaver()
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
```
|
||||
|
||||
If you want the subgraph to **have its own memory**, you can compile it `with checkpointer=True`. This is useful in [multi-agent](../concepts/multi_agent.md) systems, if you want agents to keep track of their internal message histories:
|
||||
|
||||
```python
|
||||
subgraph_builder = StateGraph(...)
|
||||
subgraph = subgraph_builder.compile(checkpointer=True)
|
||||
```
|
||||
|
||||
## View subgraph state
|
||||
|
||||
When you enable [persistence](../concepts/persistence.md), you can [inspect the graph state](../concepts/persistence.md#checkpoints) (checkpoint) via `graph.get_state(config)`. To view the subgraph state, you can use `graph.get_state(config, subgraphs=True)`.
|
||||
|
||||
!!! important "Available **only** when interrupted"
|
||||
|
||||
Subgraph state can only be viewed **when the subgraph is interrupted**. Once you resume the graph, you won't be able to access the subgraph state.
|
||||
|
||||
??? example "View interrupted subgraph state"
|
||||
|
||||
```python
|
||||
from langgraph.graph import START, StateGraph
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.types import interrupt, Command
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
class State(TypedDict):
|
||||
foo: str
|
||||
|
||||
# Subgraph
|
||||
|
||||
def subgraph_node_1(state: State):
|
||||
value = interrupt("Provide value:")
|
||||
return {"foo": state["foo"] + value}
|
||||
|
||||
subgraph_builder = StateGraph(State)
|
||||
subgraph_builder.add_node(subgraph_node_1)
|
||||
subgraph_builder.add_edge(START, "subgraph_node_1")
|
||||
|
||||
subgraph = subgraph_builder.compile()
|
||||
|
||||
# Parent graph
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("node_1", subgraph)
|
||||
builder.add_edge(START, "node_1")
|
||||
|
||||
checkpointer = InMemorySaver()
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
graph.invoke({"foo": ""}, config)
|
||||
parent_state = graph.get_state(config)
|
||||
subgraph_state = graph.get_state(config, subgraphs=True).tasks[0].state # (1)!
|
||||
|
||||
# resume the subgraph
|
||||
graph.invoke(Command(resume="bar"), config)
|
||||
```
|
||||
|
||||
1. This will be available only when the subgraph is interrupted. Once you resume the graph, you won't be able to access the subgraph state.
|
||||
|
||||
## Stream subgraph outputs
|
||||
|
||||
To include outputs from subgraphs in the streamed outputs, you can set `subgraphs=True` in the `.stream()` method of the parent graph. This will stream outputs from both the parent graph and any subgraphs.
|
||||
|
||||
```python
|
||||
for chunk in graph.stream(
|
||||
{"foo": "foo"},
|
||||
subgraphs=True, # (1)!
|
||||
stream_mode="updates",
|
||||
):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
1. Set `subgraphs=True` to stream outputs from subgraphs.
|
||||
|
||||
??? example "Stream from subgraphs"
|
||||
|
||||
```python
|
||||
from typing_extensions import TypedDict
|
||||
from langgraph.graph.state import StateGraph, START
|
||||
|
||||
# Define subgraph
|
||||
class SubgraphState(TypedDict):
|
||||
foo: str
|
||||
bar: str
|
||||
|
||||
def subgraph_node_1(state: SubgraphState):
|
||||
return {"bar": "bar"}
|
||||
|
||||
def subgraph_node_2(state: SubgraphState):
|
||||
# note that this node is using a state key ('bar') that is only available in the subgraph
|
||||
# and is sending update on the shared state key ('foo')
|
||||
return {"foo": state["foo"] + state["bar"]}
|
||||
|
||||
subgraph_builder = StateGraph(SubgraphState)
|
||||
subgraph_builder.add_node(subgraph_node_1)
|
||||
subgraph_builder.add_node(subgraph_node_2)
|
||||
subgraph_builder.add_edge(START, "subgraph_node_1")
|
||||
subgraph_builder.add_edge("subgraph_node_1", "subgraph_node_2")
|
||||
subgraph = subgraph_builder.compile()
|
||||
|
||||
# Define parent graph
|
||||
class ParentState(TypedDict):
|
||||
foo: str
|
||||
|
||||
def node_1(state: ParentState):
|
||||
return {"foo": "hi! " + state["foo"]}
|
||||
|
||||
builder = StateGraph(ParentState)
|
||||
builder.add_node("node_1", node_1)
|
||||
builder.add_node("node_2", subgraph)
|
||||
builder.add_edge(START, "node_1")
|
||||
builder.add_edge("node_1", "node_2")
|
||||
graph = builder.compile()
|
||||
|
||||
for chunk in graph.stream(
|
||||
{"foo": "foo"},
|
||||
stream_mode="updates",
|
||||
subgraphs=True, # (1)!
|
||||
):
|
||||
print(chunk)
|
||||
```
|
||||
|
||||
1. Set `subgraphs=True` to stream outputs from subgraphs.
|
||||
|
||||
```
|
||||
((), {'node_1': {'foo': 'hi! foo'}})
|
||||
(('node_2:e58e5673-a661-ebb0-70d4-e298a7fc28b7',), {'subgraph_node_1': {'bar': 'bar'}})
|
||||
(('node_2:e58e5673-a661-ebb0-70d4-e298a7fc28b7',), {'subgraph_node_2': {'foo': 'hi! foobar'}})
|
||||
((), {'node_2': {'foo': 'hi! foobar'}})
|
||||
|
||||
@@ -112,7 +112,7 @@ nav:
|
||||
- LangGraph APIs:
|
||||
- Graph API:
|
||||
- Overview: concepts/low_level.md
|
||||
- Use the Graph API: how-tos/graph-api.ipynb
|
||||
- Use the Graph API: how-tos/graph-api.md
|
||||
- Functional API:
|
||||
- Overview: concepts/functional_api.md
|
||||
- Use the Functional API: how-tos/use-functional-api.md
|
||||
@@ -150,12 +150,13 @@ nav:
|
||||
- Use Server API: cloud/how-tos/human_in_the_loop_time_travel.md
|
||||
- Subgraphs:
|
||||
- Overview: concepts/subgraphs.md
|
||||
- Use subgraphs: how-tos/subgraph.ipynb
|
||||
- Use subgraphs: how-tos/subgraph.md
|
||||
- Multi-agent:
|
||||
- Overview: concepts/multi_agent.md
|
||||
- Prebuilt implementation: agents/multi-agent.md
|
||||
- Custom implementation: how-tos/multi_agent.ipynb
|
||||
- Custom implementation: how-tos/multi_agent.md
|
||||
- MCP:
|
||||
- Overview: concepts/mcp.md
|
||||
- Use MCP: agents/mcp.md
|
||||
- Server API: concepts/server-mcp.md
|
||||
- Evaluation:
|
||||
@@ -259,9 +260,10 @@ nav:
|
||||
- MCP Adapters: reference/mcp.md
|
||||
- LangGraph Platform:
|
||||
- Server API: cloud/reference/api/api_ref.md
|
||||
- Control Plane API: cloud/reference/api/api_ref_control_plane.md
|
||||
- CLI: cloud/reference/cli.md
|
||||
- SDK (Python): cloud/reference/sdk/python_sdk_ref.md
|
||||
- SDK (JS/TS): cloud/reference/sdk/js_ts_sdk_ref.md
|
||||
- SDK (JS/TS): https://langchain-ai.github.io/langgraphjs/reference/modules/sdk.html
|
||||
- RemoteGraph: reference/remote_graph.md
|
||||
- Environment variables: cloud/reference/env_var.md
|
||||
|
||||
|
||||
@@ -2448,7 +2448,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.60"
|
||||
version = "0.3.67"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@@ -2459,9 +2459,9 @@ dependencies = [
|
||||
{ name = "tenacity" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/75/95129aaada92980a002a31e002610a80af3c8967ae7884710372e89cdde0/langchain_core-0.3.60.tar.gz", hash = "sha256:63dd1bdf7939816115399522661ca85a2f3686a61440f2f46ebd86d1b028595b", size = 557456, upload-time = "2025-05-15T15:23:23.642Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/40/875af0194024d0006874f061958fa417d3500bbfdc9a57e1bd1c2f4e6ed2/langchain_core-0.3.67.tar.gz", hash = "sha256:2c14aa44a0e78e014e96d7f2f8916ac109d0a0ba87ed67ee25bf7296bed7e7ba", size = 561952, upload-time = "2025-06-30T17:09:35.142Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/bc/344f5b11fdfe0e27f7064d2e829921a791461dc32e5ed285fe6325518c26/langchain_core-0.3.60-py3-none-any.whl", hash = "sha256:2ccdf06b12e699b1b0962bc02837056c075b4981c3d13f82a4d4c30bb22ea3dc", size = 437890, upload-time = "2025-05-15T15:23:22.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/2b/a0d283089c6d08c12d47dca39a55029ff714e939ec04f4560420426ab613/langchain_core-0.3.67-py3-none-any.whl", hash = "sha256:b699f1f24b24fa2747c05e2daa280aa64478a51e01a4e82c7f8e20b6167dfa99", size = 440237, upload-time = "2025-06-30T17:09:33.323Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2590,7 +2590,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
source = { editable = "../libs/langgraph" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -2894,7 +2894,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
source = { editable = "../libs/prebuilt" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -2903,7 +2903,7 @@ dependencies = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "langchain-core", specifier = ">=0.3.22" },
|
||||
{ name = "langchain-core", specifier = ">=0.3.67" },
|
||||
{ name = "langgraph-checkpoint", editable = "../libs/checkpoint" },
|
||||
]
|
||||
|
||||
@@ -2989,7 +2989,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langsmith"
|
||||
version = "0.3.42"
|
||||
version = "0.3.45"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
@@ -3000,9 +3000,9 @@ dependencies = [
|
||||
{ name = "requests-toolbelt" },
|
||||
{ name = "zstandard" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/44/fe171c0b0fb0377b191aebf0b7779e0c7b2a53693c6a01ddad737212495d/langsmith-0.3.42.tar.gz", hash = "sha256:2b5cbc450ab808b992362aac6943bb1d285579aa68a3a8be901d30a393458f25", size = 345619, upload-time = "2025-05-03T03:07:17.873Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/86/b941012013260f95af2e90a3d9415af4a76a003a28412033fc4b09f35731/langsmith-0.3.45.tar.gz", hash = "sha256:1df3c6820c73ed210b2c7bc5cdb7bfa19ddc9126cd03fdf0da54e2e171e6094d", size = 348201, upload-time = "2025-06-05T05:10:28.948Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/89/8e/e8a58e0abaae3f3ac4702e9ca35d1fc6159711556b64ffd0e247771a3f12/langsmith-0.3.42-py3-none-any.whl", hash = "sha256:18114327f3364385dae4026ebfd57d1c1cb46d8f80931098f0f10abe533475ff", size = 360334, upload-time = "2025-05-03T03:07:15.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/f4/c206c0888f8a506404cb4f16ad89593bdc2f70cf00de26a1a0a7a76ad7a3/langsmith-0.3.45-py3-none-any.whl", hash = "sha256:5b55f0518601fa65f3bb6b1a3100379a96aa7b3ed5e9380581615ba9c65ed8ed", size = 363002, upload-time = "2025-06-05T05:10:27.228Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to extract images from the graph-api.ipynb notebook and save them to assets folder.
|
||||
"""
|
||||
|
||||
import json
|
||||
import base64
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
def extract_images_from_notebook(notebook_path, assets_dir):
|
||||
"""Extract images from notebook and save them to assets directory."""
|
||||
|
||||
# Read the notebook
|
||||
with open(notebook_path, 'r') as f:
|
||||
notebook = json.load(f)
|
||||
|
||||
# Create assets directory if it doesn't exist
|
||||
os.makedirs(assets_dir, exist_ok=True)
|
||||
|
||||
image_count = 0
|
||||
|
||||
# Process each cell
|
||||
for cell_idx, cell in enumerate(notebook['cells']):
|
||||
if cell['cell_type'] == 'code':
|
||||
# Check if this cell contains draw_mermaid_png
|
||||
source = ''.join(cell.get('source', []))
|
||||
if 'draw_mermaid_png' in source:
|
||||
print(f"Found draw_mermaid_png in cell {cell_idx}")
|
||||
|
||||
# Check for outputs with images
|
||||
if 'outputs' in cell:
|
||||
for output_idx, output in enumerate(cell['outputs']):
|
||||
if output.get('output_type') == 'display_data':
|
||||
data = output.get('data', {})
|
||||
|
||||
# Check for PNG data
|
||||
if 'image/png' in data:
|
||||
png_data = data['image/png']
|
||||
|
||||
# Decode base64 data
|
||||
try:
|
||||
image_bytes = base64.b64decode(png_data)
|
||||
|
||||
# Generate filename
|
||||
image_count += 1
|
||||
filename = f"graph_api_image_{image_count}.png"
|
||||
filepath = os.path.join(assets_dir, filename)
|
||||
|
||||
# Save the image
|
||||
with open(filepath, 'wb') as img_file:
|
||||
img_file.write(image_bytes)
|
||||
|
||||
print(f"Saved image: {filepath}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error decoding image {image_count}: {e}")
|
||||
|
||||
print(f"Extracted {image_count} images to {assets_dir}")
|
||||
return image_count
|
||||
|
||||
if __name__ == "__main__":
|
||||
notebook_path = "docs/docs/how-tos/graph-api.ipynb"
|
||||
assets_dir = "docs/docs/how-tos/assets"
|
||||
|
||||
if os.path.exists(notebook_path):
|
||||
count = extract_images_from_notebook(notebook_path, assets_dir)
|
||||
print(f"Successfully extracted {count} images")
|
||||
else:
|
||||
print(f"Notebook not found: {notebook_path}")
|
||||
@@ -1,28 +0,0 @@
|
||||
index.cjs
|
||||
index.js
|
||||
index.d.ts
|
||||
index.d.cts
|
||||
client.cjs
|
||||
client.js
|
||||
client.d.ts
|
||||
client.d.cts
|
||||
auth.cjs
|
||||
auth.js
|
||||
auth.d.ts
|
||||
auth.d.cts
|
||||
react.cjs
|
||||
react.js
|
||||
react.d.ts
|
||||
react.d.cts
|
||||
react-ui.cjs
|
||||
react-ui.js
|
||||
react-ui.d.ts
|
||||
react-ui.d.cts
|
||||
react-ui/server.cjs
|
||||
react-ui/server.js
|
||||
react-ui/server.d.ts
|
||||
react-ui/server.d.cts
|
||||
node_modules
|
||||
dist
|
||||
.yarn
|
||||
docs
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 LangChain, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,64 +1 @@
|
||||
# LangGraph JS/TS SDK
|
||||
|
||||
This repository contains the JS/TS SDK for interacting with the LangGraph REST API.
|
||||
|
||||
## Quick Start
|
||||
|
||||
To get started with the JS/TS SDK, [install the package](https://www.npmjs.com/package/@langchain/langgraph-sdk)
|
||||
|
||||
```bash
|
||||
yarn add @langchain/langgraph-sdk
|
||||
```
|
||||
|
||||
You will need a running LangGraph API server. If you're running a server locally using `langgraph-cli`, SDK will automatically point at `http://localhost:8123`, otherwise
|
||||
you would need to specify the server URL when creating a client.
|
||||
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client();
|
||||
|
||||
// List all assistants
|
||||
const assistants = await client.assistants.search({
|
||||
metadata: null,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
// We auto-create an assistant for each graph you register in config.
|
||||
const agent = assistants[0];
|
||||
|
||||
// Start a new thread
|
||||
const thread = await client.threads.create();
|
||||
|
||||
// Start a streaming run
|
||||
const messages = [{ role: "human", content: "what's the weather in la" }];
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
agent["assistant_id"],
|
||||
{
|
||||
input: { messages },
|
||||
}
|
||||
);
|
||||
|
||||
for await (const chunk of streamResponse) {
|
||||
console.log(chunk);
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
To generate documentation, run the following commands:
|
||||
|
||||
1. Generate docs.
|
||||
|
||||
yarn typedoc
|
||||
|
||||
1. Consolidate doc files into one markdown file.
|
||||
|
||||
npx concat-md --decrease-title-levels --ignore=js_ts_sdk_ref.md --start-title-level-at 2 docs > docs/js_ts_sdk_ref.md
|
||||
|
||||
1. Copy `js_ts_sdk_ref.md` to MkDocs directory.
|
||||
|
||||
cp docs/js_ts_sdk_ref.md ../../docs/docs/cloud/reference/sdk/js_ts_sdk_ref.md
|
||||
This repository has been moved to [langchain-ai/langgraphjs](https://github.com/langchain-ai/langgraphjs/tree/main/libs/sdk).
|
||||
@@ -1,17 +0,0 @@
|
||||
/** @type {import('jest').Config} */
|
||||
export default {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
moduleNameMapper: {
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1',
|
||||
},
|
||||
transform: {
|
||||
'^.+\\.tsx?$': [
|
||||
'ts-jest',
|
||||
{
|
||||
useESM: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
/**
|
||||
* @param {string} relativePath
|
||||
* @returns {string}
|
||||
*/
|
||||
function abs(relativePath) {
|
||||
return resolve(dirname(fileURLToPath(import.meta.url)), relativePath);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
internals: [/react/],
|
||||
entrypoints: {
|
||||
index: "index",
|
||||
client: "client",
|
||||
auth: "auth/index",
|
||||
react: "react/index",
|
||||
"react-ui": "react-ui/index",
|
||||
"react-ui/server": "react-ui/server/index",
|
||||
},
|
||||
tsConfigPath: resolve("./tsconfig.json"),
|
||||
cjsSource: "./dist-cjs",
|
||||
cjsDestination: "./dist",
|
||||
additionalGitignorePaths: ["docs"],
|
||||
abs,
|
||||
};
|
||||
@@ -1,147 +0,0 @@
|
||||
{
|
||||
"name": "@langchain/langgraph-sdk",
|
||||
"version": "0.0.89",
|
||||
"description": "Client library for interacting with the LangGraph API",
|
||||
"type": "module",
|
||||
"packageManager": "yarn@1.22.19",
|
||||
"scripts": {
|
||||
"clean": "rm -rf dist/ dist-cjs/",
|
||||
"build": "yarn clean && yarn lc_build --create-entrypoints --pre --tree-shaking",
|
||||
"prepack": "yarn run build",
|
||||
"format": "prettier --write src",
|
||||
"lint": "prettier --check src && tsc --noEmit",
|
||||
"test": "vitest",
|
||||
"typedoc": "typedoc && typedoc src/react/index.ts --out docs/react --options typedoc.react.json && typedoc src/auth/index.ts --out docs/auth --options typedoc.auth.json"
|
||||
},
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"p-queue": "^6.6.2",
|
||||
"p-retry": "4",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@langchain/langgraph-api": "~0.0.41",
|
||||
"@langchain/core": "^0.3.61",
|
||||
"@langchain/langgraph": "^0.3.5",
|
||||
"@langchain/scripts": "^0.1.4",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@tsconfig/recommended": "^1.0.2",
|
||||
"@types/node": "^20.12.12",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"concat-md": "^0.5.1",
|
||||
"hono": "^4.8.2",
|
||||
"jsdom": "^26.1.0",
|
||||
"msw": "^2.8.2",
|
||||
"prettier": "^3.2.5",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"typedoc": "^0.27.7",
|
||||
"typedoc-plugin-markdown": "^4.4.2",
|
||||
"typescript": "^5.4.5",
|
||||
"vitest": "^3.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": ">=0.2.31 <0.4.0",
|
||||
"react": "^18 || ^19"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"@langchain/core": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": {
|
||||
"import": "./index.d.ts",
|
||||
"require": "./index.d.cts",
|
||||
"default": "./index.d.ts"
|
||||
},
|
||||
"import": "./index.js",
|
||||
"require": "./index.cjs"
|
||||
},
|
||||
"./client": {
|
||||
"types": {
|
||||
"import": "./client.d.ts",
|
||||
"require": "./client.d.cts",
|
||||
"default": "./client.d.ts"
|
||||
},
|
||||
"import": "./client.js",
|
||||
"require": "./client.cjs"
|
||||
},
|
||||
"./auth": {
|
||||
"types": {
|
||||
"import": "./auth.d.ts",
|
||||
"require": "./auth.d.cts",
|
||||
"default": "./auth.d.ts"
|
||||
},
|
||||
"import": "./auth.js",
|
||||
"require": "./auth.cjs"
|
||||
},
|
||||
"./react": {
|
||||
"types": {
|
||||
"import": "./react.d.ts",
|
||||
"require": "./react.d.cts",
|
||||
"default": "./react.d.ts"
|
||||
},
|
||||
"import": "./react.js",
|
||||
"require": "./react.cjs"
|
||||
},
|
||||
"./react-ui": {
|
||||
"types": {
|
||||
"import": "./react-ui.d.ts",
|
||||
"require": "./react-ui.d.cts",
|
||||
"default": "./react-ui.d.ts"
|
||||
},
|
||||
"import": "./react-ui.js",
|
||||
"require": "./react-ui.cjs"
|
||||
},
|
||||
"./react-ui/server": {
|
||||
"types": {
|
||||
"import": "./react-ui/server.d.ts",
|
||||
"require": "./react-ui/server.d.cts",
|
||||
"default": "./react-ui/server.d.ts"
|
||||
},
|
||||
"import": "./react-ui/server.js",
|
||||
"require": "./react-ui/server.cjs"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"dist/",
|
||||
"index.cjs",
|
||||
"index.js",
|
||||
"index.d.ts",
|
||||
"index.d.cts",
|
||||
"client.cjs",
|
||||
"client.js",
|
||||
"client.d.ts",
|
||||
"client.d.cts",
|
||||
"auth.cjs",
|
||||
"auth.js",
|
||||
"auth.d.ts",
|
||||
"auth.d.cts",
|
||||
"react.cjs",
|
||||
"react.js",
|
||||
"react.d.ts",
|
||||
"react.d.cts",
|
||||
"react-ui.cjs",
|
||||
"react-ui.js",
|
||||
"react-ui.d.ts",
|
||||
"react-ui.d.cts",
|
||||
"react-ui/server.cjs",
|
||||
"react-ui/server.js",
|
||||
"react-ui/server.d.ts",
|
||||
"react-ui/server.d.cts"
|
||||
]
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
const HTTP_STATUS_MAPPING: { [key: number]: string } = {
|
||||
100: "Continue",
|
||||
101: "Switching Protocols",
|
||||
102: "Processing",
|
||||
103: "Early Hints",
|
||||
200: "OK",
|
||||
201: "Created",
|
||||
202: "Accepted",
|
||||
203: "Non-Authoritative Information",
|
||||
204: "No Content",
|
||||
205: "Reset Content",
|
||||
206: "Partial Content",
|
||||
207: "Multi-Status",
|
||||
208: "Already Reported",
|
||||
226: "IM Used",
|
||||
300: "Multiple Choices",
|
||||
301: "Moved Permanently",
|
||||
302: "Found",
|
||||
303: "See Other",
|
||||
304: "Not Modified",
|
||||
305: "Use Proxy",
|
||||
307: "Temporary Redirect",
|
||||
308: "Permanent Redirect",
|
||||
400: "Bad Request",
|
||||
401: "Unauthorized",
|
||||
402: "Payment Required",
|
||||
403: "Forbidden",
|
||||
404: "Not Found",
|
||||
405: "Method Not Allowed",
|
||||
406: "Not Acceptable",
|
||||
407: "Proxy Authentication Required",
|
||||
408: "Request Timeout",
|
||||
409: "Conflict",
|
||||
410: "Gone",
|
||||
411: "Length Required",
|
||||
412: "Precondition Failed",
|
||||
413: "Request Entity Too Large",
|
||||
414: "Request-URI Too Long",
|
||||
415: "Unsupported Media Type",
|
||||
416: "Requested Range Not Satisfiable",
|
||||
417: "Expectation Failed",
|
||||
418: "I'm a Teapot",
|
||||
421: "Misdirected Request",
|
||||
422: "Unprocessable Entity",
|
||||
423: "Locked",
|
||||
424: "Failed Dependency",
|
||||
425: "Too Early",
|
||||
426: "Upgrade Required",
|
||||
428: "Precondition Required",
|
||||
429: "Too Many Requests",
|
||||
431: "Request Header Fields Too Large",
|
||||
451: "Unavailable For Legal Reasons",
|
||||
500: "Internal Server Error",
|
||||
501: "Not Implemented",
|
||||
502: "Bad Gateway",
|
||||
503: "Service Unavailable",
|
||||
504: "Gateway Timeout",
|
||||
505: "HTTP Version Not Supported",
|
||||
506: "Variant Also Negotiates",
|
||||
507: "Insufficient Storage",
|
||||
508: "Loop Detected",
|
||||
510: "Not Extended",
|
||||
511: "Network Authentication Required",
|
||||
};
|
||||
|
||||
export class HTTPException extends Error {
|
||||
status: number;
|
||||
headers: HeadersInit;
|
||||
|
||||
constructor(
|
||||
status: number,
|
||||
options?: { message?: string; headers?: HeadersInit; cause?: unknown },
|
||||
) {
|
||||
super(options?.message ?? HTTP_STATUS_MAPPING[status] ?? "Unknown error", {
|
||||
cause: options?.cause,
|
||||
});
|
||||
this.status = status;
|
||||
this.headers = options?.headers ?? {};
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import type {
|
||||
AuthenticateCallback,
|
||||
AnyCallback,
|
||||
CallbackEvent,
|
||||
OnCallback,
|
||||
BaseAuthReturn,
|
||||
ToUserLike,
|
||||
BaseUser,
|
||||
} from "./types.js";
|
||||
|
||||
export class Auth<
|
||||
TExtra = {},
|
||||
TAuthReturn extends BaseAuthReturn = BaseAuthReturn,
|
||||
TUser extends BaseUser = ToUserLike<TAuthReturn>,
|
||||
> {
|
||||
/**
|
||||
* @internal
|
||||
* @ignore
|
||||
*/
|
||||
"~handlerCache": {
|
||||
authenticate?: AuthenticateCallback<BaseAuthReturn>;
|
||||
callbacks?: Record<string, AnyCallback>;
|
||||
} = {};
|
||||
|
||||
authenticate<T extends BaseAuthReturn>(
|
||||
cb: AuthenticateCallback<T>,
|
||||
): Auth<TExtra, T> {
|
||||
this["~handlerCache"].authenticate = cb;
|
||||
return this as unknown as Auth<TExtra, T>;
|
||||
}
|
||||
|
||||
on<T extends CallbackEvent>(event: T, callback: OnCallback<T, TUser>): this {
|
||||
this["~handlerCache"].callbacks ??= {};
|
||||
const events: string[] = Array.isArray(event) ? event : [event];
|
||||
for (const event of events) {
|
||||
this["~handlerCache"].callbacks[event] = callback as AnyCallback;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export type {
|
||||
Filters as AuthFilters,
|
||||
EventValueMap as AuthEventValueMap,
|
||||
} from "./types.js";
|
||||
export { HTTPException } from "./error.js";
|
||||
@@ -1,411 +0,0 @@
|
||||
type Maybe<T> = T | null | undefined;
|
||||
type PromiseMaybe<T> = Promise<T> | T;
|
||||
|
||||
interface AssistantConfig {
|
||||
tags?: Maybe<string[]>;
|
||||
recursion_limit?: Maybe<number>;
|
||||
configurable?: Maybe<{
|
||||
thread_id?: Maybe<string>;
|
||||
thread_ts?: Maybe<string>;
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface AssistantCreate {
|
||||
assistant_id?: Maybe<string>;
|
||||
metadata?: Maybe<Record<string, unknown>>;
|
||||
config?: Maybe<AssistantConfig>;
|
||||
if_exists?: Maybe<"raise" | "do_nothing">;
|
||||
name?: Maybe<string>;
|
||||
graph_id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface AssistantRead {
|
||||
assistant_id: string;
|
||||
metadata?: Maybe<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface AssistantUpdate {
|
||||
assistant_id: string;
|
||||
metadata?: Maybe<Record<string, unknown>>;
|
||||
config?: Maybe<AssistantConfig>;
|
||||
graph_id?: Maybe<string>;
|
||||
name?: Maybe<string>;
|
||||
version?: Maybe<number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface AssistantDelete {
|
||||
assistant_id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface AssistantSearch {
|
||||
graph_id?: Maybe<string>;
|
||||
metadata?: Maybe<Record<string, unknown>>;
|
||||
limit?: Maybe<number>;
|
||||
offset?: Maybe<number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface ThreadCreate {
|
||||
thread_id?: Maybe<string>;
|
||||
metadata?: Maybe<Record<string, unknown>>;
|
||||
if_exists?: Maybe<"raise" | "do_nothing">;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface ThreadRead {
|
||||
thread_id?: Maybe<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface ThreadUpdate {
|
||||
thread_id?: Maybe<string>;
|
||||
metadata?: Maybe<Record<string, unknown>>;
|
||||
action?: Maybe<"interrupt" | "rollback">;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface ThreadDelete {
|
||||
thread_id?: Maybe<string>;
|
||||
run_id?: Maybe<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface ThreadSearch {
|
||||
thread_id?: Maybe<string>;
|
||||
status?: Maybe<"idle" | "busy" | "interrupted" | "error" | (string & {})>;
|
||||
metadata?: Maybe<Record<string, unknown>>;
|
||||
values?: Maybe<Record<string, unknown>>;
|
||||
limit?: Maybe<number>;
|
||||
offset?: Maybe<number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface CronCreate {
|
||||
payload?: Maybe<Record<string, unknown>>;
|
||||
schedule: string;
|
||||
cron_id?: Maybe<string>;
|
||||
thread_id?: Maybe<string>;
|
||||
user_id?: Maybe<string>;
|
||||
end_time?: Maybe<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface CronRead {
|
||||
cron_id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface CronUpdate {
|
||||
cron_id: string;
|
||||
payload?: Maybe<Record<string, unknown>>;
|
||||
schedule?: Maybe<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface CronDelete {
|
||||
cron_id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface CronSearch {
|
||||
assistant_id?: Maybe<string>;
|
||||
thread_id?: Maybe<string>;
|
||||
limit?: Maybe<number>;
|
||||
offset?: Maybe<number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface StorePut {
|
||||
namespace: string[];
|
||||
key: string;
|
||||
value: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface StoreGet {
|
||||
namespace: Maybe<string[]>;
|
||||
key: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface StoreSearch {
|
||||
namespace?: Maybe<string[]>;
|
||||
filter?: Maybe<Record<string, unknown>>;
|
||||
limit?: Maybe<number>;
|
||||
offset?: Maybe<number>;
|
||||
query?: Maybe<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface StoreListNamespaces {
|
||||
namespace?: Maybe<string[]>;
|
||||
suffix?: Maybe<string[]>;
|
||||
max_depth?: Maybe<number>;
|
||||
limit?: Maybe<number>;
|
||||
offset?: Maybe<number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface StoreDelete {
|
||||
namespace?: Maybe<string[]>;
|
||||
key: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
interface RunsCreate {
|
||||
thread_id?: Maybe<string>;
|
||||
assistant_id: string;
|
||||
run_id: string;
|
||||
status: Maybe<
|
||||
"pending" | "running" | "error" | "success" | "timeout" | "interrupted"
|
||||
>;
|
||||
metadata?: Maybe<Record<string, unknown>>;
|
||||
prevent_insert_if_inflight?: Maybe<boolean>;
|
||||
multitask_strategy?: Maybe<"interrupt" | "rollback" | "reject" | "enqueue">;
|
||||
if_not_exists?: Maybe<"reject" | "create">;
|
||||
after_seconds?: Maybe<number>;
|
||||
kwargs: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface EventValueMap {
|
||||
["threads:create"]: ThreadCreate;
|
||||
["threads:read"]: ThreadRead;
|
||||
["threads:update"]: ThreadUpdate;
|
||||
["threads:delete"]: ThreadDelete;
|
||||
["threads:search"]: ThreadSearch;
|
||||
["threads:create_run"]: RunsCreate;
|
||||
|
||||
["assistants:create"]: AssistantCreate;
|
||||
["assistants:read"]: AssistantRead;
|
||||
["assistants:update"]: AssistantUpdate;
|
||||
["assistants:delete"]: AssistantDelete;
|
||||
["assistants:search"]: AssistantSearch;
|
||||
|
||||
["crons:create"]: CronCreate;
|
||||
["crons:read"]: CronRead;
|
||||
["crons:update"]: CronUpdate;
|
||||
["crons:delete"]: CronDelete;
|
||||
["crons:search"]: CronSearch;
|
||||
|
||||
["store:put"]: StorePut;
|
||||
["store:get"]: StoreGet;
|
||||
["store:search"]: StoreSearch;
|
||||
["store:list_namespaces"]: StoreListNamespaces;
|
||||
["store:delete"]: StoreDelete;
|
||||
}
|
||||
interface ResourceType {
|
||||
threads:
|
||||
| "threads:create"
|
||||
| "threads:read"
|
||||
| "threads:update"
|
||||
| "threads:delete"
|
||||
| "threads:search"
|
||||
| "threads:create_run";
|
||||
|
||||
assistants:
|
||||
| "assistants:create"
|
||||
| "assistants:read"
|
||||
| "assistants:update"
|
||||
| "assistants:delete"
|
||||
| "assistants:search";
|
||||
crons:
|
||||
| "crons:create"
|
||||
| "crons:read"
|
||||
| "crons:update"
|
||||
| "crons:delete"
|
||||
| "crons:search";
|
||||
|
||||
store:
|
||||
| "store:put"
|
||||
| "store:get"
|
||||
| "store:search"
|
||||
| "store:list_namespaces"
|
||||
| "store:delete";
|
||||
}
|
||||
interface ActionType {
|
||||
"*:create": "threads:create" | "assistants:create" | "crons:create";
|
||||
|
||||
"*:read": "threads:read" | "assistants:read" | "crons:read";
|
||||
|
||||
"*:update": "threads:update" | "assistants:update" | "crons:update";
|
||||
|
||||
"*:delete":
|
||||
| "threads:delete"
|
||||
| "assistants:delete"
|
||||
| "crons:delete"
|
||||
| "store:delete";
|
||||
|
||||
"*:search":
|
||||
| "threads:search"
|
||||
| "assistants:search"
|
||||
| "crons:search"
|
||||
| "store:search";
|
||||
|
||||
"*:create_run": "threads:create_run";
|
||||
|
||||
"*:put": "store:put";
|
||||
|
||||
"*:get": "store:get";
|
||||
|
||||
"*:list_namespaces": "store:list_namespaces";
|
||||
}
|
||||
|
||||
export type BaseAuthReturn =
|
||||
| {
|
||||
is_authenticated?: boolean;
|
||||
display_name?: string;
|
||||
identity: string;
|
||||
permissions: string[];
|
||||
}
|
||||
| string;
|
||||
|
||||
export interface BaseUser {
|
||||
is_authenticated: boolean;
|
||||
display_name: string;
|
||||
identity: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
export type ToUserLike<T extends BaseAuthReturn> = T extends string
|
||||
? {
|
||||
is_authenticated: boolean;
|
||||
display_name: string;
|
||||
identity: string;
|
||||
permissions: string[];
|
||||
}
|
||||
: Omit<T, "is_authenticated" | "display_name"> & {
|
||||
is_authenticated: boolean;
|
||||
display_name: string;
|
||||
};
|
||||
|
||||
type CallbackParameter<
|
||||
Event extends string = string,
|
||||
Resource extends string = string,
|
||||
Action extends string = string,
|
||||
Value extends unknown = unknown,
|
||||
TUser extends BaseUser = BaseUser,
|
||||
> = {
|
||||
event: Event;
|
||||
resource: Resource;
|
||||
action: Action;
|
||||
value: Value;
|
||||
user: TUser;
|
||||
permissions: string[];
|
||||
};
|
||||
|
||||
type ContextMap = {
|
||||
[EventType in keyof EventValueMap]: CallbackParameter<
|
||||
EventType,
|
||||
EventType extends `${infer Resource}:${string}` ? Resource : never,
|
||||
EventType extends `${string}:${infer Action}` ? Action : never,
|
||||
EventValueMap[EventType],
|
||||
BaseUser
|
||||
>;
|
||||
};
|
||||
|
||||
type ActionCallbackParameter<
|
||||
T extends keyof ActionType,
|
||||
TUser extends BaseUser = BaseUser,
|
||||
> = ContextMap[ActionType[T]] & { user: TUser };
|
||||
type AuthCallbackParameter<
|
||||
T extends keyof EventValueMap,
|
||||
TUser extends BaseUser = BaseUser,
|
||||
> = ContextMap[T] & { user: TUser };
|
||||
type ResourceCallbackParameter<
|
||||
T extends keyof ResourceType,
|
||||
TUser extends BaseUser = BaseUser,
|
||||
> = ContextMap[ResourceType[T]] & { user: TUser };
|
||||
|
||||
export type Filters<TKey extends string | number | symbol> = {
|
||||
[key in TKey]: string | { [op in "$contains" | "$eq"]?: string };
|
||||
};
|
||||
|
||||
export interface AuthenticateCallback<T extends BaseAuthReturn> {
|
||||
(request: Request): PromiseMaybe<T>;
|
||||
}
|
||||
|
||||
type OnKey = keyof ResourceType | keyof ActionType | keyof EventValueMap;
|
||||
|
||||
type OnSingleParameter<
|
||||
T extends OnKey,
|
||||
TUser extends BaseUser = BaseUser,
|
||||
> = T extends keyof ResourceType
|
||||
? ResourceCallbackParameter<T, TUser>
|
||||
: T extends keyof ActionType
|
||||
? ActionCallbackParameter<T, TUser>
|
||||
: T extends keyof EventValueMap
|
||||
? AuthCallbackParameter<T, TUser>
|
||||
: never;
|
||||
|
||||
type OnParameter<
|
||||
T extends "*" | OnKey | OnKey[],
|
||||
TUser extends BaseUser = BaseUser,
|
||||
> = T extends OnKey[]
|
||||
? OnSingleParameter<T[number], TUser>
|
||||
: T extends "*"
|
||||
? AuthCallbackParameter<keyof EventValueMap, TUser>
|
||||
: T extends OnKey
|
||||
? OnSingleParameter<T, TUser>
|
||||
: never;
|
||||
|
||||
export type AnyCallback = (
|
||||
request: CallbackParameter,
|
||||
) => void | boolean | Filters<string>;
|
||||
|
||||
export type CallbackEvent = "*" | OnKey | OnKey[];
|
||||
|
||||
export type OnCallback<
|
||||
T extends CallbackEvent,
|
||||
TUser extends BaseUser = BaseUser,
|
||||
TMetadata extends Record<string, unknown> = Record<string, unknown>,
|
||||
> = (
|
||||
request: OnParameter<T, TUser>,
|
||||
) => void | boolean | Filters<keyof TMetadata>;
|
||||
@@ -1,56 +0,0 @@
|
||||
export { Client, getApiKey } from "./client.js";
|
||||
export type { ClientConfig, RequestHook } from "./client.js";
|
||||
|
||||
export type {
|
||||
Assistant,
|
||||
AssistantBase,
|
||||
AssistantGraph,
|
||||
AssistantVersion,
|
||||
Checkpoint,
|
||||
Config,
|
||||
Cron,
|
||||
CronCreateForThreadResponse,
|
||||
CronCreateResponse,
|
||||
DefaultValues,
|
||||
GraphSchema,
|
||||
Interrupt,
|
||||
Item,
|
||||
ListNamespaceResponse,
|
||||
Metadata,
|
||||
Run,
|
||||
SearchItem,
|
||||
SearchItemsResponse,
|
||||
Thread,
|
||||
ThreadState,
|
||||
ThreadStatus,
|
||||
ThreadTask,
|
||||
} from "./schema.js";
|
||||
export { overrideFetchImplementation } from "./singletons/fetch.js";
|
||||
|
||||
export type {
|
||||
Command,
|
||||
OnConflictBehavior,
|
||||
RunsInvokePayload,
|
||||
} from "./types.js";
|
||||
export type {
|
||||
AIMessage,
|
||||
FunctionMessage,
|
||||
HumanMessage,
|
||||
Message,
|
||||
RemoveMessage,
|
||||
SystemMessage,
|
||||
ToolMessage,
|
||||
} from "./types.messages.js";
|
||||
export type {
|
||||
CustomStreamEvent,
|
||||
DebugStreamEvent,
|
||||
ErrorStreamEvent,
|
||||
EventsStreamEvent,
|
||||
FeedbackStreamEvent,
|
||||
MessagesStreamEvent,
|
||||
MessagesTupleStreamEvent,
|
||||
MetadataStreamEvent,
|
||||
StreamMode,
|
||||
UpdatesStreamEvent,
|
||||
ValuesStreamEvent,
|
||||
} from "./types.stream.js";
|
||||
@@ -1,279 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useStream } from "../react/index.js";
|
||||
import type { UIMessage } from "./types.js";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import * as JsxRuntime from "react/jsx-runtime";
|
||||
import type { UseStream } from "../react/stream.js";
|
||||
|
||||
const UseStreamContext = React.createContext<{
|
||||
stream: ReturnType<typeof useStream>;
|
||||
meta: unknown;
|
||||
}>(null!);
|
||||
|
||||
type BagTemplate = {
|
||||
ConfigurableType?: Record<string, unknown>;
|
||||
InterruptType?: unknown;
|
||||
CustomEventType?: unknown;
|
||||
UpdateType?: unknown;
|
||||
MetaType?: unknown;
|
||||
};
|
||||
|
||||
type GetMetaType<Bag extends BagTemplate> = Bag extends { MetaType: unknown }
|
||||
? Bag["MetaType"]
|
||||
: unknown;
|
||||
|
||||
interface UseStreamContext<
|
||||
StateType extends Record<string, unknown> = Record<string, unknown>,
|
||||
Bag extends BagTemplate = BagTemplate,
|
||||
> extends UseStream<StateType, Bag> {
|
||||
meta?: GetMetaType<Bag>;
|
||||
}
|
||||
|
||||
export function useStreamContext<
|
||||
StateType extends Record<string, unknown> = Record<string, unknown>,
|
||||
Bag extends {
|
||||
ConfigurableType?: Record<string, unknown>;
|
||||
InterruptType?: unknown;
|
||||
CustomEventType?: unknown;
|
||||
UpdateType?: unknown;
|
||||
MetaType?: unknown;
|
||||
} = BagTemplate,
|
||||
>(): UseStreamContext<StateType, Bag> {
|
||||
const ctx = React.useContext(UseStreamContext);
|
||||
if (!ctx) {
|
||||
throw new Error(
|
||||
"useStreamContext must be used within a LoadExternalComponent",
|
||||
);
|
||||
}
|
||||
|
||||
return new Proxy(ctx, {
|
||||
get(target, prop: keyof UseStreamContext<StateType, Bag>) {
|
||||
if (prop === "meta") return target.meta;
|
||||
return target.stream[prop];
|
||||
},
|
||||
}) as unknown as UseStreamContext<StateType, Bag>;
|
||||
}
|
||||
|
||||
interface ComponentTarget {
|
||||
comp: React.FunctionComponent | React.ComponentClass;
|
||||
target: HTMLElement;
|
||||
}
|
||||
|
||||
class ComponentStore {
|
||||
private cache: Record<string, ComponentTarget> = {};
|
||||
private boundCache: Record<
|
||||
string,
|
||||
{
|
||||
subscribe: (onStoreChange: () => void) => () => void;
|
||||
getSnapshot: () => ComponentTarget | undefined;
|
||||
}
|
||||
> = {};
|
||||
private callbacks: Record<
|
||||
string,
|
||||
((
|
||||
comp: React.FunctionComponent | React.ComponentClass,
|
||||
el: HTMLElement,
|
||||
) => void)[]
|
||||
> = {};
|
||||
|
||||
respond(
|
||||
shadowRootId: string,
|
||||
comp: React.FunctionComponent | React.ComponentClass,
|
||||
targetElement: HTMLElement,
|
||||
) {
|
||||
this.cache[shadowRootId] = { comp, target: targetElement };
|
||||
this.callbacks[shadowRootId]?.forEach((c) => c(comp, targetElement));
|
||||
}
|
||||
|
||||
getBoundStore(shadowRootId: string) {
|
||||
this.boundCache[shadowRootId] ??= {
|
||||
subscribe: (onStoreChange: () => void) => {
|
||||
this.callbacks[shadowRootId] ??= [];
|
||||
this.callbacks[shadowRootId].push(onStoreChange);
|
||||
return () => {
|
||||
this.callbacks[shadowRootId] = this.callbacks[shadowRootId].filter(
|
||||
(c) => c !== onStoreChange,
|
||||
);
|
||||
};
|
||||
},
|
||||
getSnapshot: () => this.cache[shadowRootId],
|
||||
};
|
||||
|
||||
return this.boundCache[shadowRootId];
|
||||
}
|
||||
}
|
||||
|
||||
const COMPONENT_STORE = new ComponentStore();
|
||||
const EXT_STORE_SYMBOL = Symbol.for("LGUI_EXT_STORE");
|
||||
const REQUIRE_SYMBOL = Symbol.for("LGUI_REQUIRE");
|
||||
const REQUIRE_EXTRA_SYMBOL = Symbol.for("LGUI_REQUIRE_EXTRA");
|
||||
|
||||
interface LoadExternalComponentProps
|
||||
extends Pick<React.HTMLAttributes<HTMLDivElement>, "style" | "className"> {
|
||||
/** Stream of the assistant */
|
||||
stream: ReturnType<typeof useStream>;
|
||||
|
||||
/** Namespace of UI components. Defaults to assistant ID. */
|
||||
namespace?: string;
|
||||
|
||||
/** UI message to be rendered */
|
||||
message: UIMessage;
|
||||
|
||||
/** Additional context to be passed to the child component */
|
||||
meta?: unknown;
|
||||
|
||||
/** Fallback to be rendered when the component is loading */
|
||||
fallback?: React.ReactNode | Record<string, React.ReactNode>;
|
||||
|
||||
/**
|
||||
* Map of components that can be rendered directly without fetching the UI code
|
||||
* from the server.
|
||||
*/
|
||||
components?: Record<string, React.FunctionComponent | React.ComponentClass>;
|
||||
}
|
||||
|
||||
const isIterable = (value: unknown): value is Iterable<unknown> =>
|
||||
value != null && typeof value === "object" && Symbol.iterator in value;
|
||||
|
||||
const isPromise = (value: unknown): value is Promise<unknown> =>
|
||||
value != null &&
|
||||
typeof value === "object" &&
|
||||
"then" in value &&
|
||||
typeof value.then === "function";
|
||||
|
||||
const isReactNode = (value: unknown): value is React.ReactNode => {
|
||||
if (React.isValidElement(value)) return true;
|
||||
if (value == null) return true;
|
||||
if (
|
||||
typeof value === "string" ||
|
||||
typeof value === "number" ||
|
||||
typeof value === "bigint" ||
|
||||
typeof value === "boolean"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isIterable(value)) return true;
|
||||
if (isPromise(value)) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export function LoadExternalComponent({
|
||||
stream,
|
||||
namespace,
|
||||
message,
|
||||
meta,
|
||||
fallback,
|
||||
components,
|
||||
...props
|
||||
}: LoadExternalComponentProps) {
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const id = React.useId();
|
||||
const shadowRootId = `child-shadow-${id}`;
|
||||
|
||||
const store = React.useMemo(
|
||||
() => COMPONENT_STORE.getBoundStore(shadowRootId),
|
||||
[shadowRootId],
|
||||
);
|
||||
const state = React.useSyncExternalStore(store.subscribe, store.getSnapshot);
|
||||
|
||||
const clientComponent = components?.[message.name];
|
||||
const hasClientComponent = clientComponent != null;
|
||||
|
||||
const fallbackComponent = isReactNode(fallback)
|
||||
? fallback
|
||||
: typeof fallback === "object" && fallback != null
|
||||
? fallback?.[message.name]
|
||||
: null;
|
||||
|
||||
const uiNamespace = namespace ?? stream.assistantId;
|
||||
const uiClient = stream.client["~ui"];
|
||||
React.useEffect(() => {
|
||||
if (hasClientComponent) return;
|
||||
uiClient.getComponent(uiNamespace, message.name).then((html) => {
|
||||
const dom = ref.current;
|
||||
if (!dom) return;
|
||||
const root = dom.shadowRoot ?? dom.attachShadow({ mode: "open" });
|
||||
const fragment = document
|
||||
.createRange()
|
||||
.createContextualFragment(
|
||||
html.replace("{{shadowRootId}}", shadowRootId),
|
||||
);
|
||||
root.appendChild(fragment);
|
||||
});
|
||||
}, [uiClient, uiNamespace, message.name, shadowRootId, hasClientComponent]);
|
||||
|
||||
if (hasClientComponent) {
|
||||
return (
|
||||
<UseStreamContext.Provider value={{ stream, meta }}>
|
||||
{React.createElement(clientComponent, message.props)}
|
||||
</UseStreamContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id={shadowRootId} ref={ref} {...props} />
|
||||
|
||||
<UseStreamContext.Provider value={{ stream, meta }}>
|
||||
{state?.target != null
|
||||
? ReactDOM.createPortal(
|
||||
React.createElement(state.comp, message.props),
|
||||
state.target,
|
||||
)
|
||||
: fallbackComponent}
|
||||
</UseStreamContext.Provider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
[EXT_STORE_SYMBOL]: ComponentStore;
|
||||
[REQUIRE_SYMBOL]: (name: string) => unknown;
|
||||
[REQUIRE_EXTRA_SYMBOL]: Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
|
||||
export function experimental_loadShare(name: string, module: unknown) {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
window[REQUIRE_EXTRA_SYMBOL] ??= {};
|
||||
window[REQUIRE_EXTRA_SYMBOL][name] = module;
|
||||
}
|
||||
|
||||
export function bootstrapUiContext() {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
window[EXT_STORE_SYMBOL] = COMPONENT_STORE;
|
||||
window[REQUIRE_SYMBOL] = (name: string) => {
|
||||
if (name === "react") return React;
|
||||
if (name === "react-dom") return ReactDOM;
|
||||
if (name === "react/jsx-runtime") return JsxRuntime;
|
||||
if (name === "@langchain/langgraph-sdk/react") return { useStream };
|
||||
if (name === "@langchain/langgraph-sdk/react-ui") {
|
||||
return {
|
||||
useStreamContext,
|
||||
LoadExternalComponent: () => {
|
||||
throw new Error("Nesting LoadExternalComponent is not supported");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
window[REQUIRE_EXTRA_SYMBOL] != null &&
|
||||
typeof window[REQUIRE_EXTRA_SYMBOL] === "object" &&
|
||||
name in window[REQUIRE_EXTRA_SYMBOL]
|
||||
) {
|
||||
return window[REQUIRE_EXTRA_SYMBOL][name];
|
||||
}
|
||||
|
||||
throw new Error(`Unknown module...: ${name}`);
|
||||
};
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { bootstrapUiContext } from "./client.js";
|
||||
bootstrapUiContext();
|
||||
|
||||
export {
|
||||
useStreamContext,
|
||||
LoadExternalComponent,
|
||||
experimental_loadShare,
|
||||
} from "./client.js";
|
||||
export {
|
||||
uiMessageReducer,
|
||||
isUIMessage,
|
||||
isRemoveUIMessage,
|
||||
type UIMessage,
|
||||
type RemoveUIMessage,
|
||||
} from "./types.js";
|
||||
@@ -1,6 +0,0 @@
|
||||
export { typedUi } from "./server.js";
|
||||
export {
|
||||
uiMessageReducer,
|
||||
type UIMessage,
|
||||
type RemoveUIMessage,
|
||||
} from "../types.js";
|
||||
@@ -1,99 +0,0 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import type { ComponentPropsWithoutRef, ElementType } from "react";
|
||||
import type { RemoveUIMessage, UIMessage } from "../types.js";
|
||||
|
||||
interface MessageLike {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to send and persist UI messages. Accepts a map of component names to React components
|
||||
* as type argument to provide type safety. Will also write to the `options?.stateKey` state.
|
||||
*
|
||||
* @param config LangGraphRunnableConfig
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
export const typedUi = <Decl extends Record<string, ElementType>>(
|
||||
config: {
|
||||
writer?: (chunk: unknown) => void;
|
||||
runId?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
tags?: string[];
|
||||
runName?: string;
|
||||
configurable?: {
|
||||
__pregel_send?: (writes_: [string, unknown][]) => void;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
},
|
||||
options?: {
|
||||
/** The key to write the UI messages to. Defaults to `ui`. */
|
||||
stateKey?: string;
|
||||
},
|
||||
) => {
|
||||
type PropMap = { [K in keyof Decl]: ComponentPropsWithoutRef<Decl[K]> };
|
||||
let items: (UIMessage | RemoveUIMessage)[] = [];
|
||||
const stateKey = options?.stateKey ?? "ui";
|
||||
|
||||
const runId = (config.metadata?.run_id as string | undefined) ?? config.runId;
|
||||
if (!runId) throw new Error("run_id is required");
|
||||
|
||||
function handlePush<K extends keyof PropMap & string>(
|
||||
message: {
|
||||
id?: string;
|
||||
name: K;
|
||||
props: PropMap[K];
|
||||
metadata?: Record<string, unknown>;
|
||||
},
|
||||
options?: { message?: MessageLike; merge?: boolean },
|
||||
): UIMessage<K, PropMap[K]>;
|
||||
|
||||
function handlePush<K extends keyof PropMap & string>(
|
||||
message: {
|
||||
id?: string;
|
||||
name: K;
|
||||
props: Partial<PropMap[K]>;
|
||||
metadata?: Record<string, unknown>;
|
||||
},
|
||||
options: { message?: MessageLike; merge: true },
|
||||
): UIMessage<K, Partial<PropMap[K]>>;
|
||||
|
||||
function handlePush<K extends keyof PropMap & string>(
|
||||
message: {
|
||||
id?: string;
|
||||
name: K;
|
||||
props: PropMap[K] | Partial<PropMap[K]>;
|
||||
metadata?: Record<string, unknown>;
|
||||
},
|
||||
options?: { message?: MessageLike; merge?: boolean },
|
||||
): UIMessage<K, PropMap[K] | Partial<PropMap[K]>> {
|
||||
const evt: UIMessage<K, PropMap[K] | Partial<PropMap[K]>> = {
|
||||
type: "ui" as const,
|
||||
id: message?.id ?? uuidv4(),
|
||||
name: message?.name,
|
||||
props: message?.props,
|
||||
metadata: {
|
||||
merge: options?.merge || undefined,
|
||||
run_id: runId,
|
||||
tags: config.tags,
|
||||
name: config.runName,
|
||||
...message?.metadata,
|
||||
...(options?.message ? { message_id: options.message.id } : null),
|
||||
},
|
||||
};
|
||||
items.push(evt);
|
||||
config.writer?.(evt);
|
||||
config.configurable?.__pregel_send?.([[stateKey, evt]]);
|
||||
return evt;
|
||||
}
|
||||
|
||||
const handleDelete = (id: string): RemoveUIMessage => {
|
||||
const evt: RemoveUIMessage = { type: "remove-ui", id };
|
||||
items.push(evt);
|
||||
config.writer?.(evt);
|
||||
config.configurable?.__pregel_send?.([[stateKey, evt]]);
|
||||
return evt;
|
||||
};
|
||||
|
||||
return { push: handlePush, delete: handleDelete, items };
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
export interface UIMessage<
|
||||
TName extends string = string,
|
||||
TProps extends Record<string, unknown> = Record<string, unknown>,
|
||||
> {
|
||||
type: "ui";
|
||||
|
||||
id: string;
|
||||
name: TName;
|
||||
props: TProps;
|
||||
metadata?: {
|
||||
merge?: boolean;
|
||||
run_id?: string;
|
||||
name?: string;
|
||||
tags?: string[];
|
||||
message_id?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RemoveUIMessage {
|
||||
type: "remove-ui";
|
||||
id: string;
|
||||
}
|
||||
|
||||
export function isUIMessage(message: unknown): message is UIMessage {
|
||||
if (typeof message !== "object" || message == null) return false;
|
||||
if (!("type" in message)) return false;
|
||||
return message.type === "ui";
|
||||
}
|
||||
|
||||
export function isRemoveUIMessage(
|
||||
message: unknown,
|
||||
): message is RemoveUIMessage {
|
||||
if (typeof message !== "object" || message == null) return false;
|
||||
if (!("type" in message)) return false;
|
||||
return message.type === "remove-ui";
|
||||
}
|
||||
|
||||
export function uiMessageReducer(
|
||||
state: UIMessage[],
|
||||
update: UIMessage | RemoveUIMessage | (UIMessage | RemoveUIMessage)[],
|
||||
) {
|
||||
const events = Array.isArray(update) ? update : [update];
|
||||
let newState = state.slice();
|
||||
|
||||
for (const event of events) {
|
||||
if (event.type === "remove-ui") {
|
||||
newState = newState.filter((ui) => ui.id !== event.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
const index = state.findIndex((ui) => ui.id === event.id);
|
||||
if (index !== -1) {
|
||||
newState[index] =
|
||||
typeof event.metadata === "object" &&
|
||||
event.metadata != null &&
|
||||
event.metadata.merge
|
||||
? { ...event, props: { ...state[index].props, ...event.props } }
|
||||
: event;
|
||||
} else {
|
||||
newState.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import { ThreadState } from "../schema.js";
|
||||
|
||||
interface Node<StateType = any> {
|
||||
type: "node";
|
||||
value: ThreadState<StateType>;
|
||||
path: string[];
|
||||
}
|
||||
|
||||
interface Fork<StateType = any> {
|
||||
type: "fork";
|
||||
items: Array<Sequence<StateType>>;
|
||||
}
|
||||
|
||||
interface Sequence<StateType = any> {
|
||||
type: "sequence";
|
||||
items: Array<Node<StateType> | Fork<StateType>>;
|
||||
}
|
||||
|
||||
interface ValidFork<StateType = any> {
|
||||
type: "fork";
|
||||
items: Array<ValidSequence<StateType>>;
|
||||
}
|
||||
|
||||
interface ValidSequence<StateType = any> {
|
||||
type: "sequence";
|
||||
items: [Node<StateType>, ...(Node<StateType> | ValidFork<StateType>)[]];
|
||||
}
|
||||
|
||||
// forks
|
||||
export type CheckpointBranchPath = string[];
|
||||
|
||||
export type MessageBranch = {
|
||||
current: CheckpointBranchPath;
|
||||
options: CheckpointBranchPath[];
|
||||
};
|
||||
|
||||
export function DebugSegmentsView(props: {
|
||||
sequence: ValidSequence<ThreadState>;
|
||||
}) {
|
||||
const concatContent = (value: ThreadState<any>) => {
|
||||
let content;
|
||||
try {
|
||||
content = value.values?.messages?.at(-1)?.content ?? "";
|
||||
} catch {
|
||||
content = JSON.stringify(value.values);
|
||||
}
|
||||
|
||||
content = content.replace(/(\n|\r\n)/g, "");
|
||||
if (content.length <= 23) return content;
|
||||
return `${content.slice(0, 10)}...${content.slice(-10)}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{props.sequence.items.map((item, index) => {
|
||||
if (item.type === "fork") {
|
||||
return (
|
||||
<div key={index}>
|
||||
{item.items.map((fork, idx) => {
|
||||
const [first] = fork.items;
|
||||
return (
|
||||
<details key={idx}>
|
||||
<summary>
|
||||
Fork{" "}
|
||||
<span className="font-mono">
|
||||
...{first.path.at(-1)?.slice(-4)}
|
||||
</span>
|
||||
</summary>
|
||||
<div className="ml-4">
|
||||
<DebugSegmentsView sequence={fork} />
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.type === "node") {
|
||||
return (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<pre>
|
||||
({item.value.metadata?.step}) ...
|
||||
{item.value.checkpoint.checkpoint_id?.slice(-4)} (
|
||||
{item.value.metadata?.source}): {concatContent(item.value)}
|
||||
</pre>
|
||||
<button
|
||||
type="button"
|
||||
className="border rounded-sm text-sm py-0.5 px-1 text-muted-foreground"
|
||||
onClick={() => console.log(item.path, item.value)}
|
||||
>
|
||||
console.log
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export {
|
||||
useStream,
|
||||
type MessageMetadata,
|
||||
type UseStream,
|
||||
type UseStreamOptions,
|
||||
} from "./stream.js";
|
||||
@@ -1,334 +0,0 @@
|
||||
import type { JSONSchema7 } from "json-schema";
|
||||
|
||||
type Optional<T> = T | null | undefined;
|
||||
|
||||
export type RunStatus =
|
||||
| "pending"
|
||||
| "running"
|
||||
| "error"
|
||||
| "success"
|
||||
| "timeout"
|
||||
| "interrupted";
|
||||
|
||||
export type ThreadStatus = "idle" | "busy" | "interrupted" | "error";
|
||||
|
||||
type MultitaskStrategy = "reject" | "interrupt" | "rollback" | "enqueue";
|
||||
|
||||
export type CancelAction = "interrupt" | "rollback";
|
||||
|
||||
export type Config = {
|
||||
/**
|
||||
* Tags for this call and any sub-calls (eg. a Chain calling an LLM).
|
||||
* You can use these to filter calls.
|
||||
*/
|
||||
tags?: string[];
|
||||
|
||||
/**
|
||||
* Maximum number of times a call can recurse.
|
||||
* If not provided, defaults to 25.
|
||||
*/
|
||||
recursion_limit?: number;
|
||||
|
||||
/**
|
||||
* Runtime values for attributes previously made configurable on this Runnable.
|
||||
*/
|
||||
configurable?: {
|
||||
/**
|
||||
* ID of the thread
|
||||
*/
|
||||
thread_id?: Optional<string>;
|
||||
|
||||
/**
|
||||
* Timestamp of the state checkpoint
|
||||
*/
|
||||
checkpoint_id?: Optional<string>;
|
||||
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
export interface GraphSchema {
|
||||
/**
|
||||
* The ID of the graph.
|
||||
*/
|
||||
graph_id: string;
|
||||
|
||||
/**
|
||||
* The schema for the input state.
|
||||
* Missing if unable to generate JSON schema from graph.
|
||||
*/
|
||||
input_schema?: JSONSchema7 | null | undefined;
|
||||
|
||||
/**
|
||||
* The schema for the output state.
|
||||
* Missing if unable to generate JSON schema from graph.
|
||||
*/
|
||||
output_schema?: JSONSchema7 | null | undefined;
|
||||
|
||||
/**
|
||||
* The schema for the graph state.
|
||||
* Missing if unable to generate JSON schema from graph.
|
||||
*/
|
||||
state_schema?: JSONSchema7 | null | undefined;
|
||||
|
||||
/**
|
||||
* The schema for the graph config.
|
||||
* Missing if unable to generate JSON schema from graph.
|
||||
*/
|
||||
config_schema?: JSONSchema7 | null | undefined;
|
||||
}
|
||||
|
||||
export type Subgraphs = Record<string, GraphSchema>;
|
||||
|
||||
export type Metadata = Optional<{
|
||||
source?: "input" | "loop" | "update" | (string & {});
|
||||
|
||||
step?: number;
|
||||
|
||||
writes?: Record<string, unknown> | null;
|
||||
|
||||
parents?: Record<string, string>;
|
||||
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
|
||||
export interface AssistantBase {
|
||||
/** The ID of the assistant. */
|
||||
assistant_id: string;
|
||||
|
||||
/** The ID of the graph. */
|
||||
graph_id: string;
|
||||
|
||||
/** The assistant config. */
|
||||
config: Config;
|
||||
|
||||
/** The time the assistant was created. */
|
||||
created_at: string;
|
||||
|
||||
/** The assistant metadata. */
|
||||
metadata: Metadata;
|
||||
|
||||
/** The version of the assistant. */
|
||||
version: number;
|
||||
|
||||
/** The name of the assistant */
|
||||
name: string;
|
||||
|
||||
/** The description of the assistant */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface AssistantVersion extends AssistantBase {}
|
||||
|
||||
export interface Assistant extends AssistantBase {
|
||||
/** The last time the assistant was updated. */
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface AssistantGraph {
|
||||
nodes: Array<{
|
||||
id: string | number;
|
||||
name?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data?: Record<string, any> | string;
|
||||
metadata?: unknown;
|
||||
}>;
|
||||
edges: Array<{
|
||||
source: string;
|
||||
target: string;
|
||||
data?: string;
|
||||
conditional?: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interrupt thrown inside a thread.
|
||||
*/
|
||||
export interface Interrupt<TValue = unknown> {
|
||||
value?: TValue;
|
||||
when: "during" | (string & {});
|
||||
resumable?: boolean;
|
||||
ns?: string[];
|
||||
}
|
||||
|
||||
export interface Thread<ValuesType = DefaultValues> {
|
||||
/** The ID of the thread. */
|
||||
thread_id: string;
|
||||
|
||||
/** The time the thread was created. */
|
||||
created_at: string;
|
||||
|
||||
/** The last time the thread was updated. */
|
||||
updated_at: string;
|
||||
|
||||
/** The thread metadata. */
|
||||
metadata: Metadata;
|
||||
|
||||
/** The status of the thread */
|
||||
status: ThreadStatus;
|
||||
|
||||
/** The current state of the thread. */
|
||||
values: ValuesType;
|
||||
|
||||
/** Interrupts which were thrown in this thread */
|
||||
interrupts: Record<string, Array<Interrupt>>;
|
||||
}
|
||||
|
||||
export interface Cron {
|
||||
/** The ID of the cron */
|
||||
cron_id: string;
|
||||
|
||||
/** The ID of the assistant */
|
||||
assistant_id: string;
|
||||
|
||||
/** The ID of the thread */
|
||||
thread_id: Optional<string>;
|
||||
|
||||
/** The end date to stop running the cron. */
|
||||
end_time: Optional<string>;
|
||||
|
||||
/** The schedule to run, cron format. */
|
||||
schedule: string;
|
||||
|
||||
/** The time the cron was created. */
|
||||
created_at: string;
|
||||
|
||||
/** The last time the cron was updated. */
|
||||
updated_at: string;
|
||||
|
||||
/** The run payload to use for creating new run. */
|
||||
payload: Record<string, unknown>;
|
||||
|
||||
/** The user ID of the cron */
|
||||
user_id: Optional<string>;
|
||||
|
||||
/** The next run date of the cron */
|
||||
next_run_date: Optional<string>;
|
||||
|
||||
/** The metadata of the cron */
|
||||
metadata: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type DefaultValues = Record<string, unknown>[] | Record<string, unknown>;
|
||||
|
||||
export interface ThreadState<ValuesType = DefaultValues> {
|
||||
/** The state values */
|
||||
values: ValuesType;
|
||||
|
||||
/** The next nodes to execute. If empty, the thread is done until new input is received */
|
||||
next: string[];
|
||||
|
||||
/** Checkpoint of the thread state */
|
||||
checkpoint: Checkpoint;
|
||||
|
||||
/** Metadata for this state */
|
||||
metadata: Metadata;
|
||||
|
||||
/** Time of state creation */
|
||||
created_at: Optional<string>;
|
||||
|
||||
/** The parent checkpoint. If missing, this is the root checkpoint */
|
||||
parent_checkpoint: Optional<Checkpoint>;
|
||||
|
||||
/** Tasks to execute in this step. If already attempted, may contain an error */
|
||||
tasks: Array<ThreadTask>;
|
||||
}
|
||||
|
||||
export interface ThreadTask {
|
||||
id: string;
|
||||
name: string;
|
||||
result?: unknown;
|
||||
error: Optional<string>;
|
||||
interrupts: Array<Interrupt>;
|
||||
checkpoint: Optional<Checkpoint>;
|
||||
state: Optional<ThreadState>;
|
||||
}
|
||||
|
||||
export interface Run {
|
||||
/** The ID of the run */
|
||||
run_id: string;
|
||||
|
||||
/** The ID of the thread */
|
||||
thread_id: string;
|
||||
|
||||
/** The assistant that wwas used for this run */
|
||||
assistant_id: string;
|
||||
|
||||
/** The time the run was created */
|
||||
created_at: string;
|
||||
|
||||
/** The last time the run was updated */
|
||||
updated_at: string;
|
||||
|
||||
/** The status of the run. */
|
||||
status: RunStatus;
|
||||
|
||||
/** Run metadata */
|
||||
metadata: Metadata;
|
||||
|
||||
/** Strategy to handle concurrent runs on the same thread */
|
||||
multitask_strategy: Optional<MultitaskStrategy>;
|
||||
}
|
||||
|
||||
export type Checkpoint = {
|
||||
thread_id: string;
|
||||
checkpoint_ns: string;
|
||||
checkpoint_id: Optional<string>;
|
||||
checkpoint_map: Optional<Record<string, unknown>>;
|
||||
};
|
||||
|
||||
export interface ListNamespaceResponse {
|
||||
namespaces: string[][];
|
||||
}
|
||||
export interface Item {
|
||||
namespace: string[];
|
||||
key: string;
|
||||
value: Record<string, any>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface SearchItem extends Item {
|
||||
score?: number;
|
||||
}
|
||||
export interface SearchItemsResponse {
|
||||
items: SearchItem[];
|
||||
}
|
||||
|
||||
export interface CronCreateResponse {
|
||||
cron_id: string;
|
||||
assistant_id: string;
|
||||
thread_id: string | undefined;
|
||||
user_id: string;
|
||||
payload: Record<string, unknown>;
|
||||
schedule: string;
|
||||
next_run_date: string;
|
||||
end_time: string | undefined;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
metadata: Metadata;
|
||||
}
|
||||
|
||||
export interface CronCreateForThreadResponse
|
||||
extends Omit<CronCreateResponse, "thread_id"> {
|
||||
thread_id: string;
|
||||
}
|
||||
|
||||
export type AssistantSortBy =
|
||||
| "assistant_id"
|
||||
| "graph_id"
|
||||
| "name"
|
||||
| "created_at"
|
||||
| "updated_at";
|
||||
|
||||
export type ThreadSortBy = "thread_id" | "status" | "created_at" | "updated_at";
|
||||
|
||||
export type CronSortBy =
|
||||
| "cron_id"
|
||||
| "assistant_id"
|
||||
| "thread_id"
|
||||
| "created_at"
|
||||
| "updated_at"
|
||||
| "next_run_date";
|
||||
|
||||
export type SortOrder = "asc" | "desc";
|
||||
@@ -1,29 +0,0 @@
|
||||
// Wrap the default fetch call due to issues with illegal invocations
|
||||
// in some environments:
|
||||
// https://stackoverflow.com/questions/69876859/why-does-bind-fix-failed-to-execute-fetch-on-window-illegal-invocation-err
|
||||
// @ts-expect-error Broad typing to support a range of fetch implementations
|
||||
const DEFAULT_FETCH_IMPLEMENTATION = (...args: any[]) => fetch(...args);
|
||||
|
||||
const LANGSMITH_FETCH_IMPLEMENTATION_KEY = Symbol.for(
|
||||
"lg:fetch_implementation",
|
||||
);
|
||||
|
||||
/**
|
||||
* Overrides the fetch implementation used for LangSmith calls.
|
||||
* You should use this if you need to use an implementation of fetch
|
||||
* other than the default global (e.g. for dealing with proxies).
|
||||
* @param fetch The new fetch function to use.
|
||||
*/
|
||||
export const overrideFetchImplementation = (fetch: (...args: any[]) => any) => {
|
||||
(globalThis as any)[LANGSMITH_FETCH_IMPLEMENTATION_KEY] = fetch;
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const _getFetchImplementation: () => (...args: any[]) => any = () => {
|
||||
return (
|
||||
(globalThis as any)[LANGSMITH_FETCH_IMPLEMENTATION_KEY] ??
|
||||
DEFAULT_FETCH_IMPLEMENTATION
|
||||
);
|
||||
};
|
||||
@@ -1,201 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { Client } from "../client.js";
|
||||
import { overrideFetchImplementation } from "../singletons/fetch.js";
|
||||
|
||||
describe.each([["global"], ["mocked"]])(
|
||||
"Client uses %s fetch",
|
||||
(description: string) => {
|
||||
let globalFetchMock: ReturnType<typeof vi.fn>;
|
||||
let overriddenFetch: ReturnType<typeof vi.fn>;
|
||||
|
||||
let expectedFetchMock: ReturnType<typeof vi.fn>;
|
||||
let unexpectedFetchMock: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
globalFetchMock = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
batch_ingest_config: {
|
||||
use_multipart_endpoint: true,
|
||||
},
|
||||
}),
|
||||
text: () => Promise.resolve(""),
|
||||
headers: new Headers({}),
|
||||
}),
|
||||
);
|
||||
overriddenFetch = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
batch_ingest_config: {
|
||||
use_multipart_endpoint: true,
|
||||
},
|
||||
}),
|
||||
text: () => Promise.resolve(""),
|
||||
headers: new Headers({}),
|
||||
}),
|
||||
);
|
||||
expectedFetchMock =
|
||||
description === "mocked" ? overriddenFetch : globalFetchMock;
|
||||
unexpectedFetchMock =
|
||||
description === "mocked" ? globalFetchMock : overriddenFetch;
|
||||
|
||||
if (description === "mocked") {
|
||||
overrideFetchImplementation(overriddenFetch);
|
||||
} else {
|
||||
overrideFetchImplementation(globalFetchMock);
|
||||
}
|
||||
// Mock global fetch
|
||||
(globalThis as any).fetch = globalFetchMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("createRuns", () => {
|
||||
it("should create an example with the given input and generation", async () => {
|
||||
const client = new Client({ apiKey: "test-api-key" });
|
||||
|
||||
const thread = await client.threads.create();
|
||||
expect(expectedFetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(unexpectedFetchMock).not.toHaveBeenCalled();
|
||||
|
||||
vi.clearAllMocks(); // Clear all mocks before the next operation
|
||||
|
||||
// Then clear & run the function
|
||||
await client.runs.create(thread.thread_id, "somegraph", {
|
||||
input: { foo: "bar" },
|
||||
});
|
||||
expect(expectedFetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(unexpectedFetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("header coalescing", () => {
|
||||
it("should properly merge headers with conflicting name casing", async () => {
|
||||
const client = new Client({ apiKey: "test-api-key" });
|
||||
await (client.threads as any).fetch("/test", {
|
||||
headers: { "X-Api-Key": "custom-value" },
|
||||
});
|
||||
expect(expectedFetchMock).toHaveBeenCalledWith(
|
||||
expect.any(URL),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"x-api-key": "custom-value",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should properly merge headers from multiple sources", async () => {
|
||||
const client = new Client({
|
||||
apiKey: "test-api-key",
|
||||
defaultHeaders: {
|
||||
"x-default": "default-value",
|
||||
"x-override": "default-value",
|
||||
},
|
||||
});
|
||||
|
||||
await (client.threads as any).fetch("/test", {
|
||||
headers: {
|
||||
"x-custom": "custom-value",
|
||||
"x-override": "custom-value",
|
||||
},
|
||||
});
|
||||
|
||||
expect(expectedFetchMock).toHaveBeenCalledWith(
|
||||
expect.any(URL),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"x-api-key": "test-api-key",
|
||||
"x-default": "default-value",
|
||||
"x-custom": "custom-value",
|
||||
"x-override": "custom-value",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Test with null/undefined values
|
||||
await (client.threads as any).fetch("/test", {
|
||||
headers: {
|
||||
"x-null": null,
|
||||
"x-undefined": undefined,
|
||||
"x-empty": "",
|
||||
},
|
||||
});
|
||||
|
||||
expect(expectedFetchMock).toHaveBeenCalledWith(
|
||||
expect.any(URL),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"x-api-key": "test-api-key",
|
||||
"x-default": "default-value",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(expectedFetchMock).not.toHaveBeenCalledWith(
|
||||
expect.any(URL),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"x-null": null,
|
||||
"x-undefined": undefined,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle Headers object input", async () => {
|
||||
const client = new Client({ apiKey: "test-api-key" });
|
||||
const headers = new Headers();
|
||||
headers.append("x-custom", "custom-value");
|
||||
headers.append("x-multi", "value1");
|
||||
headers.append("x-multi", "value2");
|
||||
|
||||
await (client.threads as any).fetch("/test", { headers });
|
||||
|
||||
expect(expectedFetchMock).toHaveBeenCalledWith(
|
||||
expect.any(URL),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"x-api-key": "test-api-key",
|
||||
"x-custom": "custom-value",
|
||||
"x-multi": "value1, value2",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle array of header tuples", async () => {
|
||||
const client = new Client({
|
||||
apiKey: "test-api-key",
|
||||
defaultHeaders: {
|
||||
"x-custom": "custom-value",
|
||||
},
|
||||
});
|
||||
const headers = [
|
||||
["x-multi", "value1"],
|
||||
["x-multi", "value2"],
|
||||
];
|
||||
|
||||
await (client.threads as any).fetch("/test", { headers });
|
||||
|
||||
expect(expectedFetchMock).toHaveBeenCalledWith(
|
||||
expect.any(URL),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
"x-api-key": "test-api-key",
|
||||
"x-custom": "custom-value",
|
||||
"x-multi": "value1, value2",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1,183 +0,0 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { Readable } from "node:stream";
|
||||
import { IterableReadableStream } from "../utils/stream.js";
|
||||
import { BytesLineDecoder, SSEDecoder } from "../utils/sse.js";
|
||||
|
||||
const gather = async <T>(stream: ReadableStream<T>): Promise<T[]> => {
|
||||
const results: T[] = [];
|
||||
const iterator = IterableReadableStream.fromReadableStream(stream);
|
||||
for await (const chunk of iterator) results.push(chunk);
|
||||
return results;
|
||||
};
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
const textDecoder = new TextDecoder();
|
||||
|
||||
describe("BytesLineDecoder", () => {
|
||||
const createStream = (chunks: Uint8Array[]) => {
|
||||
return Readable.toWeb(Readable.from(chunks)) as ReadableStream<Uint8Array>;
|
||||
};
|
||||
|
||||
test("handles single line with newline", async () => {
|
||||
const input = createStream([textEncoder.encode("hello\n")]);
|
||||
const decoded = input.pipeThrough(BytesLineDecoder());
|
||||
const results = await gather(decoded);
|
||||
|
||||
expect(results.length).toBe(1);
|
||||
expect(textDecoder.decode(results[0])).toBe("hello");
|
||||
});
|
||||
|
||||
test("handles multiple lines", async () => {
|
||||
const input = createStream([textEncoder.encode("line1\nline2\nline3\n")]);
|
||||
const decoded = input.pipeThrough(BytesLineDecoder());
|
||||
const results = await gather(decoded);
|
||||
|
||||
expect(results.length).toBe(3);
|
||||
expect(textDecoder.decode(results[0])).toBe("line1");
|
||||
expect(textDecoder.decode(results[1])).toBe("line2");
|
||||
expect(textDecoder.decode(results[2])).toBe("line3");
|
||||
});
|
||||
|
||||
test("handles split chunks", async () => {
|
||||
const input = createStream([
|
||||
textEncoder.encode("li"),
|
||||
textEncoder.encode("ne1\nli"),
|
||||
textEncoder.encode("ne2\n"),
|
||||
]);
|
||||
const decoded = input.pipeThrough(BytesLineDecoder());
|
||||
const results = await gather(decoded);
|
||||
|
||||
expect(results.length).toBe(2);
|
||||
expect(textDecoder.decode(results[0])).toBe("line1");
|
||||
expect(textDecoder.decode(results[1])).toBe("line2");
|
||||
});
|
||||
|
||||
test("handles CR LF line endings", async () => {
|
||||
const input = createStream([textEncoder.encode("line1\r\nline2\r\n")]);
|
||||
const decoded = input.pipeThrough(BytesLineDecoder());
|
||||
const results = await gather(decoded);
|
||||
|
||||
expect(results.length).toBe(2);
|
||||
expect(textDecoder.decode(results[0])).toBe("line1");
|
||||
expect(textDecoder.decode(results[1])).toBe("line2");
|
||||
});
|
||||
|
||||
test("handles split CR LF", async () => {
|
||||
const input = createStream([
|
||||
textEncoder.encode("line1\r"),
|
||||
textEncoder.encode("\nline2\r\n"),
|
||||
]);
|
||||
const decoded = input.pipeThrough(BytesLineDecoder());
|
||||
const results = await gather(decoded);
|
||||
|
||||
expect(results.length).toBe(2);
|
||||
expect(textDecoder.decode(results[0])).toBe("line1");
|
||||
expect(textDecoder.decode(results[1])).toBe("line2");
|
||||
});
|
||||
|
||||
test("handles stale line", async () => {
|
||||
const input = createStream([textEncoder.encode("hello")]);
|
||||
const decoded = input.pipeThrough(BytesLineDecoder());
|
||||
const results = await gather(decoded);
|
||||
|
||||
expect(results.length).toBe(1);
|
||||
expect(textDecoder.decode(results[0])).toBe("hello");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SSEDecoder", () => {
|
||||
const createStream = (lines: string[]) => {
|
||||
return Readable.toWeb(
|
||||
Readable.from(lines.map((line) => textEncoder.encode(line))),
|
||||
) as ReadableStream<Uint8Array>;
|
||||
};
|
||||
|
||||
test("decodes simple event", async () => {
|
||||
const input = createStream([
|
||||
"event: test\n",
|
||||
'data: {"message": "hello"}\n',
|
||||
"\n",
|
||||
]);
|
||||
const decoded = input
|
||||
.pipeThrough(BytesLineDecoder())
|
||||
.pipeThrough(SSEDecoder());
|
||||
|
||||
const results = await gather(decoded);
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0]).toEqual({
|
||||
event: "test",
|
||||
data: { message: "hello" },
|
||||
});
|
||||
});
|
||||
|
||||
test("ignores comments", async () => {
|
||||
const input = createStream([
|
||||
": this is a comment\n",
|
||||
"event: test\n",
|
||||
'data: {"message": "hello"}\n',
|
||||
]);
|
||||
const decoded = input
|
||||
.pipeThrough(BytesLineDecoder())
|
||||
.pipeThrough(SSEDecoder());
|
||||
|
||||
const results = await gather(decoded);
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0]).toEqual({
|
||||
event: "test",
|
||||
data: { message: "hello" },
|
||||
});
|
||||
});
|
||||
|
||||
test("handles multiple events", async () => {
|
||||
const input = createStream([
|
||||
"event: test1\n",
|
||||
'data: {"message": "hello"}\n',
|
||||
"\n",
|
||||
"event: test2\n",
|
||||
'data: {"message": "world"}\n',
|
||||
"\n",
|
||||
]);
|
||||
const decoded = input
|
||||
.pipeThrough(BytesLineDecoder())
|
||||
.pipeThrough(SSEDecoder());
|
||||
|
||||
const results = await gather(decoded);
|
||||
expect(results.length).toBe(2);
|
||||
expect(results[0]).toEqual({
|
||||
event: "test1",
|
||||
data: { message: "hello" },
|
||||
});
|
||||
expect(results[1]).toEqual({
|
||||
event: "test2",
|
||||
data: { message: "world" },
|
||||
});
|
||||
});
|
||||
|
||||
test("end event without data", async () => {
|
||||
const input = createStream(["event: test\n"]);
|
||||
const decoded = input
|
||||
.pipeThrough(BytesLineDecoder())
|
||||
.pipeThrough(SSEDecoder());
|
||||
|
||||
const results = await gather(decoded);
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0]).toEqual({
|
||||
event: "test",
|
||||
data: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("end event without newline", async () => {
|
||||
const input = createStream(["event: end"]);
|
||||
const decoded = input
|
||||
.pipeThrough(BytesLineDecoder())
|
||||
.pipeThrough(SSEDecoder());
|
||||
|
||||
const results = await gather(decoded);
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0]).toEqual({
|
||||
event: "end",
|
||||
data: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,450 +0,0 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { setupServer } from "msw/node";
|
||||
import { http } from "msw";
|
||||
import { useStream } from "../react/stream.js";
|
||||
import type { Message } from "../types.messages.js";
|
||||
|
||||
import { StateGraph, MessagesAnnotation, START } from "@langchain/langgraph";
|
||||
import { MemorySaver } from "@langchain/langgraph-checkpoint";
|
||||
import { FakeStreamingChatModel } from "@langchain/core/utils/testing";
|
||||
import { AIMessage } from "@langchain/core/messages";
|
||||
import { createEmbedServer } from "@langchain/langgraph-api/experimental/embed";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { useState } from "react";
|
||||
|
||||
const threads = (() => {
|
||||
const THREADS: Record<
|
||||
string,
|
||||
{ thread_id: string; metadata: Record<string, unknown> }
|
||||
> = {};
|
||||
|
||||
return {
|
||||
get: async (id: string) => THREADS[id],
|
||||
put: async (
|
||||
threadId: string,
|
||||
{ metadata }: { metadata?: Record<string, unknown> },
|
||||
) => {
|
||||
THREADS[threadId] = { thread_id: threadId, metadata: metadata ?? {} };
|
||||
},
|
||||
delete: async (threadId: string) => {
|
||||
delete THREADS[threadId];
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
const checkpointer = new MemorySaver();
|
||||
|
||||
const model = new FakeStreamingChatModel({ responses: [new AIMessage("Hey")] });
|
||||
const agent = new StateGraph(MessagesAnnotation)
|
||||
.addNode("agent", async (state: { messages: Message[] }) => {
|
||||
const response = await model.invoke(state.messages);
|
||||
return { messages: [response] };
|
||||
})
|
||||
.addEdge(START, "agent")
|
||||
.compile();
|
||||
|
||||
const app = createEmbedServer({ graph: { agent }, checkpointer, threads });
|
||||
const server = setupServer(http.all("*", (ctx) => app.fetch(ctx.request)));
|
||||
|
||||
function TestChatComponent() {
|
||||
const { messages, isLoading, error, submit, stop } = useStream({
|
||||
assistantId: "agent",
|
||||
apiKey: "test-api-key",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="messages">
|
||||
{messages.map((msg, i) => (
|
||||
<div key={msg.id ?? i} data-testid={`message-${i}`}>
|
||||
{typeof msg.content === "string"
|
||||
? msg.content
|
||||
: JSON.stringify(msg.content)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div data-testid="loading">
|
||||
{isLoading ? "Loading..." : "Not loading"}
|
||||
</div>
|
||||
{error ? <div data-testid="error">{String(error)}</div> : null}
|
||||
<button
|
||||
data-testid="submit"
|
||||
onClick={() =>
|
||||
submit({ messages: [{ content: "Hello", type: "human" }] })
|
||||
}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
<button data-testid="stop" onClick={stop}>
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
describe("useStream", () => {
|
||||
beforeEach(() => server.listen());
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
server.close();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders initial state correctly", () => {
|
||||
render(<TestChatComponent />);
|
||||
|
||||
expect(screen.getByTestId("loading")).toHaveTextContent("Not loading");
|
||||
expect(screen.getByTestId("messages")).toBeEmptyDOMElement();
|
||||
expect(screen.queryByTestId("error")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles message submission and streaming", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<TestChatComponent />);
|
||||
|
||||
// Check loading state
|
||||
await user.click(screen.getByTestId("submit"));
|
||||
expect(screen.getByTestId("loading")).toHaveTextContent("Loading...");
|
||||
|
||||
// Wait for messages to appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("message-0")).toHaveTextContent("Hello");
|
||||
expect(screen.getByTestId("message-1")).toHaveTextContent("Hey");
|
||||
});
|
||||
|
||||
// Check final state
|
||||
expect(screen.getByTestId("loading")).toHaveTextContent("Not loading");
|
||||
});
|
||||
|
||||
it("handles stop functionality", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<TestChatComponent />);
|
||||
|
||||
// Start streaming and stop immediately
|
||||
await user.click(screen.getByTestId("submit"));
|
||||
await user.click(screen.getByTestId("stop"));
|
||||
|
||||
// Check loading state is reset
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("loading")).toHaveTextContent("Not loading");
|
||||
});
|
||||
});
|
||||
|
||||
it("displays initial values immediately and clears them when submitting", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
function TestCachedComponent() {
|
||||
const { messages, values, submit } = useStream<{
|
||||
messages: Message[];
|
||||
}>({
|
||||
assistantId: "agent",
|
||||
apiKey: "test-api-key",
|
||||
initialValues: {
|
||||
messages: [
|
||||
{ id: "cached-1", type: "human", content: "Cached user message" },
|
||||
{ id: "cached-2", type: "ai", content: "Cached AI response" },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="messages">
|
||||
{messages.map((msg, i) => (
|
||||
<div
|
||||
key={msg.id ?? i}
|
||||
data-testid={
|
||||
msg.id?.includes("cached")
|
||||
? `message-cached-${i}`
|
||||
: `message-${i}`
|
||||
}
|
||||
>
|
||||
{typeof msg.content === "string"
|
||||
? msg.content
|
||||
: JSON.stringify(msg.content)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div data-testid="values">{JSON.stringify(values)}</div>
|
||||
<button
|
||||
data-testid="submit"
|
||||
onClick={() =>
|
||||
submit({ messages: [{ content: "Hello", type: "human" }] })
|
||||
}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(<TestCachedComponent />);
|
||||
|
||||
// Should immediately show cached messages
|
||||
expect(screen.getByTestId("message-cached-0")).toHaveTextContent(
|
||||
"Cached user message",
|
||||
);
|
||||
expect(screen.getByTestId("message-cached-1")).toHaveTextContent(
|
||||
"Cached AI response",
|
||||
);
|
||||
|
||||
// Values should include initial values
|
||||
expect(screen.getByTestId("values")).toHaveTextContent(
|
||||
"Cached user message",
|
||||
);
|
||||
|
||||
// Submitting should clear out the cached messages
|
||||
await user.click(screen.getByTestId("submit"));
|
||||
|
||||
// Wait for messages to appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("message-0")).toHaveTextContent("Hello");
|
||||
expect(screen.getByTestId("message-1")).toHaveTextContent("Hey");
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts newThreadId option without errors", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const spy = vi.fn();
|
||||
const predeterminedThreadId = randomUUID();
|
||||
|
||||
// Test that newThreadId option can be passed without causing errors
|
||||
function TestNewThreadComponent() {
|
||||
const stream = useStream<{ messages: Message[] }>({
|
||||
assistantId: "agent",
|
||||
apiKey: "test-api-key",
|
||||
threadId: null, // Start with no thread
|
||||
onThreadId: spy, // Mock callback
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="loading">
|
||||
{stream.isLoading ? "Loading..." : "Not loading"}
|
||||
</div>
|
||||
<div data-testid="thread-id">
|
||||
{stream.client ? "Client ready" : "No client"}
|
||||
</div>
|
||||
<button
|
||||
data-testid="submit"
|
||||
onClick={() =>
|
||||
stream.submit({}, { threadId: predeterminedThreadId })
|
||||
}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(<TestNewThreadComponent />);
|
||||
|
||||
// Should render without errors
|
||||
expect(screen.getByTestId("loading")).toHaveTextContent("Not loading");
|
||||
expect(screen.getByTestId("thread-id")).toHaveTextContent("Client ready");
|
||||
|
||||
await user.click(screen.getByTestId("submit"));
|
||||
expect(spy).toHaveBeenCalledWith(predeterminedThreadId);
|
||||
expect(await threads.get(predeterminedThreadId)).toEqual({
|
||||
thread_id: predeterminedThreadId,
|
||||
metadata: {
|
||||
graph_id: "agent",
|
||||
assistant_id: "agent",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("onStop callback is called when stop is called", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onStopCallback = vi.fn();
|
||||
|
||||
function TestComponent() {
|
||||
const { submit, stop } = useStream({
|
||||
assistantId: "agent",
|
||||
apiKey: "test-api-key",
|
||||
onStop: onStopCallback,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button data-testid="submit" onClick={() => submit({})}>
|
||||
Send
|
||||
</button>
|
||||
<button data-testid="stop" onClick={stop}>
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(<TestComponent />);
|
||||
|
||||
// Start a stream and stop it
|
||||
await user.click(screen.getByTestId("submit"));
|
||||
await user.click(screen.getByTestId("stop"));
|
||||
|
||||
// Verify onStop was called with mutate function
|
||||
expect(onStopCallback).toHaveBeenCalledTimes(1);
|
||||
expect(onStopCallback).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
mutate: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("onStop mutate function updates stream values immediately", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
function TestComponent() {
|
||||
const [stopped, setStopped] = useState(false);
|
||||
const { submit, stop, messages } = useStream<{ messages: Message[] }>({
|
||||
assistantId: "agent",
|
||||
apiKey: "test-api-key",
|
||||
onStop: ({ mutate }) => {
|
||||
setStopped(true);
|
||||
mutate((prev) => ({
|
||||
...prev,
|
||||
messages: [
|
||||
...(prev.messages ?? []),
|
||||
{ type: "ai", content: "Stream stopped" },
|
||||
],
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="stopped-status">
|
||||
{stopped ? "Stopped" : "Not stopped"}
|
||||
</div>
|
||||
<div data-testid="messages">
|
||||
{messages.map((msg, i) => (
|
||||
<div key={msg.id ?? i} data-testid={`message-${i}`}>
|
||||
{typeof msg.content === "string"
|
||||
? msg.content
|
||||
: JSON.stringify(msg.content)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button data-testid="submit" onClick={() => submit({})}>
|
||||
Send
|
||||
</button>
|
||||
<button data-testid="stop" onClick={stop}>
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(<TestComponent />);
|
||||
|
||||
// Initial state
|
||||
expect(screen.getByTestId("stopped-status")).toHaveTextContent(
|
||||
"Not stopped",
|
||||
);
|
||||
|
||||
// Start and stop stream
|
||||
await user.click(screen.getByTestId("submit"));
|
||||
await user.click(screen.getByTestId("stop"));
|
||||
|
||||
// Verify state was updated immediately
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("stopped-status")).toHaveTextContent("Stopped");
|
||||
expect(screen.getByTestId("message-0")).toHaveTextContent(
|
||||
"Stream stopped",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("onStop handles functional updates correctly", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
function TestComponent() {
|
||||
const { submit, stop, values } = useStream({
|
||||
assistantId: "agent",
|
||||
apiKey: "test-api-key",
|
||||
initialValues: {
|
||||
counter: 5,
|
||||
items: ["item1", "item2"],
|
||||
},
|
||||
onStop: ({ mutate }) => {
|
||||
mutate((prev: any) => ({
|
||||
...prev,
|
||||
counter: (prev.counter || 0) + 10,
|
||||
items: [...(prev.items || []), "stopped"],
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="counter">{(values as any).counter}</div>
|
||||
<div data-testid="items">{(values as any).items?.join(", ")}</div>
|
||||
<button data-testid="submit" onClick={() => submit({})}>
|
||||
Send
|
||||
</button>
|
||||
<button data-testid="stop" onClick={stop}>
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(<TestComponent />);
|
||||
|
||||
// Initial state
|
||||
expect(screen.getByTestId("counter")).toHaveTextContent("5");
|
||||
expect(screen.getByTestId("items")).toHaveTextContent("item1, item2");
|
||||
|
||||
// Start and stop stream
|
||||
await user.click(screen.getByTestId("submit"));
|
||||
await user.click(screen.getByTestId("stop"));
|
||||
|
||||
// Verify functional update was applied correctly
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("counter")).toHaveTextContent("15");
|
||||
expect(screen.getByTestId("items")).toHaveTextContent(
|
||||
"item1, item2, stopped",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("onStop is not called when stream completes naturally", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const onStopCallback = vi.fn();
|
||||
|
||||
function TestComponent() {
|
||||
const { submit } = useStream({
|
||||
assistantId: "agent",
|
||||
apiKey: "test-api-key",
|
||||
onStop: onStopCallback,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button data-testid="submit" onClick={() => submit({})}>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(<TestComponent />);
|
||||
|
||||
// Start a stream and let it complete naturally
|
||||
await user.click(screen.getByTestId("submit"));
|
||||
|
||||
// Wait for stream to complete naturally
|
||||
await waitFor(() => {
|
||||
expect(onStopCallback).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,101 +0,0 @@
|
||||
type ImageDetail = "auto" | "low" | "high";
|
||||
type MessageContentImageUrl = {
|
||||
type: "image_url";
|
||||
image_url: string | { url: string; detail?: ImageDetail | undefined };
|
||||
};
|
||||
|
||||
type MessageContentText = { type: "text"; text: string };
|
||||
type MessageContentComplex = MessageContentText | MessageContentImageUrl;
|
||||
type MessageContent = string | MessageContentComplex[];
|
||||
|
||||
/**
|
||||
* Model-specific additional kwargs, which is passed back to the underlying LLM.
|
||||
*/
|
||||
type MessageAdditionalKwargs = Record<string, unknown>;
|
||||
|
||||
type BaseMessage = {
|
||||
additional_kwargs?: MessageAdditionalKwargs | undefined;
|
||||
content: MessageContent;
|
||||
id?: string | undefined;
|
||||
name?: string | undefined;
|
||||
response_metadata?: Record<string, unknown> | undefined;
|
||||
};
|
||||
|
||||
export type HumanMessage = BaseMessage & {
|
||||
type: "human";
|
||||
example?: boolean | undefined;
|
||||
};
|
||||
|
||||
export type AIMessage = BaseMessage & {
|
||||
type: "ai";
|
||||
example?: boolean | undefined;
|
||||
tool_calls?:
|
||||
| {
|
||||
name: string;
|
||||
args: { [x: string]: any };
|
||||
id?: string | undefined;
|
||||
type?: "tool_call" | undefined;
|
||||
}[]
|
||||
| undefined;
|
||||
invalid_tool_calls?:
|
||||
| {
|
||||
name?: string | undefined;
|
||||
args?: string | undefined;
|
||||
id?: string | undefined;
|
||||
error?: string | undefined;
|
||||
type?: "invalid_tool_call" | undefined;
|
||||
}[]
|
||||
| undefined;
|
||||
usage_metadata?:
|
||||
| {
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
total_tokens: number;
|
||||
input_token_details?:
|
||||
| {
|
||||
audio?: number | undefined;
|
||||
cache_read?: number | undefined;
|
||||
cache_creation?: number | undefined;
|
||||
}
|
||||
| undefined;
|
||||
output_token_details?:
|
||||
| { audio?: number | undefined; reasoning?: number | undefined }
|
||||
| undefined;
|
||||
}
|
||||
| undefined;
|
||||
};
|
||||
|
||||
export type ToolMessage = BaseMessage & {
|
||||
type: "tool";
|
||||
status?: "error" | "success" | undefined;
|
||||
tool_call_id: string;
|
||||
/**
|
||||
* Artifact of the Tool execution which is not meant to be sent to the model.
|
||||
*
|
||||
* Should only be specified if it is different from the message content, e.g. if only
|
||||
* a subset of the full tool output is being passed as message content but the full
|
||||
* output is needed in other parts of the code.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
artifact?: any;
|
||||
};
|
||||
|
||||
export type SystemMessage = BaseMessage & {
|
||||
type: "system";
|
||||
};
|
||||
|
||||
export type FunctionMessage = BaseMessage & {
|
||||
type: "function";
|
||||
};
|
||||
|
||||
export type RemoveMessage = BaseMessage & {
|
||||
type: "remove";
|
||||
};
|
||||
|
||||
export type Message =
|
||||
| HumanMessage
|
||||
| AIMessage
|
||||
| ToolMessage
|
||||
| SystemMessage
|
||||
| FunctionMessage
|
||||
| RemoveMessage;
|
||||
@@ -1,231 +0,0 @@
|
||||
import type { Message } from "./types.messages.js";
|
||||
|
||||
/**
|
||||
* Stream modes
|
||||
* - "values": Stream only the state values.
|
||||
* - "messages": Stream complete messages.
|
||||
* - "messages-tuple": Stream (message chunk, metadata) tuples.
|
||||
* - "updates": Stream updates to the state.
|
||||
* - "events": Stream events occurring during execution.
|
||||
* - "debug": Stream detailed debug information.
|
||||
* - "custom": Stream custom events.
|
||||
*/
|
||||
export type StreamMode =
|
||||
| "values"
|
||||
| "messages"
|
||||
| "updates"
|
||||
| "events"
|
||||
| "debug"
|
||||
| "custom"
|
||||
| "messages-tuple";
|
||||
|
||||
type MessageTupleMetadata = {
|
||||
tags: string[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
type AsSubgraph<TEvent extends { id?: string; event: string; data: unknown }> =
|
||||
{
|
||||
id?: TEvent["id"];
|
||||
event: TEvent["event"] | `${TEvent["event"]}|${string}`;
|
||||
data: TEvent["data"];
|
||||
};
|
||||
|
||||
/**
|
||||
* Stream event with values after completion of each step.
|
||||
*/
|
||||
export type ValuesStreamEvent<StateType> = {
|
||||
id?: string;
|
||||
event: "values";
|
||||
data: StateType;
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export type SubgraphValuesStreamEvent<StateType> = AsSubgraph<
|
||||
ValuesStreamEvent<StateType>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Stream event with message chunks coming from LLM invocations inside nodes.
|
||||
*/
|
||||
export type MessagesTupleStreamEvent = {
|
||||
event: "messages";
|
||||
// TODO: add types for message and config, which do not depend on LangChain
|
||||
// while making sure it's easy to keep them in sync.
|
||||
data: [message: Message, config: MessageTupleMetadata];
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export type SubgraphMessagesTupleStreamEvent =
|
||||
AsSubgraph<MessagesTupleStreamEvent>;
|
||||
|
||||
/**
|
||||
* Metadata stream event with information about the run and thread
|
||||
*/
|
||||
export type MetadataStreamEvent = {
|
||||
id?: string;
|
||||
event: "metadata";
|
||||
data: { run_id: string; thread_id: string };
|
||||
};
|
||||
|
||||
/**
|
||||
* Stream event with error information.
|
||||
*/
|
||||
export type ErrorStreamEvent = {
|
||||
id?: string;
|
||||
event: "error";
|
||||
data: { error: string; message: string };
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export type SubgraphErrorStreamEvent = AsSubgraph<ErrorStreamEvent>;
|
||||
|
||||
/**
|
||||
* Stream event with updates to the state after each step.
|
||||
* The streamed outputs include the name of the node that
|
||||
* produced the update as well as the update.
|
||||
*/
|
||||
export type UpdatesStreamEvent<UpdateType> = {
|
||||
id?: string;
|
||||
event: "updates";
|
||||
data: { [node: string]: UpdateType };
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export type SubgraphUpdatesStreamEvent<UpdateType> = AsSubgraph<
|
||||
UpdatesStreamEvent<UpdateType>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Streaming custom data from inside the nodes.
|
||||
*/
|
||||
export type CustomStreamEvent<T> = { event: "custom"; data: T };
|
||||
|
||||
/** @internal */
|
||||
export type SubgraphCustomStreamEvent<T> = AsSubgraph<CustomStreamEvent<T>>;
|
||||
|
||||
type MessagesMetadataStreamEvent = {
|
||||
id?: string;
|
||||
event: "messages/metadata";
|
||||
data: { [messageId: string]: { metadata: unknown } };
|
||||
};
|
||||
type MessagesCompleteStreamEvent = {
|
||||
id?: string;
|
||||
event: "messages/complete";
|
||||
data: Message[];
|
||||
};
|
||||
type MessagesPartialStreamEvent = {
|
||||
id?: string;
|
||||
event: "messages/partial";
|
||||
data: Message[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Message stream event specific to LangGraph Server.
|
||||
* @deprecated Use `streamMode: "messages-tuple"` instead.
|
||||
*/
|
||||
export type MessagesStreamEvent =
|
||||
| MessagesMetadataStreamEvent
|
||||
| MessagesCompleteStreamEvent
|
||||
| MessagesPartialStreamEvent;
|
||||
|
||||
/** @internal */
|
||||
export type SubgraphMessagesStreamEvent =
|
||||
| AsSubgraph<MessagesMetadataStreamEvent>
|
||||
| AsSubgraph<MessagesCompleteStreamEvent>
|
||||
| AsSubgraph<MessagesPartialStreamEvent>;
|
||||
|
||||
/**
|
||||
* Stream event with detailed debug information.
|
||||
*/
|
||||
export type DebugStreamEvent = { id?: string; event: "debug"; data: unknown };
|
||||
|
||||
/** @internal */
|
||||
export type SubgraphDebugStreamEvent = AsSubgraph<DebugStreamEvent>;
|
||||
|
||||
/**
|
||||
* Stream event with events occurring during execution.
|
||||
*/
|
||||
export type EventsStreamEvent = {
|
||||
id?: string;
|
||||
event: "events";
|
||||
data: {
|
||||
event:
|
||||
| `on_${"chat_model" | "llm" | "chain" | "tool" | "retriever" | "prompt"}_${"start" | "stream" | "end"}`
|
||||
| (string & {});
|
||||
name: string;
|
||||
tags: string[];
|
||||
run_id: string;
|
||||
metadata: Record<string, unknown>;
|
||||
parent_ids: string[];
|
||||
data: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export type SubgraphEventsStreamEvent = AsSubgraph<EventsStreamEvent>;
|
||||
|
||||
/**
|
||||
* Stream event with a feedback key to signed URL map. Set `feedbackKeys` in
|
||||
* the `RunsStreamPayload` to receive this event.
|
||||
*/
|
||||
export type FeedbackStreamEvent = {
|
||||
id?: string;
|
||||
event: "feedback";
|
||||
data: { [feedbackKey: string]: string };
|
||||
};
|
||||
|
||||
type GetStreamModeMap<
|
||||
TStreamMode extends StreamMode | StreamMode[],
|
||||
TStateType = unknown,
|
||||
TUpdateType = TStateType,
|
||||
TCustomType = unknown,
|
||||
> =
|
||||
| {
|
||||
values: ValuesStreamEvent<TStateType>;
|
||||
updates: UpdatesStreamEvent<TUpdateType>;
|
||||
custom: CustomStreamEvent<TCustomType>;
|
||||
debug: DebugStreamEvent;
|
||||
messages: MessagesStreamEvent;
|
||||
"messages-tuple": MessagesTupleStreamEvent;
|
||||
events: EventsStreamEvent;
|
||||
}[TStreamMode extends StreamMode[] ? TStreamMode[number] : TStreamMode]
|
||||
| ErrorStreamEvent
|
||||
| MetadataStreamEvent
|
||||
| FeedbackStreamEvent;
|
||||
|
||||
type GetSubgraphsStreamModeMap<
|
||||
TStreamMode extends StreamMode | StreamMode[],
|
||||
TStateType = unknown,
|
||||
TUpdateType = TStateType,
|
||||
TCustomType = unknown,
|
||||
> =
|
||||
| {
|
||||
values: SubgraphValuesStreamEvent<TStateType>;
|
||||
updates: SubgraphUpdatesStreamEvent<TUpdateType>;
|
||||
custom: SubgraphCustomStreamEvent<TCustomType>;
|
||||
debug: SubgraphDebugStreamEvent;
|
||||
messages: SubgraphMessagesStreamEvent;
|
||||
"messages-tuple": SubgraphMessagesTupleStreamEvent;
|
||||
events: SubgraphEventsStreamEvent;
|
||||
}[TStreamMode extends StreamMode[] ? TStreamMode[number] : TStreamMode]
|
||||
| SubgraphErrorStreamEvent
|
||||
| MetadataStreamEvent
|
||||
| FeedbackStreamEvent;
|
||||
|
||||
export type TypedAsyncGenerator<
|
||||
TStreamMode extends StreamMode | StreamMode[] = [],
|
||||
TSubgraphs extends boolean = false,
|
||||
TStateType = unknown,
|
||||
TUpdateType = TStateType,
|
||||
TCustomType = unknown,
|
||||
> = AsyncGenerator<
|
||||
TSubgraphs extends true
|
||||
? GetSubgraphsStreamModeMap<
|
||||
TStreamMode,
|
||||
TStateType,
|
||||
TUpdateType,
|
||||
TCustomType
|
||||
>
|
||||
: GetStreamModeMap<TStreamMode, TStateType, TUpdateType, TCustomType>
|
||||
>;
|
||||
@@ -1,209 +0,0 @@
|
||||
import { LangChainTracer } from "@langchain/core/tracers/tracer_langchain";
|
||||
import { Checkpoint, Config, Metadata } from "./schema.js";
|
||||
import { StreamMode } from "./types.stream.js";
|
||||
|
||||
export type MultitaskStrategy = "reject" | "interrupt" | "rollback" | "enqueue";
|
||||
export type OnConflictBehavior = "raise" | "do_nothing";
|
||||
export type OnCompletionBehavior = "complete" | "continue";
|
||||
export type DisconnectMode = "cancel" | "continue";
|
||||
export type StreamEvent =
|
||||
| "events"
|
||||
| "metadata"
|
||||
| "debug"
|
||||
| "updates"
|
||||
| "values"
|
||||
| "messages/partial"
|
||||
| "messages/metadata"
|
||||
| "messages/complete"
|
||||
| "messages"
|
||||
| (string & {});
|
||||
|
||||
export interface Send {
|
||||
node: string;
|
||||
input: unknown | null;
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
/**
|
||||
* An object to update the thread state with.
|
||||
*/
|
||||
update?: Record<string, unknown> | [string, unknown][] | null;
|
||||
|
||||
/**
|
||||
* The value to return from an `interrupt` function call.
|
||||
*/
|
||||
resume?: unknown;
|
||||
|
||||
/**
|
||||
* Determine the next node to navigate to. Can be one of the following:
|
||||
* - Name(s) of the node names to navigate to next.
|
||||
* - `Send` command(s) to execute node(s) with provided input.
|
||||
*/
|
||||
goto?: Send | Send[] | string | string[];
|
||||
}
|
||||
|
||||
export interface RunsInvokePayload {
|
||||
/**
|
||||
* Input to the run. Pass `null` to resume from the current state of the thread.
|
||||
*/
|
||||
input?: Record<string, unknown> | null;
|
||||
|
||||
/**
|
||||
* Metadata for the run.
|
||||
*/
|
||||
metadata?: Metadata;
|
||||
|
||||
/**
|
||||
* Additional configuration for the run.
|
||||
*/
|
||||
config?: Config;
|
||||
|
||||
/**
|
||||
* Checkpoint ID for when creating a new run.
|
||||
*/
|
||||
checkpointId?: string;
|
||||
|
||||
/**
|
||||
* Checkpoint for when creating a new run.
|
||||
*/
|
||||
checkpoint?: Omit<Checkpoint, "thread_id">;
|
||||
|
||||
/**
|
||||
* Whether to checkpoint during the run (or only at the end/interruption).
|
||||
*/
|
||||
checkpointDuring?: boolean;
|
||||
|
||||
/**
|
||||
* Interrupt execution before entering these nodes.
|
||||
*/
|
||||
interruptBefore?: "*" | string[];
|
||||
|
||||
/**
|
||||
* Interrupt execution after leaving these nodes.
|
||||
*/
|
||||
interruptAfter?: "*" | string[];
|
||||
|
||||
/**
|
||||
* Strategy to handle concurrent runs on the same thread. Only relevant if
|
||||
* there is a pending/inflight run on the same thread. One of:
|
||||
* - "reject": Reject the new run.
|
||||
* - "interrupt": Interrupt the current run, keeping steps completed until now,
|
||||
and start a new one.
|
||||
* - "rollback": Cancel and delete the existing run, rolling back the thread to
|
||||
the state before it had started, then start the new run.
|
||||
* - "enqueue": Queue up the new run to start after the current run finishes.
|
||||
*/
|
||||
multitaskStrategy?: MultitaskStrategy;
|
||||
|
||||
/**
|
||||
* Abort controller signal to cancel the run.
|
||||
*/
|
||||
signal?: AbortController["signal"];
|
||||
|
||||
/**
|
||||
* Behavior to handle run completion. Only relevant if
|
||||
* there is a pending/inflight run on the same thread. One of:
|
||||
* - "complete": Complete the run.
|
||||
* - "continue": Continue the run.
|
||||
*/
|
||||
onCompletion?: OnCompletionBehavior;
|
||||
|
||||
/**
|
||||
* Webhook to call when the run is complete.
|
||||
*/
|
||||
webhook?: string;
|
||||
|
||||
/**
|
||||
* Behavior to handle disconnection. Only relevant if
|
||||
* there is a pending/inflight run on the same thread. One of:
|
||||
* - "cancel": Cancel the run.
|
||||
* - "continue": Continue the run.
|
||||
*/
|
||||
onDisconnect?: DisconnectMode;
|
||||
|
||||
/**
|
||||
* The number of seconds to wait before starting the run.
|
||||
* Use to schedule future runs.
|
||||
*/
|
||||
afterSeconds?: number;
|
||||
|
||||
/**
|
||||
* Behavior if the specified run doesn't exist. Defaults to "reject".
|
||||
*/
|
||||
ifNotExists?: "create" | "reject";
|
||||
|
||||
/**
|
||||
* One or more commands to invoke the graph with.
|
||||
*/
|
||||
command?: Command;
|
||||
|
||||
/**
|
||||
* Callback when a run is created.
|
||||
*/
|
||||
onRunCreated?: (params: { run_id: string; thread_id?: string }) => void;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* For LangSmith tracing purposes only. Not part of the public API.
|
||||
*/
|
||||
_langsmithTracer?: LangChainTracer;
|
||||
}
|
||||
|
||||
export interface RunsStreamPayload<
|
||||
TStreamMode extends StreamMode | StreamMode[] = [],
|
||||
TSubgraphs extends boolean = false,
|
||||
> extends RunsInvokePayload {
|
||||
/**
|
||||
* One of `"values"`, `"messages"`, `"messages-tuple"`, `"updates"`, `"events"`, `"debug"`, `"custom"`.
|
||||
*/
|
||||
streamMode?: TStreamMode;
|
||||
|
||||
/**
|
||||
* Stream output from subgraphs. By default, streams only the top graph.
|
||||
*/
|
||||
streamSubgraphs?: TSubgraphs;
|
||||
|
||||
/**
|
||||
* Whether the stream is considered resumable.
|
||||
* If true, the stream can be resumed and replayed in its entirety even after disconnection.
|
||||
*/
|
||||
streamResumable?: boolean;
|
||||
|
||||
/**
|
||||
* Pass one or more feedbackKeys if you want to request short-lived signed URLs
|
||||
* for submitting feedback to LangSmith with this key for this run.
|
||||
*/
|
||||
feedbackKeys?: string[];
|
||||
}
|
||||
|
||||
export interface RunsCreatePayload extends RunsInvokePayload {
|
||||
/**
|
||||
* One of `"values"`, `"messages"`, `"messages-tuple"`, `"updates"`, `"events"`, `"debug"`, `"custom"`.
|
||||
*/
|
||||
streamMode?: StreamMode | Array<StreamMode>;
|
||||
|
||||
/**
|
||||
* Stream output from subgraphs. By default, streams only the top graph.
|
||||
*/
|
||||
streamSubgraphs?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the stream is considered resumable.
|
||||
* If true, the stream can be resumed and replayed in its entirety even after disconnection.
|
||||
*/
|
||||
streamResumable?: boolean;
|
||||
}
|
||||
|
||||
export interface CronsCreatePayload extends RunsCreatePayload {
|
||||
/**
|
||||
* Schedule for running the Cron Job
|
||||
*/
|
||||
schedule: string;
|
||||
}
|
||||
|
||||
export interface RunsWaitPayload extends RunsStreamPayload {
|
||||
/**
|
||||
* Raise errors returned by the run. Default is `true`.
|
||||
*/
|
||||
raiseError?: boolean;
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
import pRetry from "p-retry";
|
||||
import PQueueMod from "p-queue";
|
||||
import { _getFetchImplementation } from "../singletons/fetch.js";
|
||||
|
||||
const STATUS_NO_RETRY = [
|
||||
400, // Bad Request
|
||||
401, // Unauthorized
|
||||
402, // Payment required
|
||||
403, // Forbidden
|
||||
404, // Not Found
|
||||
405, // Method Not Allowed
|
||||
406, // Not Acceptable
|
||||
407, // Proxy Authentication Required
|
||||
408, // Request Timeout
|
||||
409, // Conflict
|
||||
422, // Unprocessable Entity
|
||||
];
|
||||
|
||||
type ResponseCallback = (response?: Response) => Promise<boolean>;
|
||||
|
||||
export interface AsyncCallerParams {
|
||||
/**
|
||||
* The maximum number of concurrent calls that can be made.
|
||||
* Defaults to `Infinity`, which means no limit.
|
||||
*/
|
||||
maxConcurrency?: number;
|
||||
/**
|
||||
* The maximum number of retries that can be made for a single call,
|
||||
* with an exponential backoff between each attempt. Defaults to 6.
|
||||
*/
|
||||
maxRetries?: number;
|
||||
|
||||
onFailedResponseHook?: ResponseCallback;
|
||||
|
||||
/**
|
||||
* Specify a custom fetch implementation.
|
||||
*
|
||||
* By default we expect the `fetch` is available in the global scope.
|
||||
*/
|
||||
fetch?: typeof fetch | ((...args: any[]) => any);
|
||||
}
|
||||
|
||||
export interface AsyncCallerCallOptions {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not rely on globalThis.Response, rather just
|
||||
* do duck typing
|
||||
*/
|
||||
function isResponse(x: unknown): x is Response {
|
||||
if (x == null || typeof x !== "object") return false;
|
||||
return "status" in x && "statusText" in x && "text" in x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility error to properly handle failed requests
|
||||
*/
|
||||
class HTTPError extends Error {
|
||||
status: number;
|
||||
text: string;
|
||||
|
||||
response?: Response;
|
||||
|
||||
constructor(status: number, message: string, response?: Response) {
|
||||
super(`HTTP ${status}: ${message}`);
|
||||
this.status = status;
|
||||
this.text = message;
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
static async fromResponse(
|
||||
response: Response,
|
||||
options?: { includeResponse?: boolean },
|
||||
): Promise<HTTPError> {
|
||||
try {
|
||||
return new HTTPError(
|
||||
response.status,
|
||||
await response.text(),
|
||||
options?.includeResponse ? response : undefined,
|
||||
);
|
||||
} catch {
|
||||
return new HTTPError(
|
||||
response.status,
|
||||
response.statusText,
|
||||
options?.includeResponse ? response : undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that can be used to make async calls with concurrency and retry logic.
|
||||
*
|
||||
* This is useful for making calls to any kind of "expensive" external resource,
|
||||
* be it because it's rate-limited, subject to network issues, etc.
|
||||
*
|
||||
* Concurrent calls are limited by the `maxConcurrency` parameter, which defaults
|
||||
* to `Infinity`. This means that by default, all calls will be made in parallel.
|
||||
*
|
||||
* Retries are limited by the `maxRetries` parameter, which defaults to 5. This
|
||||
* means that by default, each call will be retried up to 5 times, with an
|
||||
* exponential backoff between each attempt.
|
||||
*/
|
||||
export class AsyncCaller {
|
||||
protected maxConcurrency: AsyncCallerParams["maxConcurrency"];
|
||||
|
||||
protected maxRetries: AsyncCallerParams["maxRetries"];
|
||||
|
||||
private queue: (typeof import("p-queue"))["default"]["prototype"];
|
||||
|
||||
private onFailedResponseHook?: ResponseCallback;
|
||||
|
||||
private customFetch?: typeof fetch;
|
||||
|
||||
constructor(params: AsyncCallerParams) {
|
||||
this.maxConcurrency = params.maxConcurrency ?? Infinity;
|
||||
this.maxRetries = params.maxRetries ?? 4;
|
||||
|
||||
if ("default" in PQueueMod) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.queue = new (PQueueMod.default as any)({
|
||||
concurrency: this.maxConcurrency,
|
||||
});
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.queue = new (PQueueMod as any)({ concurrency: this.maxConcurrency });
|
||||
}
|
||||
this.onFailedResponseHook = params?.onFailedResponseHook;
|
||||
this.customFetch = params.fetch;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
call<A extends any[], T extends (...args: A) => Promise<any>>(
|
||||
callable: T,
|
||||
...args: Parameters<T>
|
||||
): Promise<Awaited<ReturnType<T>>> {
|
||||
const onFailedResponseHook = this.onFailedResponseHook;
|
||||
return this.queue.add(
|
||||
() =>
|
||||
pRetry(
|
||||
() =>
|
||||
callable(...(args as Parameters<T>)).catch(async (error) => {
|
||||
// eslint-disable-next-line no-instanceof/no-instanceof
|
||||
if (error instanceof Error) {
|
||||
throw error;
|
||||
} else if (isResponse(error)) {
|
||||
throw await HTTPError.fromResponse(error, {
|
||||
includeResponse: !!onFailedResponseHook,
|
||||
});
|
||||
} else {
|
||||
throw new Error(error);
|
||||
}
|
||||
}),
|
||||
{
|
||||
async onFailedAttempt(error) {
|
||||
if (
|
||||
error.message.startsWith("Cancel") ||
|
||||
error.message.startsWith("TimeoutError") ||
|
||||
error.message.startsWith("AbortError")
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((error as any)?.code === "ECONNABORTED") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (error instanceof HTTPError) {
|
||||
if (STATUS_NO_RETRY.includes(error.status)) {
|
||||
throw error;
|
||||
}
|
||||
if (onFailedResponseHook && error.response) {
|
||||
await onFailedResponseHook(error.response);
|
||||
}
|
||||
}
|
||||
},
|
||||
// If needed we can change some of the defaults here,
|
||||
// but they're quite sensible.
|
||||
retries: this.maxRetries,
|
||||
randomize: true,
|
||||
},
|
||||
),
|
||||
{ throwOnTimeout: true },
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
callWithOptions<A extends any[], T extends (...args: A) => Promise<any>>(
|
||||
options: AsyncCallerCallOptions,
|
||||
callable: T,
|
||||
...args: Parameters<T>
|
||||
): Promise<Awaited<ReturnType<T>>> {
|
||||
// Note this doesn't cancel the underlying request,
|
||||
// when available prefer to use the signal option of the underlying call
|
||||
if (options.signal) {
|
||||
return Promise.race([
|
||||
this.call<A, T>(callable, ...args),
|
||||
new Promise<never>((_, reject) => {
|
||||
options.signal?.addEventListener("abort", () => {
|
||||
reject(new Error("AbortError"));
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
return this.call<A, T>(callable, ...args);
|
||||
}
|
||||
|
||||
fetch(...args: Parameters<typeof fetch>): ReturnType<typeof fetch> {
|
||||
const fetchFn =
|
||||
this.customFetch ?? (_getFetchImplementation() as typeof fetch);
|
||||
return this.call(() =>
|
||||
fetchFn(...args).then((res) => (res.ok ? res : Promise.reject(res))),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
export function getEnvironmentVariable(name: string): string | undefined {
|
||||
// Certain setups (Deno, frontend) will throw an error if you try to access environment variables
|
||||
try {
|
||||
return typeof process !== "undefined"
|
||||
? // eslint-disable-next-line no-process-env
|
||||
process.env?.[name]
|
||||
: undefined;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
export function mergeSignals(...signals: (AbortSignal | null | undefined)[]) {
|
||||
const nonZeroSignals = signals.filter(
|
||||
(signal): signal is AbortSignal => signal != null,
|
||||
);
|
||||
|
||||
if (nonZeroSignals.length === 0) return undefined;
|
||||
if (nonZeroSignals.length === 1) return nonZeroSignals[0];
|
||||
|
||||
const controller = new AbortController();
|
||||
for (const signal of signals) {
|
||||
if (signal?.aborted) {
|
||||
controller.abort(signal.reason);
|
||||
return controller.signal;
|
||||
}
|
||||
|
||||
signal?.addEventListener("abort", () => controller.abort(signal.reason), {
|
||||
once: true,
|
||||
});
|
||||
}
|
||||
|
||||
return controller.signal;
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
const CR = "\r".charCodeAt(0);
|
||||
const LF = "\n".charCodeAt(0);
|
||||
const NULL = "\0".charCodeAt(0);
|
||||
const COLON = ":".charCodeAt(0);
|
||||
const SPACE = " ".charCodeAt(0);
|
||||
|
||||
const TRAILING_NEWLINE = [CR, LF];
|
||||
|
||||
export function BytesLineDecoder() {
|
||||
let buffer: Uint8Array[] = [];
|
||||
let trailingCr = false;
|
||||
|
||||
return new TransformStream<Uint8Array, Uint8Array>({
|
||||
start() {
|
||||
buffer = [];
|
||||
trailingCr = false;
|
||||
},
|
||||
|
||||
transform(chunk, controller) {
|
||||
// See https://docs.python.org/3/glossary.html#term-universal-newlines
|
||||
let text = chunk;
|
||||
|
||||
// Handle trailing CR from previous chunk
|
||||
if (trailingCr) {
|
||||
text = joinArrays([[CR], text]);
|
||||
trailingCr = false;
|
||||
}
|
||||
|
||||
// Check for trailing CR in current chunk
|
||||
if (text.length > 0 && text.at(-1) === CR) {
|
||||
trailingCr = true;
|
||||
text = text.subarray(0, -1);
|
||||
}
|
||||
|
||||
if (!text.length) return;
|
||||
const trailingNewline = TRAILING_NEWLINE.includes(text.at(-1)!);
|
||||
|
||||
const lastIdx = text.length - 1;
|
||||
const { lines } = text.reduce<{ lines: Uint8Array[]; from: number }>(
|
||||
(acc, cur, idx) => {
|
||||
if (acc.from > idx) return acc;
|
||||
|
||||
if (cur === CR || cur === LF) {
|
||||
acc.lines.push(text.subarray(acc.from, idx));
|
||||
if (cur === CR && text[idx + 1] === LF) {
|
||||
acc.from = idx + 2;
|
||||
} else {
|
||||
acc.from = idx + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (idx === lastIdx && acc.from <= lastIdx) {
|
||||
acc.lines.push(text.subarray(acc.from));
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ lines: [], from: 0 },
|
||||
);
|
||||
|
||||
if (lines.length === 1 && !trailingNewline) {
|
||||
buffer.push(lines[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (buffer.length) {
|
||||
// Include existing buffer in first line
|
||||
buffer.push(lines[0]);
|
||||
lines[0] = joinArrays(buffer);
|
||||
buffer = [];
|
||||
}
|
||||
|
||||
if (!trailingNewline) {
|
||||
// If the last segment is not newline terminated,
|
||||
// buffer it for the next chunk
|
||||
if (lines.length) buffer = [lines.pop()!];
|
||||
}
|
||||
|
||||
// Enqueue complete lines
|
||||
for (const line of lines) {
|
||||
controller.enqueue(line);
|
||||
}
|
||||
},
|
||||
|
||||
flush(controller) {
|
||||
if (buffer.length) {
|
||||
controller.enqueue(joinArrays(buffer));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
interface StreamPart {
|
||||
id: string | undefined;
|
||||
event: string;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export function SSEDecoder() {
|
||||
let event = "";
|
||||
let data: Uint8Array[] = [];
|
||||
let lastEventId = "";
|
||||
let retry: number | null = null;
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
return new TransformStream<Uint8Array, StreamPart>({
|
||||
transform(chunk, controller) {
|
||||
// Handle empty line case
|
||||
if (!chunk.length) {
|
||||
if (!event && !data.length && !lastEventId && retry == null) return;
|
||||
|
||||
const sse = {
|
||||
id: lastEventId || undefined,
|
||||
event,
|
||||
data: data.length ? decodeArraysToJson(decoder, data) : null,
|
||||
};
|
||||
|
||||
// NOTE: as per the SSE spec, do not reset lastEventId
|
||||
event = "";
|
||||
data = [];
|
||||
retry = null;
|
||||
|
||||
controller.enqueue(sse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore comments
|
||||
if (chunk[0] === COLON) return;
|
||||
|
||||
const sepIdx = chunk.indexOf(COLON);
|
||||
if (sepIdx === -1) return;
|
||||
|
||||
const fieldName = decoder.decode(chunk.subarray(0, sepIdx));
|
||||
let value = chunk.subarray(sepIdx + 1);
|
||||
if (value[0] === SPACE) value = value.subarray(1);
|
||||
|
||||
if (fieldName === "event") {
|
||||
event = decoder.decode(value);
|
||||
} else if (fieldName === "data") {
|
||||
data.push(value);
|
||||
} else if (fieldName === "id") {
|
||||
if (value.indexOf(NULL) === -1) lastEventId = decoder.decode(value);
|
||||
} else if (fieldName === "retry") {
|
||||
const retryNum = Number.parseInt(decoder.decode(value));
|
||||
if (!Number.isNaN(retryNum)) retry = retryNum;
|
||||
}
|
||||
},
|
||||
|
||||
flush(controller) {
|
||||
if (event) {
|
||||
controller.enqueue({
|
||||
id: lastEventId || undefined,
|
||||
event,
|
||||
data: data.length ? decodeArraysToJson(decoder, data) : null,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function joinArrays(data: ArrayLike<number>[]) {
|
||||
const totalLength = data.reduce((acc, curr) => acc + curr.length, 0);
|
||||
let merged = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const c of data) {
|
||||
merged.set(c, offset);
|
||||
offset += c.length;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function decodeArraysToJson(decoder: TextDecoder, data: ArrayLike<number>[]) {
|
||||
return JSON.parse(decoder.decode(joinArrays(data)));
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
// in this case don't quite match.
|
||||
type IterableReadableStreamInterface<T> = ReadableStream<T> & AsyncIterable<T>;
|
||||
|
||||
/*
|
||||
* Support async iterator syntax for ReadableStreams in all environments.
|
||||
* Source: https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1627354490
|
||||
*/
|
||||
export class IterableReadableStream<T>
|
||||
extends ReadableStream<T>
|
||||
implements IterableReadableStreamInterface<T>
|
||||
{
|
||||
public reader: ReadableStreamDefaultReader<T>;
|
||||
|
||||
ensureReader() {
|
||||
if (!this.reader) {
|
||||
this.reader = this.getReader();
|
||||
}
|
||||
}
|
||||
|
||||
async next(): Promise<IteratorResult<T>> {
|
||||
this.ensureReader();
|
||||
try {
|
||||
const result = await this.reader.read();
|
||||
if (result.done) {
|
||||
this.reader.releaseLock(); // release lock when stream becomes closed
|
||||
return {
|
||||
done: true,
|
||||
value: undefined,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
done: false,
|
||||
value: result.value,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
this.reader.releaseLock(); // release lock when stream becomes errored
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async return(): Promise<IteratorResult<T>> {
|
||||
this.ensureReader();
|
||||
// If wrapped in a Node stream, cancel is already called.
|
||||
if (this.locked) {
|
||||
const cancelPromise = this.reader.cancel(); // cancel first, but don't await yet
|
||||
this.reader.releaseLock(); // release lock first
|
||||
await cancelPromise; // now await it
|
||||
}
|
||||
return { done: true, value: undefined };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async throw(e: any): Promise<IteratorResult<T>> {
|
||||
this.ensureReader();
|
||||
if (this.locked) {
|
||||
const cancelPromise = this.reader.cancel(); // cancel first, but don't await yet
|
||||
this.reader.releaseLock(); // release lock first
|
||||
await cancelPromise; // now await it
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Not present in Node 18 types, required in latest Node 22
|
||||
async [Symbol.asyncDispose]() {
|
||||
await this.return();
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator]() {
|
||||
return this;
|
||||
}
|
||||
|
||||
static fromReadableStream<T>(stream: ReadableStream<T>) {
|
||||
// From https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams#reading_the_stream
|
||||
const reader = stream.getReader();
|
||||
return new IterableReadableStream<T>({
|
||||
start(controller) {
|
||||
return pump();
|
||||
function pump(): Promise<T | undefined> {
|
||||
return reader.read().then(({ done, value }) => {
|
||||
// When no more data needs to be consumed, close the stream
|
||||
if (done) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
// Enqueue the next data chunk into our target stream
|
||||
controller.enqueue(value);
|
||||
return pump();
|
||||
});
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
reader.releaseLock();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static fromAsyncGenerator<T>(generator: AsyncGenerator<T>) {
|
||||
return new IterableReadableStream<T>({
|
||||
async pull(controller) {
|
||||
const { value, done } = await generator.next();
|
||||
// When no more data needs to be consumed, close the stream
|
||||
if (done) {
|
||||
controller.close();
|
||||
}
|
||||
// Fix: `else if (value)` will hang the streaming when nullish value (e.g. empty string) is pulled
|
||||
controller.enqueue(value);
|
||||
},
|
||||
async cancel(reason) {
|
||||
await generator.return(reason);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"declaration": false
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "**/tests"]
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"extends": "@tsconfig/recommended",
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"lib": ["ES2021", "ES2022.Object", "ES2022.Error", "DOM"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "nodenext",
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"useDefineForClassFields": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "coverage"],
|
||||
"includeVersion": true,
|
||||
"typedocOptions": {
|
||||
"entryPoints": ["src/client.ts"],
|
||||
"readme": "none",
|
||||
"out": "docs",
|
||||
"plugin": ["typedoc-plugin-markdown"],
|
||||
"excludePrivate": true,
|
||||
"excludeProtected": true,
|
||||
"excludeExternals": false
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"pageTitleTemplates": {
|
||||
"index": "{projectName}/auth"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"pageTitleTemplates": {
|
||||
"index": "{projectName}/react"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
globals: true,
|
||||
include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
|
||||
},
|
||||
});
|
||||