mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-18 05:35:43 +02:00
Compare commits
140
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1d4abf70e | ||
|
|
fadbe7d710 | ||
|
|
c861614337 | ||
|
|
c020f01425 | ||
|
|
14949c81c8 | ||
|
|
3af857922a | ||
|
|
7ec049e0c7 | ||
|
|
1923ff8d85 | ||
|
|
4b9b7d0b1c | ||
|
|
624247a51f | ||
|
|
b21b595927 | ||
|
|
f4633a0015 | ||
|
|
86017c010c | ||
|
|
2115cffc94 | ||
|
|
03726f9bc6 | ||
|
|
509dfd1f21 | ||
|
|
efca21070d | ||
|
|
dba20d0577 | ||
|
|
5145dac12b | ||
|
|
440c7ff12a | ||
|
|
5eef290c4e | ||
|
|
a8b3746356 | ||
|
|
7541331643 | ||
|
|
76814676c2 | ||
|
|
0804984f9d | ||
|
|
8f11b6a003 | ||
|
|
aa6b122e4c | ||
|
|
23491e5c9a | ||
|
|
1491f30a07 | ||
|
|
f63635d3c8 | ||
|
|
6672032568 | ||
|
|
311ce7b04f | ||
|
|
14b732740e | ||
|
|
370825a48a | ||
|
|
264bae5a7e | ||
|
|
f6aa19709e | ||
|
|
8495f6f95d | ||
|
|
6710908d40 | ||
|
|
d24ad3d980 | ||
|
|
2d8288fd0f | ||
|
|
c1ef10a0ec | ||
|
|
a3d7b6f44e | ||
|
|
bc9d45b476 | ||
|
|
7d3f0089aa | ||
|
|
ed678f4701 | ||
|
|
22411ba0fd | ||
|
|
5b9021ff37 | ||
|
|
aaff464115 | ||
|
|
3a23a256e2 | ||
|
|
f6857395b9 | ||
|
|
cdaa7ba003 | ||
|
|
39745ed794 | ||
|
|
738bb8a343 | ||
|
|
b2dde8d9af | ||
|
|
5312edc830 | ||
|
|
276310e116 | ||
|
|
39977ded8c | ||
|
|
439038fc3a | ||
|
|
2e5445c565 | ||
|
|
d38510ad03 | ||
|
|
951486d107 | ||
|
|
745a1e7a29 | ||
|
|
24731d6a28 | ||
|
|
a4689a5d10 | ||
|
|
b05ce0bf60 | ||
|
|
4501e41991 | ||
|
|
bc83287fc8 | ||
|
|
11547e1990 | ||
|
|
fadd9c4577 | ||
|
|
fd2933a792 | ||
|
|
40fa69f8ee | ||
|
|
0824161984 | ||
|
|
b028f502e1 | ||
|
|
9dc3fed6b8 | ||
|
|
0232201b7b | ||
|
|
869b0f2de4 | ||
|
|
90e3adcd71 | ||
|
|
cb918601d1 | ||
|
|
4a4c8db635 | ||
|
|
56a9ce57b1 | ||
|
|
d1ee1cf1f1 | ||
|
|
29c3a579b3 | ||
|
|
9e3cb1f034 | ||
|
|
508e333220 | ||
|
|
aa1bbe3d01 | ||
|
|
139cad373b | ||
|
|
2a86abb8c4 | ||
|
|
d1f0799002 | ||
|
|
be088801ba | ||
|
|
1ee6bfeb8d | ||
|
|
2153d36726 | ||
|
|
819eae891e | ||
|
|
457edaa75b | ||
|
|
90ba4c5205 | ||
|
|
b3c5298100 | ||
|
|
9d9476e664 | ||
|
|
03bec97767 | ||
|
|
dd9b5c42e8 | ||
|
|
951a3f2d1c | ||
|
|
adaa340c15 | ||
|
|
2c85cba9ca | ||
|
|
cb7b924006 | ||
|
|
c61ac946af | ||
|
|
ec0a30008c | ||
|
|
00c7909c27 | ||
|
|
58d396fcf1 | ||
|
|
9f0abf014d | ||
|
|
8d6cd15669 | ||
|
|
61676b8db0 | ||
|
|
3b85e53360 | ||
|
|
250a17d711 | ||
|
|
f63bec8578 | ||
|
|
dc0f0c5944 | ||
|
|
777fe692d4 | ||
|
|
fdbe31a3aa | ||
|
|
78a9933144 | ||
|
|
5717eefa79 | ||
|
|
a5fe3316b6 | ||
|
|
e0699fbdaf | ||
|
|
adc732272c | ||
|
|
c6d674cd3e | ||
|
|
294078adab | ||
|
|
6e9e1ca146 | ||
|
|
2eecaa8500 | ||
|
|
d935a2d110 | ||
|
|
0837263542 | ||
|
|
e0bf4a7bc3 | ||
|
|
5f00938aa2 | ||
|
|
e5ded1888b | ||
|
|
d1710e2eac | ||
|
|
e5947bcd30 | ||
|
|
b6dc566ec7 | ||
|
|
a84b744eb6 | ||
|
|
f5b888dd72 | ||
|
|
7a8f29847b | ||
|
|
f001246794 | ||
|
|
b7d11b4141 | ||
|
|
1d3fd9a46b | ||
|
|
8c4e698c5a | ||
|
|
c989f1c898 |
@@ -23,3 +23,7 @@ body:
|
||||
attributes:
|
||||
label: Issue Content
|
||||
description: Add the content of the issue here.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Community members should **NOT** work on Privileged issues unless these issues have been explicitly marked with a "help-wanted" tag.
|
||||
|
||||
@@ -3,7 +3,7 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [main, v1]
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
id: extract_ignore_words
|
||||
|
||||
- name: Codespell
|
||||
uses: codespell-project/actions-codespell@v2.0
|
||||
uses: codespell-project/actions-codespell@v2.1
|
||||
with:
|
||||
skip: '*.ambr,*.lock,*.ipynb,*.yaml,*.zlib,*.css.map,*.js.map'
|
||||
ignore_words_list: ${{ steps.extract_ignore_words.outputs.ignore_words_list }}
|
||||
|
||||
@@ -35,16 +35,7 @@ jobs:
|
||||
with:
|
||||
filter: "docs/docs/**"
|
||||
|
||||
# TODO: Uncomment this to run on PRs
|
||||
# run-changed-notebooks:
|
||||
# needs: get-changed-files
|
||||
# uses: ./.github/workflows/run_notebooks.yml
|
||||
# secrets: inherit
|
||||
# with:
|
||||
# changed-files: ${{ needs.get-changed-files.outputs.changed-files }}
|
||||
|
||||
deploy:
|
||||
# needs: run-changed-notebooks
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10 # Job will be cancelled if it runs for more than 10 minutes
|
||||
env:
|
||||
|
||||
@@ -39,6 +39,7 @@ jobs:
|
||||
scheduler-kafka
|
||||
sdk-py
|
||||
docs
|
||||
ci
|
||||
requireScope: false
|
||||
ignoreLabels: |
|
||||
ignore-lint-pr-title
|
||||
|
||||
@@ -137,7 +137,9 @@ jobs:
|
||||
needs:
|
||||
- build
|
||||
- release-notes
|
||||
permissions: write-all
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
uses: ./.github/workflows/_test_release.yml
|
||||
with:
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
|
||||
@@ -310,6 +310,12 @@ def _highlight_code_blocks(markdown: str) -> str:
|
||||
return markdown
|
||||
|
||||
|
||||
TARGET_LANGUAGE = os.environ.get("TARGET_LANGUAGE", "python")
|
||||
|
||||
if TARGET_LANGUAGE not in {"python", "js"}:
|
||||
raise ValueError(f"TARGET_LANGUAGE must be 'python' or 'js', got {TARGET_LANGUAGE}")
|
||||
|
||||
|
||||
def _on_page_markdown_with_config(
|
||||
markdown: str,
|
||||
page: Page,
|
||||
@@ -332,16 +338,15 @@ def _on_page_markdown_with_config(
|
||||
markdown = _highlight_code_blocks(markdown)
|
||||
|
||||
# Apply conditional rendering for code blocks
|
||||
target_language = kwargs.get("target_language", "python")
|
||||
markdown = _apply_conditional_rendering(markdown, target_language)
|
||||
if target_language == "js":
|
||||
markdown = _apply_conditional_rendering(markdown, TARGET_LANGUAGE)
|
||||
if TARGET_LANGUAGE == "js":
|
||||
markdown = _resolve_cross_references(markdown, JS_LINK_MAP)
|
||||
elif target_language == "python":
|
||||
elif TARGET_LANGUAGE == "python":
|
||||
# Via a dedicated plugin
|
||||
pass
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported target language: {target_language}. "
|
||||
f"Unsupported target language: {TARGET_LANGUAGE}. "
|
||||
"Supported languages are 'python' and 'js'."
|
||||
)
|
||||
|
||||
|
||||
+40
-23
@@ -8,60 +8,75 @@ Context includes *any* data outside the message list that can shape behavior. Th
|
||||
- Internal state updated during a multi-step reasoning process.
|
||||
- Persistent memory or facts from previous interactions.
|
||||
|
||||
LangGraph provides **three** primary ways to supply context:
|
||||
LangGraph provides **three** primary ways to manage context:
|
||||
|
||||
| Type | Description | Mutable? | Lifetime |
|
||||
|------------------------------------------------------------------------------|-----------------------------------------------|----------|-------------------------|
|
||||
| [**Config**](#config-static-context) | data passed at the start of a run | ❌ | per run |
|
||||
| [**Runtime Context**](#runtime-context) | data passed at the start of a run | ❌ | per run |
|
||||
| [**Short-term memory (State)**](#short-term-memory-mutable-context) | dynamic data that can change during execution | ✅ | per run or conversation |
|
||||
| [**Long-term memory (Store)**](#long-term-memory-cross-conversation-context) | data that can be shared between conversations | ✅ | across conversations |
|
||||
|
||||
## Provide runtime context
|
||||
### Runtime Context
|
||||
|
||||
### Config (static context)
|
||||
Runtime context is for immutable data like user metadata, tools, db connections, etc. Use this when you have values that don't change mid-run.
|
||||
|
||||
Config is for immutable data like user metadata or API keys. Use
|
||||
when you have values that don't change mid-run.
|
||||
!!! version-added "New in LangGraph v0.6: `Runtime.context` replaces `config['configurable']`"
|
||||
|
||||
Specify configuration using a key called **"configurable"** which is reserved
|
||||
for this purpose:
|
||||
The `Runtime` object is recommended to access static context and runtime-specific information like the store and stream writer.
|
||||
|
||||
!!! note
|
||||
|
||||
Runtime context refers to local context: data and dependencies your code needs to run. It does not refer to:
|
||||
|
||||
* The LLM context, which is the data passed into the LLM's prompt.
|
||||
* The "context window", which is the maximum number of tokens that can be passed to the LLM.
|
||||
|
||||
You likely want to use the local context to optimize the LLM's context window. For example, you
|
||||
could use a user id to fetch a user's name and information from a database to populate the context window with relevant memories.
|
||||
|
||||
Specify static context via the `context` argument to `invoke` / `stream`, which is reserved for this purpose:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ContextSchema:
|
||||
user_name: str
|
||||
|
||||
graph.invoke( # (1)!
|
||||
{"messages": [{"role": "user", "content": "hi!"}]}, # (2)!
|
||||
# highlight-next-line
|
||||
config={"configurable": {"user_id": "user_123"}} # (3)!
|
||||
context={"user_name": "John Smith"} # (3)!
|
||||
)
|
||||
```
|
||||
|
||||
1. This is the invocation of the agent or graph. The `invoke` method runs the underlying graph with the provided input.
|
||||
2. This example uses messages as an input, which is common, but your application may use different input structures.
|
||||
3. This is where you pass the configuration data. The `config` parameter allows you to provide additional context that the agent can use during its execution.
|
||||
3. This is where you pass the runtime data. The `context` parameter allows you to provide additional dependencies that the agent can use during its execution.
|
||||
|
||||
=== "Agent prompt"
|
||||
|
||||
```python
|
||||
from langchain_core.messages import AnyMessage
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.runtime import get_runtime
|
||||
from langgraph.prebuilt.chat_agent_executor import AgentState
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
|
||||
# highlight-next-line
|
||||
def prompt(state: AgentState, config: RunnableConfig) -> list[AnyMessage]:
|
||||
user_name = config["configurable"].get("user_name")
|
||||
system_msg = f"You are a helpful assistant. Address the user as {user_name}."
|
||||
def prompt(state: AgentState) -> list[AnyMessage]:
|
||||
runtime = get_runtime(ContextSchema)
|
||||
system_msg = f"You are a helpful assistant. Address the user as {runtime.context.user_name}."
|
||||
return [{"role": "system", "content": system_msg}] + state["messages"]
|
||||
|
||||
agent = create_react_agent(
|
||||
model="anthropic:claude-3-7-sonnet-latest",
|
||||
tools=[get_weather],
|
||||
prompt=prompt
|
||||
prompt=prompt,
|
||||
context_schema=ContextSchema
|
||||
)
|
||||
|
||||
agent.invoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
# highlight-next-line
|
||||
config={"configurable": {"user_name": "John Smith"}}
|
||||
context={"user_name": "John Smith"}
|
||||
)
|
||||
```
|
||||
|
||||
@@ -70,11 +85,11 @@ graph.invoke( # (1)!
|
||||
=== "Workflow node"
|
||||
|
||||
```python
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.runtime import Runtime
|
||||
|
||||
# highlight-next-line
|
||||
def node(state: State, config: RunnableConfig):
|
||||
user_name = config["configurable"].get("user_name")
|
||||
def node(state: State, config: Runtime[ContextSchema]):
|
||||
user_name = runtime.context.user_name
|
||||
...
|
||||
```
|
||||
|
||||
@@ -83,14 +98,16 @@ graph.invoke( # (1)!
|
||||
=== "In a tool"
|
||||
|
||||
```python
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.runtime import get_runtime
|
||||
|
||||
@tool
|
||||
# highlight-next-line
|
||||
def get_user_info(config: RunnableConfig) -> str:
|
||||
def get_user_email() -> str:
|
||||
"""Retrieve user information based on user ID."""
|
||||
user_id = config["configurable"].get("user_id")
|
||||
return "User is John Smith" if user_id == "user_123" else "Unknown user"
|
||||
# simulate fetching user info from a database
|
||||
runtime = get_runtime(ContextSchema)
|
||||
email = get_user_email_from_db(runtime.context.user_name)
|
||||
return email
|
||||
```
|
||||
|
||||
See the [tool calling guide](../how-tos/tool-calling.md#configuration) for details.
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
# Egress for Subscription Metrics and Operational Metadata
|
||||
|
||||
> **Important: Self Hosted Only**
|
||||
> This section only applies to customers who are not running in offline mode and assumes you are using a self-hosted LangGraph Platform instance.
|
||||
> This does not apply to SaaS or Hybrid deployments.
|
||||
|
||||
Self-Hosted LangGraph Platform instances store all information locally and will never send sensitive information outside of your network. We currently only track platform usage for billing purposes according to the entitlements in your order. In order to better remotely support our customers, we do require egress to `https://beacon.langchain.com`.
|
||||
|
||||
In the future, we will be introducing support diagnostics to help us ensure that the LangGraph Platform is running at an optimal level within your environment.
|
||||
|
||||
> **Warning**
|
||||
> **This will require egress to `https://beacon.langchain.com` from your network.**
|
||||
> **If using an API key, you will also need to allow egress to `https://api.smith.langchain.com` or `https://eu.api.smith.langchain.com` for API key verification.**
|
||||
|
||||
Generally, data that we send to Beacon can be categorized as follows:
|
||||
|
||||
- **Subscription Metrics**
|
||||
- Subscription metrics are used to determine level of access and utilization of LangSmith. This includes, but are not limited to:
|
||||
- Nodes Executed
|
||||
- Runs Executed
|
||||
- License Key Verification
|
||||
- **Operational Metadata**
|
||||
- This metadata will contain and collect the above subscription metrics to assist with remote support, allowing the LangChain team to diagnose and troubleshoot performance issues more effectively and proactively.
|
||||
|
||||
## Example Payloads
|
||||
|
||||
In an effort to maximize transparency, we provide sample payloads here:
|
||||
|
||||
### License Verification (If using an Enterprise License)
|
||||
|
||||
**Endpoint:**
|
||||
|
||||
`POST beacon.langchain.com/v1/beacon/verify`
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{
|
||||
"license": "<YOUR_LICENSE_KEY>"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "Valid JWT" // Short-lived JWT token to avoid repeated license checks
|
||||
}
|
||||
```
|
||||
|
||||
### Api Key Verification (If using a LangSmith API Key)
|
||||
|
||||
**Endpoint:**
|
||||
`POST api.smith.langchain.com/auth`
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
"Headers": {
|
||||
X-Api-Key: <YOUR_API_KEY>
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"org_config": {
|
||||
"org_id": "3a1c2b6f-4430-4b92-8a5b-79b8b567bbc1",
|
||||
... // Additional organization details
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Usage Reporting
|
||||
|
||||
**Endpoint:**
|
||||
|
||||
`POST beacon.langchain.com/v1/metadata/submit`
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{
|
||||
"license": "<YOUR_LICENSE_KEY>",
|
||||
"from_timestamp": "2025-01-06T09:00:00Z",
|
||||
"to_timestamp": "2025-01-06T10:00:00Z",
|
||||
"tags": {
|
||||
"langgraph.python.version": "0.1.0",
|
||||
"langgraph_api.version": "0.2.0",
|
||||
"langgraph.platform.revision": "abc123",
|
||||
"langgraph.platform.variant": "standard",
|
||||
"langgraph.platform.host": "host-1",
|
||||
"langgraph.platform.tenant_id": "3a1c2b6f-4430-4b92-8a5b-79b8b567bbc1",
|
||||
"langgraph.platform.project_id": "c5b5f53a-4716-4326-8967-d4f7f7799735",
|
||||
"langgraph.platform.plan": "enterprise",
|
||||
"user_app.uses_indexing": "true",
|
||||
"user_app.uses_custom_app": "false",
|
||||
"user_app.uses_custom_auth": "true",
|
||||
"user_app.uses_thread_ttl": "true",
|
||||
"user_app.uses_store_ttl": "false"
|
||||
},
|
||||
"measures": {
|
||||
"langgraph.platform.runs": 150,
|
||||
"langgraph.platform.nodes": 450
|
||||
},
|
||||
"logs": []
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
"204 No Content"
|
||||
```
|
||||
|
||||
## Our Commitment
|
||||
|
||||
LangChain will not store any sensitive information in the Subscription Metrics or Operational Metadata. Any data collected will not be shared with a third party. If you have any concerns about the data being sent, please reach out to your account team.
|
||||
@@ -23,6 +23,8 @@ Before deploying, review the [conceptual guide for the Self-Hosted Control Plane
|
||||
|
||||
kubectl get storageclass
|
||||
|
||||
1. Egress to `https://beacon.langchain.com` from your network. This is required for license verification and usage reporting if not running in air-gapped mode. See the [Egress documentation](../../cloud/deployment/egress.md) for more details.
|
||||
|
||||
## Setup
|
||||
|
||||
1. As part of configuring your Self-Hosted LangSmith instance, you enable the `langgraphPlatform` option. This will provision a few key resources.
|
||||
|
||||
@@ -108,11 +108,11 @@ from langgraph.graph import StateGraph, END, START
|
||||
from my_agent.utils.nodes import call_model, should_continue, tool_node # import nodes
|
||||
from my_agent.utils.state import AgentState # import state
|
||||
|
||||
# Define the config
|
||||
class GraphConfig(TypedDict):
|
||||
# Define the runtime context
|
||||
class GraphContext(TypedDict):
|
||||
model_name: Literal["anthropic", "openai"]
|
||||
|
||||
workflow = StateGraph(AgentState, config_schema=GraphConfig)
|
||||
workflow = StateGraph(AgentState, context_schema=GraphContext)
|
||||
workflow.add_node("agent", call_model)
|
||||
workflow.add_node("action", tool_node)
|
||||
workflow.add_edge(START, "agent")
|
||||
|
||||
@@ -121,11 +121,11 @@ from langgraph.graph import StateGraph, END, START
|
||||
from my_agent.utils.nodes import call_model, should_continue, tool_node # import nodes
|
||||
from my_agent.utils.state import AgentState # import state
|
||||
|
||||
# Define the config
|
||||
class GraphConfig(TypedDict):
|
||||
# Define the runtime context
|
||||
class GraphContext(TypedDict):
|
||||
model_name: Literal["anthropic", "openai"]
|
||||
|
||||
workflow = StateGraph(AgentState, config_schema=GraphConfig)
|
||||
workflow = StateGraph(AgentState, context_schema=GraphContext)
|
||||
workflow.add_node("agent", call_model)
|
||||
workflow.add_node("action", tool_node)
|
||||
workflow.add_edge(START, "agent")
|
||||
|
||||
@@ -24,6 +24,7 @@ Before deploying, review the [conceptual guide for the Standalone Container](../
|
||||
1. `LANGSMITH_API_KEY`: (if using [Lite](../../concepts/langgraph_server.md#server-versions)) LangSmith API key. This will be used to authenticate ONCE at server start up.
|
||||
1. `LANGGRAPH_CLOUD_LICENSE_KEY`: (if using [Enterprise](../../concepts/langgraph_data_plane.md#licensing)) LangGraph Platform license key. This will be used to authenticate ONCE at server start up.
|
||||
1. `LANGSMITH_ENDPOINT`: To send traces to a [self-hosted LangSmith](https://docs.smith.langchain.com/self_hosting) instance, set `LANGSMITH_ENDPOINT` to the hostname of the self-hosted LangSmith instance.
|
||||
1. Egress to `https://beacon.langchain.com` from your network. This is required for license verification and usage reporting if not running in air-gapped mode. See the [Egress documentation](../../cloud/deployment/egress.md) for more details.
|
||||
|
||||
## Kubernetes (Helm)
|
||||
|
||||
|
||||
@@ -30,9 +30,7 @@ To review, edit, and approve tool calls in an agent or workflow, use LangGraph's
|
||||
# > [
|
||||
# > {
|
||||
# > 'value': {'text_to_revise': 'original text'},
|
||||
# > 'resumable': True,
|
||||
# > 'ns': ['human_node:fc722478-2f21-0578-c572-d9fc4dd07c3b'],
|
||||
# > 'when': 'during'
|
||||
# > 'id': '...',
|
||||
# > }
|
||||
# > ]
|
||||
|
||||
@@ -203,9 +201,7 @@ To review, edit, and approve tool calls in an agent or workflow, use LangGraph's
|
||||
# > [
|
||||
# > {
|
||||
# > 'value': {'text_to_revise': 'original text'},
|
||||
# > 'resumable': True,
|
||||
# > 'ns': ['human_node:fc722478-2f21-0578-c572-d9fc4dd07c3b'],
|
||||
# > 'when': 'during'
|
||||
# > 'id': '...',
|
||||
# > }
|
||||
# > ]
|
||||
|
||||
|
||||
@@ -2,21 +2,20 @@
|
||||
|
||||
In this guide we will show how to create, configure, and manage an [assistant](../../concepts/assistants.md).
|
||||
|
||||
First, as a brief refresher on the concept of configurations, consider the following simple `call_model` node and configuration schema. Observe that this node tries to read and use the `model_name` as defined by the `config` object's `configurable`.
|
||||
First, as a brief refresher on the concept of runtime context, consider the following simple `call_model` node and context schema. Observe that this node tries to read and use the `model_provider` as defined by the `Runtime` object's `context` property.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ContextSchema:
|
||||
llm_provider: str = "anthropic"
|
||||
|
||||
class ConfigSchema(TypedDict):
|
||||
model_name: str
|
||||
builder = StateGraph(AgentState, context_schema=ContextSchema)
|
||||
|
||||
builder = StateGraph(AgentState, config_schema=ConfigSchema)
|
||||
|
||||
def call_model(state, config):
|
||||
def call_model(state, runtime: Runtime[ContextSchema]):
|
||||
messages = state["messages"]
|
||||
model_name = config.get('configurable', {}).get("model_name", "anthropic")
|
||||
model = _get_model(model_name)
|
||||
model = _get_model(runtime.context.llm_provider)
|
||||
response = model.invoke(messages)
|
||||
# We return a list, because this will get added to the existing list
|
||||
return {"messages": [response]}
|
||||
@@ -44,7 +43,7 @@ First, as a brief refresher on the concept of configurations, consider the follo
|
||||
}
|
||||
```
|
||||
|
||||
For more information on configurations, [see here](../../concepts/low_level.md#configuration).
|
||||
For more information on runtime context, [see here](../../concepts/low_level.md#runtime-context).
|
||||
|
||||
## Create an assistant
|
||||
|
||||
|
||||
@@ -30,17 +30,33 @@ export default {
|
||||
|
||||
Next, define your UI components in your `langgraph.json` configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"node_version": "20",
|
||||
"graphs": {
|
||||
"agent": "./src/agent/index.ts:graph"
|
||||
},
|
||||
"ui": {
|
||||
"agent": "./src/agent/ui.tsx"
|
||||
}
|
||||
}
|
||||
```
|
||||
=== "Python agent"
|
||||
|
||||
```json title="langgraph.json"
|
||||
{
|
||||
"node_version": "20",
|
||||
"graphs": {
|
||||
"agent": "./src/agent.py:graph"
|
||||
},
|
||||
"ui": {
|
||||
"agent": "./src/agent/ui.tsx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS agent"
|
||||
|
||||
```json title="langgraph.json"
|
||||
{
|
||||
"node_version": "20",
|
||||
"graphs": {
|
||||
"agent": "./src/agent/index.ts:graph"
|
||||
},
|
||||
"ui": {
|
||||
"agent": "./src/agent/ui.tsx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `ui` section points to the UI components that will be used by graphs. By default, we recommend using the same key as the graph name, but you can split out the components however you like, see [Customise the namespace of UI components](#customise-the-namespace-of-ui-components) for more details.
|
||||
|
||||
|
||||
@@ -140,6 +140,22 @@ https://my-server.app/my-webhook-endpoint?token=YOUR_SECRET_TOKEN
|
||||
|
||||
Your server should extract and validate this token before processing requests.
|
||||
|
||||
## Disable webhooks
|
||||
|
||||
As of `langgraph-api>=0.2.78`, developers can disable webhooks in the `langgraph.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"http": {
|
||||
"disable_webhooks": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This feature is primarily intended for self-hosted deployments, where platform administrators or developers may prefer to disable webhooks to simplify their security posture—especially if they are not configuring firewall rules or other network controls. Disabling webhooks helps prevent untrusted payloads from being sent to internal endpoints.
|
||||
|
||||
For full configuration details, refer to the [configuration file reference](https://langchain-ai.github.io/langgraph/cloud/reference/cli/?h=disable_webhooks#configuration-file).
|
||||
|
||||
## Test webhooks
|
||||
|
||||
You can test your webhook using online services like:
|
||||
|
||||
@@ -4,6 +4,50 @@
|
||||
|
||||
---
|
||||
|
||||
## v0.2.109 (2025-07-28)
|
||||
- Fixed an issue where missing config schema occurred when `config_type` was not set.
|
||||
|
||||
## v0.2.108 (2025-07-28)
|
||||
- Added compatibility for langgraph v0.6, including new context API support and a migration to enhance context handling in assistant operations.
|
||||
|
||||
## v0.2.107 (2025-07-27)
|
||||
- Implemented caching for authentication processes to improve performance.
|
||||
- Merged count and select queries to improve database query efficiency.
|
||||
|
||||
## v0.2.106 (2025-07-27)
|
||||
- Log whether run uses resumable streams.
|
||||
|
||||
## v0.2.105 (2025-07-27)
|
||||
- Added a `/heapdump` endpoint to capture and save JS process heap data.
|
||||
|
||||
## v0.2.103 (2025-07-25)
|
||||
- Corrected the metadata endpoint to ensure accurate data retrieval.
|
||||
|
||||
## v0.2.102 (2025-07-24)
|
||||
- Captured interrupt events in the wait method to preserve legacy behavior and stream updates by default.
|
||||
- Added support for SDK structlog in the JavaScript environment, enhancing logging capabilities.
|
||||
|
||||
## v0.2.101 (2025-07-24)
|
||||
- Used the correct metadata endpoint for self-hosted environments, resolving an access issue.
|
||||
|
||||
## v0.2.99 (2025-07-22)
|
||||
- Improved license validation by adding an in-memory cache and handling Redis connection errors more effectively.
|
||||
- Automatically remove agents from memory that are removed from `langgraph.json` to prevent persistence issues.
|
||||
- Ensured the UI namespace for generated UI is a valid JavaScript property name to prevent errors.
|
||||
- Raised a 422 error for improved request validation feedback.
|
||||
|
||||
## v0.2.98 (2025-07-19)
|
||||
- Added langgraph node context for improved log filtering and trace visibility.
|
||||
|
||||
## v0.2.97 (2025-07-19)
|
||||
- Fixed scheduling issue with ckpt ingestion worker that occurred on isolated background loops.
|
||||
- Ensured queue worker starts only after all migrations have completed.
|
||||
- Added more detailed error messages for thread state issues and improved response handling when state updates fail.
|
||||
- Exposed interrupt ID while retrieving thread state for enhanced API response details.
|
||||
|
||||
## v0.2.96 (2025-07-17)
|
||||
- Added a fallback mechanism for configurable header patterns to handle exclude/include settings more effectively.
|
||||
|
||||
## v0.2.95 (2025-07-17)
|
||||
- Avoided setting the future if it is already done to prevent redundant operations.
|
||||
- Resolved compatibility errors in CI by switching from `typing.TypedDict` to `typing_extensions.TypedDict` for Python versions below 3.12.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Assistants
|
||||
|
||||
**Assistants** allow you to manage configurations (like prompts, LLM selection, tools) separately from your graph's core logic, enabling rapid changes that don't alter the graph architecture. It is a way to create multiple specialized versions of the same graph architecture, each optimized for different use cases through configuration variations rather than structural changes.
|
||||
**Assistants** allow you to manage configurations (like prompts, LLM selection, tools) separately from your graph's core logic, enabling rapid changes that don't alter the graph architecture. It is a way to create multiple specialized versions of the same graph architecture, each optimized for different use cases through context/configuration variations rather than structural changes.
|
||||
|
||||
For example, imagine a general-purpose writing agent built on a common graph architecture. While the structure remains the same, different writing styles—such as blog posts and tweets—require tailored configurations to optimize performance. To support these variations, you can create multiple assistants (e.g., one for blogs and another for tweets) that share the underlying graph but differ in model selection and system prompt.
|
||||
|
||||
@@ -14,8 +14,8 @@ The LangGraph Cloud API provides several endpoints for creating and managing ass
|
||||
|
||||
## Configuration
|
||||
|
||||
Assistants build on the LangGraph open source concept of [configuration](low_level.md#configuration).
|
||||
While configuration is available in the open source LangGraph library, assistants are only present in [LangGraph Platform](langgraph_platform.md). This is due to the fact that assistants are tightly coupled to your deployed graph. Upon deployment, LangGraph Server will automatically create a default assistant for each graph using the graph's default configuration settings.
|
||||
Assistants build on the LangGraph open source concepts of configuration and [runtime context](low_level.md#runtime-context).
|
||||
While these features are available in the open source LangGraph library, assistants are only present in [LangGraph Platform](langgraph_platform.md). This is due to the fact that assistants are tightly coupled to your deployed graph. Upon deployment, LangGraph Server will automatically create a default assistant for each graph using the graph's default context and configuration settings.
|
||||
|
||||
In practice, an assistant is just an _instance_ of a graph with a specific configuration. Therefore, multiple assistants can reference the same graph but can contain different configurations (e.g. prompts, models, tools). The LangGraph Server API provides several endpoints for creating and managing assistants. See the [API reference](../cloud/reference/api/api_ref.html) and [this how-to](../cloud/how-tos/configuration_cloud.md) for more details on how to create assistants.
|
||||
|
||||
@@ -26,6 +26,6 @@ Once you've created an assistant, subsequent edits to that assistant will create
|
||||
|
||||
## Execution
|
||||
|
||||
A **run** is an invocation of an assistant. Each run may have its own input, configuration, and metadata, which may affect execution and output of the underlying graph. A run can optionally be executed on a [thread](./persistence.md#threads).
|
||||
A **run** is an invocation of an assistant. Each run may have its own input, configuration, context, and metadata, which may affect execution and output of the underlying graph. A run can optionally be executed on a [thread](./persistence.md#threads).
|
||||
|
||||
The LangGraph Platform API provides several endpoints for creating and managing runs. See the [API reference](../cloud/reference/api/api_ref.html#tag/thread-runs/) for more details.
|
||||
|
||||
@@ -10,7 +10,7 @@ search:
|
||||
There are two free options for deploying LangGraph applications via the LangGraph Server:
|
||||
|
||||
1. [Local](../tutorials/langgraph-platform/local-server.md): Deploy for local testing and development.
|
||||
1. [Standalone Container (Lite)](../concepts/langgraph_standalone_container.md): A limited version of Standalone Container for deployments unlikely to see more that 1 million node executions per year and that do not need crons and other enterprise features. Standalone Container (Lite) deployment option is free with a LangSmith API key.
|
||||
1. [Standalone Container (Lite)](../concepts/langgraph_standalone_container.md): A limited version of Standalone Container for deployments unlikely to see more than 1 million node executions per year and that do not need crons and other enterprise features. Standalone Container (Lite) deployment option is free with a LangSmith API key.
|
||||
|
||||
## Production deployment
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ If a [node](./low_level.md#nodes) contains multiple operations, you may find it
|
||||
from typing_extensions import TypedDict
|
||||
import uuid
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
import requests
|
||||
|
||||
@@ -74,7 +74,7 @@ If a [node](./low_level.md#nodes) contains multiple operations, you may find it
|
||||
builder.add_edge("call_api", END)
|
||||
|
||||
# Specify a checkpointer
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
|
||||
# Compile the graph with the checkpointer
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
@@ -94,7 +94,7 @@ If a [node](./low_level.md#nodes) contains multiple operations, you may find it
|
||||
from typing_extensions import TypedDict
|
||||
import uuid
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.func import task
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
import requests
|
||||
@@ -129,7 +129,7 @@ If a [node](./low_level.md#nodes) contains multiple operations, you may find it
|
||||
builder.add_edge("call_api", END)
|
||||
|
||||
# Specify a checkpointer
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
|
||||
# Compile the graph with the checkpointer
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
|
||||
@@ -39,7 +39,7 @@ Here are some key differences:
|
||||
Below we demonstrate a simple application that writes an essay and [interrupts](human_in_the_loop.md) to request human review.
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import interrupt
|
||||
|
||||
@@ -50,7 +50,7 @@ def write_essay(topic: str) -> str:
|
||||
time.sleep(1) # A placeholder for a long-running task.
|
||||
return f"An essay about topic: {topic}"
|
||||
|
||||
@entrypoint(checkpointer=MemorySaver())
|
||||
@entrypoint(checkpointer=InMemorySaver())
|
||||
def workflow(topic: str) -> dict:
|
||||
"""A simple workflow that writes an essay and asks for a review."""
|
||||
essay = write_essay("cat").result()
|
||||
@@ -79,51 +79,54 @@ def workflow(topic: str) -> dict:
|
||||
```python
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import interrupt
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
|
||||
@task
|
||||
def write_essay(topic: str) -> str:
|
||||
"""Write an essay about the given topic."""
|
||||
time.sleep(1) # This is a placeholder for a long-running task.
|
||||
time.sleep(1) # This is a placeholder for a long-running task.
|
||||
return f"An essay about topic: {topic}"
|
||||
|
||||
@entrypoint(checkpointer=MemorySaver())
|
||||
@entrypoint(checkpointer=InMemorySaver())
|
||||
def workflow(topic: str) -> dict:
|
||||
"""A simple workflow that writes an essay and asks for a review."""
|
||||
essay = write_essay("cat").result()
|
||||
is_approved = interrupt({
|
||||
# Any json-serializable payload provided to interrupt as argument.
|
||||
# It will be surfaced on the client side as an Interrupt when streaming data
|
||||
# from the workflow.
|
||||
"essay": essay, # The essay we want reviewed.
|
||||
# We can add any additional information that we need.
|
||||
# For example, introduce a key called "action" with some instructions.
|
||||
"action": "Please approve/reject the essay",
|
||||
})
|
||||
|
||||
is_approved = interrupt(
|
||||
{
|
||||
# Any json-serializable payload provided to interrupt as argument.
|
||||
# It will be surfaced on the client side as an Interrupt when streaming data
|
||||
# from the workflow.
|
||||
"essay": essay, # The essay we want reviewed.
|
||||
# We can add any additional information that we need.
|
||||
# For example, introduce a key called "action" with some instructions.
|
||||
"action": "Please approve/reject the essay",
|
||||
}
|
||||
)
|
||||
return {
|
||||
"essay": essay, # The essay that was generated
|
||||
"is_approved": is_approved, # Response from HIL
|
||||
"essay": essay, # The essay that was generated
|
||||
"is_approved": is_approved, # Response from HIL
|
||||
}
|
||||
|
||||
|
||||
thread_id = str(uuid.uuid4())
|
||||
|
||||
config = {
|
||||
"configurable": {
|
||||
"thread_id": thread_id
|
||||
}
|
||||
}
|
||||
|
||||
config = {"configurable": {"thread_id": thread_id}}
|
||||
for item in workflow.stream("cat", config):
|
||||
print(item)
|
||||
```
|
||||
|
||||
```pycon
|
||||
{'write_essay': 'An essay about topic: cat'}
|
||||
{'__interrupt__': (Interrupt(value={'essay': 'An essay about topic: cat', 'action': 'Please approve/reject the essay'}, resumable=True, ns=['workflow:f7b8508b-21c0-8b4c-5958-4e8de74d2684'], when='during'),)}
|
||||
# > {'write_essay': 'An essay about topic: cat'}
|
||||
# > {
|
||||
# > '__interrupt__': (
|
||||
# > Interrupt(
|
||||
# > value={
|
||||
# > 'essay': 'An essay about topic: cat',
|
||||
# > 'action': 'Please approve/reject the essay'
|
||||
# > },
|
||||
# > id='b9b2b9d788f482663ced6dc755c9e981'
|
||||
# > ),
|
||||
# > )
|
||||
# > }
|
||||
```
|
||||
|
||||
An essay has been written and is ready for review. Once the review is provided, we can resume the workflow:
|
||||
|
||||
@@ -23,12 +23,12 @@ To review, edit, and approve tool calls in an agent or workflow, [use LangGraph'
|
||||
|
||||
## Key capabilities
|
||||
|
||||
* **Persistent execution state**: Interrupts use LangGraph's [persistence](../../concepts/persistence.md) layer, which saves the graph state, to indefinitely pause graph execution until you resume. This is possible because LangGraph checkpoints the graph state after each step, which allows the system to persist execution context and later resume the workflow, continuing from where it left off. This supports asynchronous human review or input without time constraints.
|
||||
* **Persistent execution state**: Interrupts use LangGraph's [persistence](./persistence.md) layer, which saves the graph state, to indefinitely pause graph execution until you resume. This is possible because LangGraph checkpoints the graph state after each step, which allows the system to persist execution context and later resume the workflow, continuing from where it left off. This supports asynchronous human review or input without time constraints.
|
||||
|
||||
There are two ways to pause a graph:
|
||||
|
||||
- [Dynamic interrupts](../how-tos/human_in_the_loop/add-human-in-the-loop.md#pause-using-interrupt): Use `interrupt` to pause a graph from inside a specific node, based on the current state of the graph.
|
||||
- [Static interrupts](../how-tos/human_in_the_loop/add-human-in-the-loop.md#debug-with-interrupts): Use `interrupt_before` and `interrupt_after` to pause the graph at defined points, either before or after a node executes.
|
||||
- [Static interrupts](../how-tos/human_in_the_loop/add-human-in-the-loop.md#debug-with-interrupts): Use `interrupt_before` and `interrupt_after` to pause the graph at pre-defined points, either before or after a node executes.
|
||||
|
||||
<figure markdown="1">
|
||||
{: style="max-height:400px"}
|
||||
|
||||
@@ -119,6 +119,11 @@ These metrics are displayed as charts in the Control Plane UI.
|
||||
|
||||
### LangSmith Integration
|
||||
|
||||
A [LangSmith](https://docs.smith.langchain.com/) tracing project is automatically created for each deployment. The tracing project has the same name as the deployment. When creating a deployment, the `LANGCHAIN_TRACING` and `LANGSMITH_API_KEY`/`LANGCHAIN_API_KEY` environment variables do not need to be specified; they are set automatically by the control plane.
|
||||
A [LangSmith](https://docs.smith.langchain.com/) tracing project and LangSmith API key are automatically created for each deployment. The deployment uses the API key to automatically send traces to LangSmith.
|
||||
|
||||
When a deployment is deleted, the traces and the tracing project are not deleted.
|
||||
- The tracing project has the same name as the deployment.
|
||||
- The API key has the description `LangGraph Platform: <deployment_name>`.
|
||||
- The API key is never revealed and cannot be deleted manually.
|
||||
- When creating a deployment, the `LANGCHAIN_TRACING` and `LANGSMITH_API_KEY`/`LANGCHAIN_API_KEY` environment variables do not need to be specified; they are set automatically by the control plane.
|
||||
|
||||
When a deployment is deleted, the traces and the tracing project are not deleted. However, the API will be deleted when the deployment is deleted.
|
||||
|
||||
@@ -192,35 +192,48 @@ class State(MessagesState):
|
||||
|
||||
## Nodes
|
||||
|
||||
In LangGraph, nodes are typically python functions (sync or async) where the **first** positional argument is the [state](#state), and (optionally), the **second** positional argument is a "config", containing optional [configurable parameters](#configuration) (such as a `thread_id`).
|
||||
In LangGraph, nodes are Python functions (either synchronous or asynchronous) that accept the following arguments:
|
||||
|
||||
1. `state`: The [state](#state) of the graph
|
||||
2. `config`: A `RunnableConfig` object that contains configuration information like `thread_id` and tracing information like `tags`
|
||||
3. `runtime`: A `Runtime` object that contains [runtime `context`](#runtime-context) and other information like `store` and `stream_writer`
|
||||
|
||||
|
||||
Similar to `NetworkX`, you add these nodes to a graph using the [add_node][langgraph.graph.StateGraph.add_node] method:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.runtime import Runtime
|
||||
|
||||
class State(TypedDict):
|
||||
input: str
|
||||
results: str
|
||||
|
||||
@dataclass
|
||||
class Context:
|
||||
user_id: str
|
||||
|
||||
builder = StateGraph(State)
|
||||
|
||||
def plain_node(state: State):
|
||||
return state
|
||||
|
||||
def my_node(state: State, config: RunnableConfig):
|
||||
print("In node: ", config["configurable"]["user_id"])
|
||||
def node_with_runtime(state: State, runtime: Runtime[Context]):
|
||||
print("In node: ", runtime.context.user_id)
|
||||
return {"results": f"Hello, {state['input']}!"}
|
||||
|
||||
def node_with_config(state: State, config: RunnableConfig):
|
||||
print("In node with thread_id: ", config["configurable"]["thread_id"])
|
||||
return {"results": f"Hello, {state['input']}!"}
|
||||
|
||||
|
||||
# The second argument is optional
|
||||
def my_other_node(state: State):
|
||||
return state
|
||||
|
||||
|
||||
builder.add_node("my_node", my_node)
|
||||
builder.add_node("other_node", my_other_node)
|
||||
builder.add_node("plain_node", plain_node)
|
||||
builder.add_node("node_with_runtime", node_with_runtime)
|
||||
builder.add_node("node_with_config", node_with_config)
|
||||
...
|
||||
```
|
||||
|
||||
@@ -459,33 +472,32 @@ LangGraph can easily handle migrations of graph definitions (nodes, edges, and s
|
||||
- State keys that are renamed lose their saved state in existing threads
|
||||
- State keys whose types change in incompatible ways could currently cause issues in threads with state from before the change -- if this is a blocker please reach out and we can prioritize a solution.
|
||||
|
||||
## Configuration
|
||||
## Runtime Context
|
||||
|
||||
When creating a graph, you can also mark that certain parts of the graph are configurable. This is commonly done to enable easily switching between models or system prompts. This allows you to create a single "cognitive architecture" (the graph) but have multiple different instance of it.
|
||||
|
||||
You can optionally specify a `config_schema` when creating a graph.
|
||||
When creating a graph, you can specify a `context_schema` for runtime context passed to nodes. This is useful for passing
|
||||
information to nodes that is not part of the graph state. For example, you might want to pass dependencies such as model name or a database connection.
|
||||
|
||||
```python
|
||||
class ConfigSchema(TypedDict):
|
||||
llm: str
|
||||
@dataclass
|
||||
class ContextSchema:
|
||||
llm_provider: str = "openai"
|
||||
|
||||
graph = StateGraph(State, config_schema=ConfigSchema)
|
||||
graph = StateGraph(State, context_schema=ContextSchema)
|
||||
```
|
||||
|
||||
You can then pass this configuration into the graph using the `configurable` config field.
|
||||
You can then pass this context into the graph using the `context` parameter of the `invoke` method.
|
||||
|
||||
```python
|
||||
config = {"configurable": {"llm": "anthropic"}}
|
||||
|
||||
graph.invoke(inputs, config=config)
|
||||
graph.invoke(inputs, context={"llm_provider": "anthropic"})
|
||||
```
|
||||
|
||||
You can then access and use this configuration inside a node or conditional edge:
|
||||
You can then access and use this context inside a node or conditional edge:
|
||||
|
||||
```python
|
||||
def node_a(state, config):
|
||||
llm_type = config.get("configurable", {}).get("llm", "openai")
|
||||
llm = get_llm(llm_type)
|
||||
from langgraph.runtime import Runtime
|
||||
|
||||
def node_a(state: State, runtime: Runtime[ContextSchema]):
|
||||
llm = get_llm(runtime.context.llm_provider)
|
||||
...
|
||||
```
|
||||
|
||||
@@ -496,7 +508,7 @@ See [this guide](../how-tos/graph-api.md#add-runtime-configuration) for a full b
|
||||
The recursion limit sets the maximum number of [super-steps](#graphs) the graph can execute during a single execution. Once the limit is reached, LangGraph will raise `GraphRecursionError`. By default this value is set to 25 steps. The recursion limit can be set on any graph at runtime, and is passed to `.invoke`/`.stream` via the config dictionary. Importantly, `recursion_limit` is a standalone `config` key and should not be passed inside the `configurable` key as all other user-defined configuration. See the example below:
|
||||
|
||||
```python
|
||||
graph.invoke(inputs, config={"recursion_limit": 5, "configurable":{"llm": "anthropic"}})
|
||||
graph.invoke(inputs, config={"recursion_limit": 5}, context={"llm": "anthropic"})
|
||||
```
|
||||
|
||||
Read [this how-to](https://langchain-ai.github.io/langgraph/how-tos/recursion-limit/) to learn more about how the recursion limit works.
|
||||
|
||||
@@ -487,12 +487,12 @@ If you want to fallback to pickle for objects not currently supported by our msg
|
||||
you can use the `pickle_fallback` argument of the `JsonPlusSerializer`:
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||
|
||||
# ... Define the graph ...
|
||||
graph.compile(
|
||||
checkpointer=MemorySaver(serde=JsonPlusSerializer(pickle_fallback=True))
|
||||
checkpointer=InMemorySaver(serde=JsonPlusSerializer(pickle_fallback=True))
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
@@ -77,7 +77,7 @@
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdin",
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"OPENAI_API_KEY: ········\n"
|
||||
@@ -165,7 +165,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"execution_count": null,
|
||||
"id": "d129e4e1-3766-429a-b806-cde3d8bc0469",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -173,7 +173,7 @@
|
||||
"from langchain_core.messages import convert_to_openai_messages, BaseMessage\n",
|
||||
"from langgraph.func import entrypoint, task\n",
|
||||
"from langgraph.graph import add_messages\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@task\n",
|
||||
@@ -192,7 +192,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"# add short-term memory for storing conversation history\n",
|
||||
"checkpointer = MemorySaver()\n",
|
||||
"checkpointer = InMemorySaver()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@entrypoint(checkpointer=checkpointer)\n",
|
||||
@@ -222,12 +222,12 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
|
||||
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
|
||||
"\n",
|
||||
"Find numbers between 10 and 30 in fibonacci sequence\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
|
||||
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"To find numbers between 10 and 30 in the Fibonacci sequence, we can generate the Fibonacci sequence and check which numbers fall within this range. Here's a plan:\n",
|
||||
"\n",
|
||||
@@ -253,9 +253,9 @@
|
||||
"This script will print the Fibonacci numbers between 10 and 30. Please execute the code to see the result.\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001B[31m\n",
|
||||
">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001B[0m\n",
|
||||
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
|
||||
"\u001b[31m\n",
|
||||
">>>>>>>> EXECUTING CODE BLOCK 0 (inferred language is python)...\u001b[0m\n",
|
||||
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
|
||||
"\n",
|
||||
"exitcode: 0 (execution succeeded)\n",
|
||||
"Code output: \n",
|
||||
@@ -264,7 +264,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
|
||||
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"The Fibonacci numbers between 10 and 30 are 13 and 21. \n",
|
||||
"\n",
|
||||
@@ -318,7 +318,7 @@
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001B[33muser_proxy\u001B[0m (to assistant):\n",
|
||||
"\u001b[33muser_proxy\u001b[0m (to assistant):\n",
|
||||
"\n",
|
||||
"Multiply the last number by 3\n",
|
||||
"Context: \n",
|
||||
@@ -334,7 +334,7 @@
|
||||
"TERMINATE\n",
|
||||
"\n",
|
||||
"--------------------------------------------------------------------------------\n",
|
||||
"\u001B[33massistant\u001B[0m (to user_proxy):\n",
|
||||
"\u001b[33massistant\u001b[0m (to user_proxy):\n",
|
||||
"\n",
|
||||
"The last number in the Fibonacci sequence between 10 and 30 is 21. Multiplying 21 by 3 gives:\n",
|
||||
"\n",
|
||||
|
||||
@@ -75,7 +75,7 @@ We will now create a LangGraph chatbot graph that calls AutoGen agent.
|
||||
```python
|
||||
from langchain_core.messages import convert_to_openai_messages
|
||||
from langgraph.graph import StateGraph, MessagesState, START
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
def call_autogen_agent(state: MessagesState):
|
||||
# Convert LangGraph messages to OpenAI format for AutoGen
|
||||
@@ -101,7 +101,7 @@ def call_autogen_agent(state: MessagesState):
|
||||
return {"messages": {"role": "assistant", "content": final_content}}
|
||||
|
||||
# Create the graph with memory for persistence
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
|
||||
# Build the graph
|
||||
builder = StateGraph(MessagesState)
|
||||
@@ -228,7 +228,7 @@ my-autogen-agent/
|
||||
import autogen
|
||||
from langchain_core.messages import convert_to_openai_messages
|
||||
from langgraph.graph import StateGraph, MessagesState, START
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
# AutoGen configuration
|
||||
config_list = [{"model": "gpt-4o", "api_key": os.environ["OPENAI_API_KEY"]}]
|
||||
@@ -276,7 +276,7 @@ my-autogen-agent/
|
||||
|
||||
# Create and compile the graph
|
||||
def create_graph():
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
builder = StateGraph(MessagesState)
|
||||
builder.add_node("autogen", call_autogen_agent)
|
||||
builder.add_edge(START, "autogen")
|
||||
@@ -290,7 +290,7 @@ my-autogen-agent/
|
||||
|
||||
```
|
||||
langgraph>=0.1.0
|
||||
pyautogen>=0.2.0
|
||||
ag2>=0.2.0
|
||||
langchain-core>=0.1.0
|
||||
langchain-openai>=0.0.5
|
||||
```
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
"from langchain_core.messages import BaseMessage\n",
|
||||
"from langgraph.func import entrypoint, task\n",
|
||||
"from langgraph.graph import add_messages\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
"from langgraph.store.base import BaseStore\n",
|
||||
"\n",
|
||||
"\n",
|
||||
@@ -192,7 +192,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"# NOTE: we're passing the store object here when creating a workflow via entrypoint()\n",
|
||||
"@entrypoint(checkpointer=MemorySaver(), store=in_memory_store)\n",
|
||||
"@entrypoint(checkpointer=InMemorySaver(), store=in_memory_store)\n",
|
||||
"def workflow(\n",
|
||||
" inputs: list[BaseMessage],\n",
|
||||
" *,\n",
|
||||
|
||||
@@ -514,12 +514,12 @@ To add runtime configuration:
|
||||
See below for a simple example:
|
||||
|
||||
```python
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.graph import END, StateGraph, START
|
||||
from langgraph.runtime import Runtime
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
# 1. Specify config schema
|
||||
class ConfigSchema(TypedDict):
|
||||
class ContextSchema(TypedDict):
|
||||
my_runtime_value: str
|
||||
|
||||
# 2. Define a graph that accesses the config in a node
|
||||
@@ -527,18 +527,18 @@ class State(TypedDict):
|
||||
my_state_value: str
|
||||
|
||||
# highlight-next-line
|
||||
def node(state: State, config: RunnableConfig):
|
||||
def node(state: State, runtime: Runtime[ContextSchema]):
|
||||
# highlight-next-line
|
||||
if config["configurable"]["my_runtime_value"] == "a":
|
||||
if runtime.context["my_runtime_value"] == "a":
|
||||
return {"my_state_value": 1}
|
||||
# highlight-next-line
|
||||
elif config["configurable"]["my_runtime_value"] == "b":
|
||||
elif runtime.context["my_runtime_value"] == "b":
|
||||
return {"my_state_value": 2}
|
||||
else:
|
||||
raise ValueError("Unknown values.")
|
||||
|
||||
# highlight-next-line
|
||||
builder = StateGraph(State, config_schema=ConfigSchema)
|
||||
builder = StateGraph(State, context_schema=ContextSchema)
|
||||
builder.add_node(node)
|
||||
builder.add_edge(START, "node")
|
||||
builder.add_edge("node", END)
|
||||
@@ -547,9 +547,9 @@ graph = builder.compile()
|
||||
|
||||
# 3. Pass in configuration at runtime:
|
||||
# highlight-next-line
|
||||
print(graph.invoke({}, {"configurable": {"my_runtime_value": "a"}}))
|
||||
print(graph.invoke({}, context={"my_runtime_value": "a"}))
|
||||
# highlight-next-line
|
||||
print(graph.invoke({}, {"configurable": {"my_runtime_value": "b"}}))
|
||||
print(graph.invoke({}, context={"my_runtime_value": "b"}))
|
||||
```
|
||||
```
|
||||
{'my_state_value': 1}
|
||||
@@ -560,27 +560,28 @@ print(graph.invoke({}, {"configurable": {"my_runtime_value": "b"}}))
|
||||
Below we demonstrate a practical example in which we configure what LLM to use at runtime. We will use both OpenAI and Anthropic models.
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
|
||||
from langchain.chat_models import init_chat_model
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.graph import MessagesState
|
||||
from langgraph.graph import END, StateGraph, START
|
||||
from langgraph.graph import MessagesState, END, StateGraph, START
|
||||
from langgraph.runtime import Runtime
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
class ConfigSchema(TypedDict):
|
||||
model: str
|
||||
@dataclass
|
||||
class ContextSchema:
|
||||
model_provider: str = "anthropic"
|
||||
|
||||
MODELS = {
|
||||
"anthropic": init_chat_model("anthropic:claude-3-5-haiku-latest"),
|
||||
"openai": init_chat_model("openai:gpt-4.1-mini"),
|
||||
}
|
||||
|
||||
def call_model(state: MessagesState, config: RunnableConfig):
|
||||
model = config["configurable"].get("model", "anthropic")
|
||||
model = MODELS[model]
|
||||
def call_model(state: MessagesState, runtime: Runtime[ContextSchema]):
|
||||
model = MODELS[runtime.context.model_provider]
|
||||
response = model.invoke(state["messages"])
|
||||
return {"messages": [response]}
|
||||
|
||||
builder = StateGraph(MessagesState, config_schema=ConfigSchema)
|
||||
builder = StateGraph(MessagesState, context_schema=ContextSchema)
|
||||
builder.add_node("model", call_model)
|
||||
builder.add_edge(START, "model")
|
||||
builder.add_edge("model", END)
|
||||
@@ -592,8 +593,7 @@ print(graph.invoke({}, {"configurable": {"my_runtime_value": "b"}}))
|
||||
# With no configuration, uses default (Anthropic)
|
||||
response_1 = graph.invoke({"messages": [input_message]})["messages"][-1]
|
||||
# Or, can set OpenAI
|
||||
config = {"configurable": {"model": "openai"}}
|
||||
response_2 = graph.invoke({"messages": [input_message]}, config=config)["messages"][-1]
|
||||
response_2 = graph.invoke({"messages": [input_message]}, context={"model_provider": "openai"})["messages"][-1]
|
||||
|
||||
print(response_1.response_metadata["model_name"])
|
||||
print(response_2.response_metadata["model_name"])
|
||||
@@ -607,32 +607,33 @@ print(graph.invoke({}, {"configurable": {"my_runtime_value": "b"}}))
|
||||
Below we demonstrate a practical example in which we configure two parameters: the LLM and system message to use at runtime.
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from langchain.chat_models import init_chat_model
|
||||
from langchain_core.messages import SystemMessage
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.graph import END, MessagesState, StateGraph, START
|
||||
from langgraph.runtime import Runtime
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
class ConfigSchema(TypedDict):
|
||||
model: Optional[str]
|
||||
system_message: Optional[str]
|
||||
@dataclass
|
||||
class ContextSchema:
|
||||
model_provider: str = "anthropic"
|
||||
system_message: str | None = None
|
||||
|
||||
MODELS = {
|
||||
"anthropic": init_chat_model("anthropic:claude-3-5-haiku-latest"),
|
||||
"openai": init_chat_model("openai:gpt-4.1-mini"),
|
||||
}
|
||||
|
||||
def call_model(state: MessagesState, config: RunnableConfig):
|
||||
model = config["configurable"].get("model", "anthropic")
|
||||
model = MODELS[model]
|
||||
def call_model(state: MessagesState, runtime: Runtime[ContextSchema]):
|
||||
model = MODELS[runtime.context.model_provider]
|
||||
messages = state["messages"]
|
||||
if system_message := config["configurable"].get("system_message"):
|
||||
if (system_message := runtime.context.system_message):
|
||||
messages = [SystemMessage(system_message)] + messages
|
||||
response = model.invoke(messages)
|
||||
return {"messages": [response]}
|
||||
|
||||
builder = StateGraph(MessagesState, config_schema=ConfigSchema)
|
||||
builder = StateGraph(MessagesState, context_schema=ContextSchema)
|
||||
builder.add_node("model", call_model)
|
||||
builder.add_edge(START, "model")
|
||||
builder.add_edge("model", END)
|
||||
@@ -641,8 +642,7 @@ print(graph.invoke({}, {"configurable": {"my_runtime_value": "b"}}))
|
||||
|
||||
# Usage
|
||||
input_message = {"role": "user", "content": "hi"}
|
||||
config = {"configurable": {"model": "openai", "system_message": "Respond in Italian."}}
|
||||
response = graph.invoke({"messages": [input_message]}, config)
|
||||
response = graph.invoke({"messages": [input_message]}, context={"model_provider": "openai", "system_message": "Respond in Italian."})
|
||||
for message in response["messages"]:
|
||||
message.pretty_print()
|
||||
```
|
||||
@@ -1152,12 +1152,13 @@ LangGraph supports map-reduce and other advanced branching patterns using the Se
|
||||
```python
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
from langgraph.types import Send
|
||||
from typing_extensions import TypedDict
|
||||
from typing_extensions import TypedDict, Annotated
|
||||
import operator
|
||||
|
||||
class OverallState(TypedDict):
|
||||
topic: str
|
||||
subjects: list[str]
|
||||
jokes: list[str]
|
||||
jokes: Annotated[list[str], operator.add]
|
||||
best_selected_joke: str
|
||||
|
||||
def generate_topics(state: OverallState):
|
||||
@@ -1566,9 +1567,9 @@ class State(TypedDict):
|
||||
|
||||
def node_a(state: State) -> Command[Literal["node_b", "node_c"]]:
|
||||
print("Called A")
|
||||
value = random.choice(["a", "b"])
|
||||
value = random.choice(["b", "c"])
|
||||
# this is a replacement for a conditional edge function
|
||||
if value == "a":
|
||||
if value == "b":
|
||||
goto = "node_b"
|
||||
else:
|
||||
goto = "node_c"
|
||||
|
||||
@@ -54,13 +54,7 @@ graph = graph_builder.compile(checkpointer=checkpointer) # (4)!
|
||||
config = {"configurable": {"thread_id": "some_id"}}
|
||||
result = graph.invoke({"some_text": "original text"}, config=config) # (5)!
|
||||
print(result['__interrupt__']) # (6)!
|
||||
# > [
|
||||
# > Interrupt(
|
||||
# > value={'text_to_revise': 'original text'},
|
||||
# > resumable=True,
|
||||
# > ns=['human_node:6ce9e64f-edef-fe5d-f7dc-511fa9526960']
|
||||
# > )
|
||||
# > ]
|
||||
# > [Interrupt(value={'text_to_revise': 'original text'}, id='a0d9dd40440ac7be2720dc5c20858627')]
|
||||
|
||||
# highlight-next-line
|
||||
print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)!
|
||||
@@ -80,25 +74,27 @@ print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)!
|
||||
```python
|
||||
from typing import TypedDict
|
||||
import uuid
|
||||
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.constants import START
|
||||
from langgraph.graph import StateGraph
|
||||
|
||||
# highlight-next-line
|
||||
from langgraph.types import interrupt, Command
|
||||
|
||||
|
||||
class State(TypedDict):
|
||||
some_text: str
|
||||
|
||||
|
||||
def human_node(state: State):
|
||||
# highlight-next-line
|
||||
value = interrupt( # (1)!
|
||||
value = interrupt( # (1)!
|
||||
{
|
||||
"text_to_revise": state["some_text"] # (2)!
|
||||
"text_to_revise": state["some_text"] # (2)!
|
||||
}
|
||||
)
|
||||
return {
|
||||
"some_text": value # (3)!
|
||||
"some_text": value # (3)!
|
||||
}
|
||||
|
||||
|
||||
@@ -106,25 +102,15 @@ print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)!
|
||||
graph_builder = StateGraph(State)
|
||||
graph_builder.add_node("human_node", human_node)
|
||||
graph_builder.add_edge(START, "human_node")
|
||||
|
||||
checkpointer = InMemorySaver() # (4)!
|
||||
|
||||
checkpointer = InMemorySaver() # (4)!
|
||||
graph = graph_builder.compile(checkpointer=checkpointer)
|
||||
|
||||
# Pass a thread ID to the graph to run it.
|
||||
config = {"configurable": {"thread_id": uuid.uuid4()}}
|
||||
|
||||
# Run the graph until the interrupt is hit.
|
||||
result = graph.invoke({"some_text": "original text"}, config=config) # (5)!
|
||||
result = graph.invoke({"some_text": "original text"}, config=config) # (5)!
|
||||
|
||||
print(result['__interrupt__']) # (6)!
|
||||
# > [
|
||||
# > Interrupt(
|
||||
# > value={'text_to_revise': 'original text'},
|
||||
# > resumable=True,
|
||||
# > ns=['human_node:6ce9e64f-edef-fe5d-f7dc-511fa9526960']
|
||||
# > )
|
||||
# > ]
|
||||
print(result["__interrupt__"]) # (6)!
|
||||
# > [Interrupt(value={'text_to_revise': 'original text'}, id='6d7c4048049254c83195429a3659661d')]
|
||||
|
||||
# highlight-next-line
|
||||
print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)!
|
||||
@@ -142,7 +128,7 @@ print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)!
|
||||
|
||||
!!! tip "New in 0.4.0"
|
||||
|
||||
`__interrupt__` is a special key that will be returned when running the graph if the graph is interrupted. Support for `__interrupt__` in `invoke` and `ainvoke` has been added in version 0.4.0. If you're on an older version, you will only see `__interrupt__` in the result if you use `stream` or `astream`. You can also use `graph.get_state(thread_id)` to get the interrupt value.
|
||||
`__interrupt__` is a special key that will be returned when running the graph if the graph is interrupted. Support for `__interrupt__` in `invoke` and `ainvoke` has been added in version 0.4.0. If you're on an older version, you will only see `__interrupt__` in the result if you use `stream` or `astream`. You can also use `graph.get_state(thread_id)` to get the interrupt value(s).
|
||||
|
||||
!!! warning
|
||||
|
||||
@@ -159,19 +145,67 @@ To resume execution, use the [`Command`][langgraph.types.Command] primitive, whi
|
||||
graph.invoke(Command(resume={"age": "25"}), thread_config)
|
||||
```
|
||||
|
||||
### Resume multiple interrupts with one invocation
|
||||
## Resuming Multiple interrupts
|
||||
|
||||
If you have multiple interrupts in the task queue, you can use `Command.resume` with a dictionary mapping of interrupt ids to resume with a single `invoke` / `stream` call.
|
||||
When nodes with interrupt conditions are run in parallel, it's possible to have multiple interrupts in the task queue.
|
||||
For example, the following graph has two nodes run in parallel that require human input:
|
||||
|
||||
<figure markdown="1">
|
||||
{: style="max-height:400px"}
|
||||
</figure>
|
||||
|
||||
Once your graph has been interrupted and is stalled, you can resume all the interrupts at once with `Command.resume`, passing a dictionary mapping of interrupt ids to resume values.
|
||||
|
||||
For example, once your graph has been interrupted (multiple times, theoretically) and is stalled:
|
||||
|
||||
```python
|
||||
resume_map = {
|
||||
i.interrupt_id: f"human input for prompt {i.value}"
|
||||
for i in parent.get_state(thread_config).interrupts
|
||||
}
|
||||
from typing import TypedDict
|
||||
import uuid
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.constants import START
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.types import interrupt, Command
|
||||
|
||||
parent_graph.invoke(Command(resume=resume_map), config=thread_config)
|
||||
|
||||
class State(TypedDict):
|
||||
text_1: str
|
||||
text_2: str
|
||||
|
||||
|
||||
def human_node_1(state: State):
|
||||
value = interrupt({"text_to_revise": state["text_1"]})
|
||||
return {"text_1": value}
|
||||
|
||||
|
||||
def human_node_2(state: State):
|
||||
value = interrupt({"text_to_revise": state["text_2"]})
|
||||
return {"text_2": value}
|
||||
|
||||
|
||||
graph_builder = StateGraph(State)
|
||||
graph_builder.add_node("human_node_1", human_node_1)
|
||||
graph_builder.add_node("human_node_2", human_node_2)
|
||||
|
||||
# Add both nodes in parallel from START
|
||||
graph_builder.add_edge(START, "human_node_1")
|
||||
graph_builder.add_edge(START, "human_node_2")
|
||||
|
||||
checkpointer = InMemorySaver()
|
||||
graph = graph_builder.compile(checkpointer=checkpointer)
|
||||
|
||||
thread_id = str(uuid.uuid4())
|
||||
config: RunnableConfig = {"configurable": {"thread_id": thread_id}}
|
||||
result = graph.invoke(
|
||||
{"text_1": "original text 1", "text_2": "original text 2"}, config=config
|
||||
)
|
||||
|
||||
# Resume with mapping of interrupt IDs to values
|
||||
resume_map = {
|
||||
i.id: f"edited text for {i.value['text_to_revise']}"
|
||||
for i in result["__interrupt__"]
|
||||
}
|
||||
print(graph.invoke(Command(resume=resume_map), config=config))
|
||||
# > {'text_1': 'edited text for original text 1', 'text_2': 'edited text for original text 2'}
|
||||
```
|
||||
|
||||
## Common patterns
|
||||
@@ -226,7 +260,7 @@ graph.invoke(Command(resume=True), config=thread_config)
|
||||
from langgraph.constants import START, END
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.types import interrupt, Command
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
# Define the shared graph state
|
||||
class State(TypedDict):
|
||||
@@ -271,7 +305,7 @@ graph.invoke(Command(resume=True), config=thread_config)
|
||||
builder.add_edge("approved_path", END)
|
||||
builder.add_edge("rejected_path", END)
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
|
||||
# Run until interrupt
|
||||
@@ -339,7 +373,7 @@ graph.invoke(
|
||||
from langgraph.constants import START, END
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.types import interrupt, Command
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
# Define the graph state
|
||||
class State(TypedDict):
|
||||
@@ -378,7 +412,7 @@ graph.invoke(
|
||||
builder.add_edge("downstream_use", END)
|
||||
|
||||
# Set up in-memory checkpointing for interrupt support
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
|
||||
# Invoke the graph until it hits the interrupt
|
||||
@@ -388,14 +422,15 @@ graph.invoke(
|
||||
# Output interrupt payload
|
||||
print(result["__interrupt__"])
|
||||
# Example output:
|
||||
# Interrupt(
|
||||
# value={
|
||||
# 'task': 'Please review and edit the generated summary if necessary.',
|
||||
# 'generated_summary': 'The cat sat on the mat and looked at the stars.'
|
||||
# },
|
||||
# resumable=True,
|
||||
# ...
|
||||
# )
|
||||
# > [
|
||||
# > Interrupt(
|
||||
# > value={
|
||||
# > 'task': 'Please review and edit the generated summary if necessary.',
|
||||
# > 'generated_summary': 'The cat sat on the mat and looked at the stars.'
|
||||
# > },
|
||||
# > id='...'
|
||||
# > )
|
||||
# > ]
|
||||
|
||||
# Resume the graph with human-edited input
|
||||
edited_summary = "The cat lay on the rug, gazing peacefully at the night sky."
|
||||
@@ -655,7 +690,7 @@ def human_node(state: State):
|
||||
from langgraph.constants import START, END
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.types import interrupt, Command
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
# Define graph state
|
||||
class State(TypedDict):
|
||||
@@ -694,7 +729,7 @@ def human_node(state: State):
|
||||
builder.add_edge("report_age", END)
|
||||
|
||||
# Create the graph with a memory checkpointer
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
|
||||
# Run the graph until the first interrupt
|
||||
@@ -951,7 +986,7 @@ def node_in_parent_graph(state: State):
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.constants import START
|
||||
from langgraph.types import interrupt, Command
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
|
||||
class State(TypedDict):
|
||||
@@ -977,7 +1012,7 @@ def node_in_parent_graph(state: State):
|
||||
print(f"Got an answer of {answer}")
|
||||
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
|
||||
subgraph_builder = StateGraph(State)
|
||||
subgraph_builder.add_node("some_node", node_in_subgraph)
|
||||
@@ -1008,7 +1043,7 @@ def node_in_parent_graph(state: State):
|
||||
builder.add_edge(START, "parent_node")
|
||||
|
||||
# A checkpointer must be enabled for interrupts to work!
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
|
||||
config = {
|
||||
@@ -1032,7 +1067,7 @@ def node_in_parent_graph(state: State):
|
||||
Entered `parent_node` a total of 1 times
|
||||
Entered `node_in_subgraph` a total of 1 times
|
||||
Entered human_node in sub-graph a total of 1 times
|
||||
{'__interrupt__': (Interrupt(value='what is your name?', resumable=True, ns=['parent_node:4c3a0248-21f0-1287-eacf-3002bc304db4', 'human_node:2fe86d52-6f70-2a3f-6b2f-b1eededd6348'], when='during'),)}
|
||||
{'__interrupt__': (Interrupt(value='what is your name?', id='...'),)}
|
||||
--- Resuming ---
|
||||
Entered `parent_node` a total of 2 times
|
||||
Entered human_node in sub-graph a total of 2 times
|
||||
@@ -1040,7 +1075,7 @@ def node_in_parent_graph(state: State):
|
||||
{'parent_node': {'state_counter': 1}}
|
||||
```
|
||||
|
||||
### Using multiple interrupts
|
||||
### Using multiple interrupts in a single node
|
||||
|
||||
Using multiple interrupts within a **single** node can be helpful for patterns like [validating human input](#validate-human-input). However, using multiple interrupts in the same node can lead to unexpected behavior if not handled carefully.
|
||||
|
||||
@@ -1057,7 +1092,7 @@ To avoid issues, refrain from dynamically changing the node's structure between
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.constants import START
|
||||
from langgraph.types import interrupt, Command
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
|
||||
class State(TypedDict):
|
||||
@@ -1091,7 +1126,7 @@ To avoid issues, refrain from dynamically changing the node's structure between
|
||||
builder.add_edge(START, "human_node")
|
||||
|
||||
# A checkpointer must be enabled for interrupts to work!
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
|
||||
config = {
|
||||
@@ -1108,7 +1143,7 @@ To avoid issues, refrain from dynamically changing the node's structure between
|
||||
```
|
||||
|
||||
```pycon
|
||||
{'__interrupt__': (Interrupt(value='what is your name?', resumable=True, ns=['human_node:3a007ef9-c30d-c357-1ec1-86a1a70d8fba'], when='during'),)}
|
||||
{'__interrupt__': (Interrupt(value='what is your name?', id='...'),)}
|
||||
Name: N/A. Age: John
|
||||
{'human_node': {'age': 'John', 'name': 'N/A'}}
|
||||
```
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"\n",
|
||||
"# highlight-next-line\n",
|
||||
"from langgraph.types import Command, interrupt\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
"from IPython.display import Image, display\n",
|
||||
"\n",
|
||||
"\n",
|
||||
@@ -157,7 +157,7 @@
|
||||
"builder.add_edge(\"step_3\", END)\n",
|
||||
"\n",
|
||||
"# Set up memory\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"memory = InMemorySaver()\n",
|
||||
"\n",
|
||||
"# Add\n",
|
||||
"graph = builder.compile(checkpointer=memory)\n",
|
||||
@@ -435,9 +435,9 @@
|
||||
"workflow.add_edge(\"ask_human\", \"agent\")\n",
|
||||
"\n",
|
||||
"# Set up memory\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
"\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"memory = InMemorySaver()\n",
|
||||
"\n",
|
||||
"# Finally, we compile it!\n",
|
||||
"# This compiles it into a LangChain Runnable,\n",
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
"from langgraph.prebuilt import create_react_agent\n",
|
||||
"from langgraph.graph import add_messages\n",
|
||||
"from langgraph.func import entrypoint, task\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
"from langgraph.types import interrupt, Command\n",
|
||||
"\n",
|
||||
"model = ChatAnthropic(model=\"claude-3-5-sonnet-latest\")\n",
|
||||
@@ -272,7 +272,7 @@
|
||||
" return response[\"messages\"]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"checkpointer = MemorySaver()\n",
|
||||
"checkpointer = InMemorySaver()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def string_to_uuid(input_string):\n",
|
||||
|
||||
@@ -375,7 +375,7 @@ def agent(state) -> Command[Literal["agent", "another_agent", "human"]]:
|
||||
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
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
|
||||
model = ChatAnthropic(model="claude-3-5-sonnet-latest")
|
||||
@@ -467,7 +467,7 @@ def agent(state) -> Command[Literal["agent", "another_agent", "human"]]:
|
||||
builder.add_edge(START, "travel_advisor")
|
||||
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
```
|
||||
|
||||
|
||||
@@ -28,9 +28,9 @@
|
||||
"1. Create an instance of a checkpointer:\n",
|
||||
"\n",
|
||||
" ```python\n",
|
||||
" from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
" from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
" \n",
|
||||
" checkpointer = MemorySaver() \n",
|
||||
" checkpointer = InMemorySaver() \n",
|
||||
" ```\n",
|
||||
"\n",
|
||||
"2. Pass `checkpointer` instance to the `entrypoint()` decorator:\n",
|
||||
@@ -184,7 +184,7 @@
|
||||
"from langchain_core.messages import BaseMessage\n",
|
||||
"from langgraph.graph import add_messages\n",
|
||||
"from langgraph.func import entrypoint, task\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@task\n",
|
||||
@@ -193,7 +193,7 @@
|
||||
" return response\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"checkpointer = MemorySaver()\n",
|
||||
"checkpointer = InMemorySaver()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@entrypoint(checkpointer=checkpointer)\n",
|
||||
|
||||
@@ -261,7 +261,7 @@
|
||||
"\n",
|
||||
"To add thread-level persistence to our agent:\n",
|
||||
"\n",
|
||||
"1. Select a [checkpointer](../../concepts/persistence#checkpointer-libraries): here we will use [MemorySaver](../../reference/checkpoints/#langgraph.checkpoint.memory.MemorySaver), a simple in-memory checkpointer.\n",
|
||||
"1. Select a [checkpointer](../../concepts/persistence#checkpointer-libraries): here we will use [InMemorySaver](../../reference/checkpoints/#langgraph.checkpoint.memory.InMemorySaver), a simple in-memory checkpointer.\n",
|
||||
"2. Update our entrypoint to accept the previous messages state as a second argument. Here, we simply append the message updates to the previous sequence of messages.\n",
|
||||
"3. Choose which values will be returned from the workflow and which will be saved by the checkpointer as `previous` using `entrypoint.final` (optional)"
|
||||
]
|
||||
@@ -272,10 +272,10 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
"\n",
|
||||
"# highlight-next-line\n",
|
||||
"checkpointer = MemorySaver()\n",
|
||||
"checkpointer = InMemorySaver()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# highlight-next-line\n",
|
||||
|
||||
@@ -26,7 +26,7 @@ my_workflow.invoke({"value": 1, "another_value": 2})
|
||||
```python
|
||||
import uuid
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
# Task that checks if a number is even
|
||||
@task
|
||||
@@ -39,7 +39,7 @@ my_workflow.invoke({"value": 1, "another_value": 2})
|
||||
return "The number is even." if is_even else "The number is odd."
|
||||
|
||||
# Create a checkpointer for persistence
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def workflow(inputs: dict) -> str:
|
||||
@@ -63,7 +63,7 @@ my_workflow.invoke({"value": 1, "another_value": 2})
|
||||
import uuid
|
||||
from langchain.chat_models import init_chat_model
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
llm = init_chat_model('openai:gpt-3.5-turbo')
|
||||
|
||||
@@ -77,7 +77,7 @@ my_workflow.invoke({"value": 1, "another_value": 2})
|
||||
]).content
|
||||
|
||||
# Create a checkpointer for persistence
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def workflow(topic: str) -> str:
|
||||
@@ -114,7 +114,7 @@ def graph(numbers: list[int]) -> list[str]:
|
||||
import uuid
|
||||
from langchain.chat_models import init_chat_model
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
# Initialize the LLM model
|
||||
llm = init_chat_model("openai:gpt-3.5-turbo")
|
||||
@@ -129,7 +129,7 @@ def graph(numbers: list[int]) -> list[str]:
|
||||
return response.content
|
||||
|
||||
# Create a checkpointer for persistence
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def workflow(topics: list[str]) -> str:
|
||||
@@ -176,7 +176,7 @@ def some_workflow(some_input: dict) -> int:
|
||||
import uuid
|
||||
from typing import TypedDict
|
||||
from langgraph.func import entrypoint
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.graph import StateGraph
|
||||
|
||||
# Define the shared state type
|
||||
@@ -194,7 +194,7 @@ def some_workflow(some_input: dict) -> int:
|
||||
graph = builder.compile()
|
||||
|
||||
# Define the functional API workflow
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def workflow(x: int) -> dict:
|
||||
@@ -227,10 +227,10 @@ def my_workflow(inputs: dict) -> int:
|
||||
```python
|
||||
import uuid
|
||||
from langgraph.func import entrypoint
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
# Initialize a checkpointer
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
|
||||
# A reusable sub-workflow that multiplies a number
|
||||
@entrypoint()
|
||||
@@ -258,10 +258,10 @@ Example of using the streaming API to stream both updates and custom data.
|
||||
|
||||
```python
|
||||
from langgraph.func import entrypoint
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.config import get_stream_writer # (1)!
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def main(inputs: dict) -> int:
|
||||
@@ -316,7 +316,7 @@ for mode, chunk in main.stream( # (5)!
|
||||
## Retry policy
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import RetryPolicy
|
||||
|
||||
@@ -337,7 +337,7 @@ def get_info():
|
||||
raise ValueError('Failure')
|
||||
return "OK"
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def main(inputs, writer):
|
||||
@@ -392,7 +392,7 @@ for chunk in main.stream({"x": 5}, stream_mode="updates"):
|
||||
|
||||
```python
|
||||
import time
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.types import StreamWriter
|
||||
|
||||
@@ -414,7 +414,7 @@ def get_info():
|
||||
return "OK"
|
||||
|
||||
# Initialize an in-memory checkpointer for persistence
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
|
||||
@task
|
||||
def slow_task():
|
||||
@@ -504,9 +504,9 @@ def step_3(input_query):
|
||||
We can now compose these tasks in an [entrypoint](../concepts/functional_api.md#entrypoint):
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
@@ -577,12 +577,12 @@ def review_tool_call(tool_call: ToolCall) -> Union[ToolCall, ToolMessage]:
|
||||
We can now update our [entrypoint](../concepts/functional_api.md#entrypoint) to review the generated tool calls. If a tool call is accepted or revised, we execute in the same way as before. Otherwise, we just append the `ToolMessage` supplied by the human. The results of prior tasks — in this case the initial model call — are persisted, so that they are not run again following the `interrupt`.
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.graph.message import add_messages
|
||||
from langgraph.types import Command, interrupt
|
||||
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
@@ -757,9 +757,9 @@ Use `entrypoint.final` to decouple what is returned to the caller from what is p
|
||||
```python
|
||||
from typing import Optional
|
||||
from langgraph.func import entrypoint
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def accumulate(n: int, *, previous: Optional[int]) -> entrypoint.final[int, int]:
|
||||
@@ -777,14 +777,14 @@ print(accumulate.invoke(3, config=config)) # 3
|
||||
|
||||
### Chatbot example
|
||||
|
||||
An example of a simple chatbot using the functional API and the `MemorySaver` checkpointer.
|
||||
An example of a simple chatbot using the functional API and the `InMemorySaver` checkpointer.
|
||||
The bot is able to remember the previous conversation and continue from where it left off.
|
||||
|
||||
```python
|
||||
from langchain_core.messages import BaseMessage
|
||||
from langgraph.graph import add_messages
|
||||
from langgraph.func import entrypoint, task
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
|
||||
model = ChatAnthropic(model="claude-3-5-sonnet-latest")
|
||||
@@ -794,7 +794,7 @@ def call_model(messages: list[BaseMessage]):
|
||||
response = model.invoke(messages)
|
||||
return response
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
|
||||
@entrypoint(checkpointer=checkpointer)
|
||||
def workflow(inputs: list[BaseMessage], *, previous: list[BaseMessage]):
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
options:
|
||||
members:
|
||||
- TAG_HIDDEN
|
||||
- TAG_NOSTREAM
|
||||
- START
|
||||
- END
|
||||
- END
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Runtime
|
||||
|
||||
::: langgraph.runtime.Runtime
|
||||
options:
|
||||
show_root_heading: true
|
||||
show_root_full_path: false
|
||||
members:
|
||||
- context
|
||||
- store
|
||||
- stream_writer
|
||||
- previous
|
||||
|
||||
::: langgraph.runtime
|
||||
options:
|
||||
members:
|
||||
- get_runtime
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
"from langgraph.graph import StateGraph, START\n",
|
||||
"from langgraph.graph.message import add_messages\n",
|
||||
"from typing import Annotated\n",
|
||||
@@ -267,7 +267,7 @@
|
||||
" messages: Annotated[list, add_messages]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"memory = InMemorySaver()\n",
|
||||
"workflow = StateGraph(State)\n",
|
||||
"workflow.add_node(\"info\", info_chain)\n",
|
||||
"workflow.add_node(\"prompt\", prompt_gen_chain)\n",
|
||||
|
||||
@@ -1124,7 +1124,7 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
"from langgraph.graph import END, StateGraph, START\n",
|
||||
"from langgraph.prebuilt import tools_condition\n",
|
||||
"\n",
|
||||
@@ -1144,7 +1144,7 @@
|
||||
"\n",
|
||||
"# The checkpointer lets the graph persist its state\n",
|
||||
"# this is a complete memory for the entire graph.\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"memory = InMemorySaver()\n",
|
||||
"part_1_graph = builder.compile(checkpointer=memory)"
|
||||
]
|
||||
},
|
||||
@@ -1943,7 +1943,7 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
"from langgraph.graph import StateGraph\n",
|
||||
"from langgraph.prebuilt import tools_condition\n",
|
||||
"\n",
|
||||
@@ -1967,7 +1967,7 @@
|
||||
")\n",
|
||||
"builder.add_edge(\"tools\", \"assistant\")\n",
|
||||
"\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"memory = InMemorySaver()\n",
|
||||
"part_2_graph = builder.compile(\n",
|
||||
" checkpointer=memory,\n",
|
||||
" # NEW: The graph will always halt before executing the \"tools\" node.\n",
|
||||
@@ -2532,7 +2532,7 @@
|
||||
"source": [
|
||||
"from typing import Literal\n",
|
||||
"\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
"from langgraph.graph import StateGraph\n",
|
||||
"from langgraph.prebuilt import tools_condition\n",
|
||||
"\n",
|
||||
@@ -2576,7 +2576,7 @@
|
||||
"builder.add_edge(\"safe_tools\", \"assistant\")\n",
|
||||
"builder.add_edge(\"sensitive_tools\", \"assistant\")\n",
|
||||
"\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"memory = InMemorySaver()\n",
|
||||
"part_3_graph = builder.compile(\n",
|
||||
" checkpointer=memory,\n",
|
||||
" # NEW: The graph will always halt before executing the \"tools\" node.\n",
|
||||
@@ -3477,7 +3477,7 @@
|
||||
"source": [
|
||||
"from typing import Literal\n",
|
||||
"\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
"from langgraph.graph import StateGraph\n",
|
||||
"from langgraph.prebuilt import tools_condition\n",
|
||||
"\n",
|
||||
@@ -3841,7 +3841,7 @@
|
||||
"builder.add_conditional_edges(\"fetch_user_info\", route_to_workflow)\n",
|
||||
"\n",
|
||||
"# Compile graph\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"memory = InMemorySaver()\n",
|
||||
"part_4_graph = builder.compile(\n",
|
||||
" checkpointer=memory,\n",
|
||||
" # Let the user approve or deny the use of sensitive tools\n",
|
||||
|
||||
@@ -10,14 +10,14 @@ We will see later that **checkpointing** is _much_ more powerful than simple cha
|
||||
|
||||
This tutorial builds on [Add tools](./2-add-tools.md).
|
||||
|
||||
## 1. Create a `MemorySaver` checkpointer
|
||||
## 1. Create a `InMemorySaver` checkpointer
|
||||
|
||||
Create a `MemorySaver` checkpointer:
|
||||
Create a `InMemorySaver` checkpointer:
|
||||
|
||||
``` python
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
memory = MemorySaver()
|
||||
memory = InMemorySaver()
|
||||
```
|
||||
|
||||
This is in-memory checkpointer, which is convenient for the tutorial. However, in a production application, you would likely change this to use `SqliteSaver` or `PostgresSaver` and connect a database.
|
||||
@@ -172,7 +172,7 @@ from langchain_tavily import TavilySearch
|
||||
from langchain_core.messages import BaseMessage
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.graph.message import add_messages
|
||||
from langgraph.prebuilt import ToolNode, tools_condition
|
||||
@@ -200,7 +200,7 @@ graph_builder.add_conditional_edges(
|
||||
)
|
||||
graph_builder.add_edge("tools", "chatbot")
|
||||
graph_builder.set_entry_point("chatbot")
|
||||
memory = MemorySaver()
|
||||
memory = InMemorySaver()
|
||||
graph = graph_builder.compile(checkpointer=memory)
|
||||
```
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ from langchain_tavily import TavilySearch
|
||||
from langchain_core.tools import tool
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
from langgraph.graph.message import add_messages
|
||||
from langgraph.prebuilt import ToolNode, tools_condition
|
||||
@@ -85,7 +85,7 @@ graph_builder.add_edge(START, "chatbot")
|
||||
We compile the graph with a checkpointer, as before:
|
||||
|
||||
```python
|
||||
memory = MemorySaver()
|
||||
memory = InMemorySaver()
|
||||
|
||||
graph = graph_builder.compile(checkpointer=memory)
|
||||
```
|
||||
@@ -230,7 +230,7 @@ from langchain_tavily import TavilySearch
|
||||
from langchain_core.tools import tool
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
from langgraph.graph.message import add_messages
|
||||
from langgraph.prebuilt import ToolNode, tools_condition
|
||||
@@ -268,7 +268,7 @@ graph_builder.add_conditional_edges(
|
||||
graph_builder.add_edge("tools", "chatbot")
|
||||
graph_builder.add_edge(START, "chatbot")
|
||||
|
||||
memory = MemorySaver()
|
||||
memory = InMemorySaver()
|
||||
graph = graph_builder.compile(checkpointer=memory)
|
||||
```
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@ from langchain_core.messages import ToolMessage
|
||||
from langchain_core.tools import InjectedToolCallId, tool
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
from langgraph.graph.message import add_messages
|
||||
from langgraph.prebuilt import ToolNode, tools_condition
|
||||
@@ -301,7 +301,7 @@ graph_builder.add_conditional_edges(
|
||||
graph_builder.add_edge("tools", "chatbot")
|
||||
graph_builder.add_edge(START, "chatbot")
|
||||
|
||||
memory = MemorySaver()
|
||||
memory = InMemorySaver()
|
||||
graph = graph_builder.compile(checkpointer=memory)
|
||||
```
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ from langchain_tavily import TavilySearch
|
||||
from langchain_core.messages import BaseMessage
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
from langgraph.graph.message import add_messages
|
||||
from langgraph.prebuilt import ToolNode, tools_condition
|
||||
@@ -60,7 +60,7 @@ graph_builder.add_conditional_edges(
|
||||
graph_builder.add_edge("tools", "chatbot")
|
||||
graph_builder.add_edge(START, "chatbot")
|
||||
|
||||
memory = MemorySaver()
|
||||
memory = InMemorySaver()
|
||||
graph = graph_builder.compile(checkpointer=memory)
|
||||
```
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ Before you begin, ensure you have the following:
|
||||
|
||||
=== "Python server"
|
||||
|
||||
```shell
|
||||
# Python >= 3.11 is required.
|
||||
Python >= 3.11 is required.
|
||||
|
||||
```shell
|
||||
pip install --upgrade "langgraph-cli[inmem]"
|
||||
```
|
||||
|
||||
|
||||
@@ -322,7 +322,7 @@
|
||||
"from typing import Annotated, List, Sequence\n",
|
||||
"from langgraph.graph import END, StateGraph, START\n",
|
||||
"from langgraph.graph.message import add_messages\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
"from typing_extensions import TypedDict\n",
|
||||
"\n",
|
||||
"\n",
|
||||
@@ -361,7 +361,7 @@
|
||||
"\n",
|
||||
"builder.add_conditional_edges(\"generate\", should_continue)\n",
|
||||
"builder.add_edge(\"reflect\", \"generate\")\n",
|
||||
"memory = MemorySaver()\n",
|
||||
"memory = InMemorySaver()\n",
|
||||
"graph = builder.compile(checkpointer=memory)"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -272,7 +272,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -280,10 +280,10 @@
|
||||
"from typing import Optional, Dict, Any\n",
|
||||
"from typing_extensions import Annotated, TypedDict\n",
|
||||
"from langgraph.graph import StateGraph\n",
|
||||
"from langgraph.runtime import Runtime\n",
|
||||
"\n",
|
||||
"from langchain_core.runnables import RunnableConfig\n",
|
||||
"from langgraph.constants import Send\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
"from langgraph.types import Send\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def update_candidates(\n",
|
||||
@@ -307,22 +307,27 @@
|
||||
" depth: Annotated[int, operator.add]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class Configuration(TypedDict, total=False):\n",
|
||||
"class Context(TypedDict, total=False):\n",
|
||||
" max_depth: int\n",
|
||||
" threshold: float\n",
|
||||
" k: int\n",
|
||||
" beam_size: int\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _ensure_configurable(config: RunnableConfig) -> Configuration:\n",
|
||||
"class EnsuredContext(TypedDict):\n",
|
||||
" max_depth: int\n",
|
||||
" threshold: float\n",
|
||||
" k: int\n",
|
||||
" beam_size: int\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _ensure_context(ctx: Context) -> EnsuredContext:\n",
|
||||
" \"\"\"Get params that configure the search algorithm.\"\"\"\n",
|
||||
" configurable = config.get(\"configurable\", {})\n",
|
||||
" return {\n",
|
||||
" **configurable,\n",
|
||||
" \"max_depth\": configurable.get(\"max_depth\", 10),\n",
|
||||
" \"threshold\": config.get(\"threshold\", 0.9),\n",
|
||||
" \"k\": configurable.get(\"k\", 5),\n",
|
||||
" \"beam_size\": configurable.get(\"beam_size\", 3),\n",
|
||||
" \"max_depth\": ctx.get(\"max_depth\", 10),\n",
|
||||
" \"threshold\": ctx.get(\"threshold\", 0.9),\n",
|
||||
" \"k\": ctx.get(\"k\", 5),\n",
|
||||
" \"beam_size\": ctx.get(\"beam_size\", 3),\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
"\n",
|
||||
@@ -330,9 +335,11 @@
|
||||
" seed: Optional[Candidate]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def expand(state: ExpansionState, *, config: RunnableConfig) -> Dict[str, List[str]]:\n",
|
||||
"def expand(\n",
|
||||
" state: ExpansionState, *, runtime: Runtime[Context]\n",
|
||||
") -> Dict[str, List[Candidate]]:\n",
|
||||
" \"\"\"Generate the next state.\"\"\"\n",
|
||||
" configurable = _ensure_configurable(config)\n",
|
||||
" ctx = _ensure_context(runtime.context)\n",
|
||||
" if not state.get(\"seed\"):\n",
|
||||
" candidate_str = \"\"\n",
|
||||
" else:\n",
|
||||
@@ -342,9 +349,8 @@
|
||||
" {\n",
|
||||
" \"problem\": state[\"problem\"],\n",
|
||||
" \"candidate\": candidate_str,\n",
|
||||
" \"k\": configurable[\"k\"],\n",
|
||||
" \"k\": ctx[\"k\"],\n",
|
||||
" },\n",
|
||||
" config=config,\n",
|
||||
" )\n",
|
||||
" except Exception:\n",
|
||||
" return {\"candidates\": []}\n",
|
||||
@@ -354,7 +360,7 @@
|
||||
" return {\"candidates\": new_candidates}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def score(state: ToTState) -> Dict[str, List[float]]:\n",
|
||||
"def score(state: ToTState) -> Dict[str, Any]:\n",
|
||||
" \"\"\"Evaluate the candidate generations.\"\"\"\n",
|
||||
" candidates = state[\"candidates\"]\n",
|
||||
" scored = []\n",
|
||||
@@ -363,11 +369,9 @@
|
||||
" return {\"scored_candidates\": scored, \"candidates\": \"clear\"}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def prune(\n",
|
||||
" state: ToTState, *, config: RunnableConfig\n",
|
||||
") -> Dict[str, List[Dict[str, Any]]]:\n",
|
||||
"def prune(state: ToTState, *, runtime: Runtime[Context]) -> Dict[str, Any]:\n",
|
||||
" scored_candidates = state[\"scored_candidates\"]\n",
|
||||
" beam_size = _ensure_configurable(config)[\"beam_size\"]\n",
|
||||
" beam_size = _ensure_context(runtime.context)[\"beam_size\"]\n",
|
||||
" organized = sorted(\n",
|
||||
" scored_candidates, key=lambda candidate: candidate[1], reverse=True\n",
|
||||
" )\n",
|
||||
@@ -383,11 +387,11 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"def should_terminate(\n",
|
||||
" state: ToTState, config: RunnableConfig\n",
|
||||
" state: ToTState, runtime: Runtime[Context]\n",
|
||||
") -> Union[Literal[\"__end__\"], Send]:\n",
|
||||
" configurable = _ensure_configurable(config)\n",
|
||||
" solved = state[\"candidates\"][0].score >= configurable[\"threshold\"]\n",
|
||||
" if solved or state[\"depth\"] >= configurable[\"max_depth\"]:\n",
|
||||
" ctx = _ensure_context(runtime.context)\n",
|
||||
" solved = state[\"candidates\"][0].score >= ctx[\"threshold\"]\n",
|
||||
" if solved or state[\"depth\"] >= ctx[\"max_depth\"]:\n",
|
||||
" return \"__end__\"\n",
|
||||
" return [\n",
|
||||
" Send(\"expand\", {**state, \"somevalseed\": candidate})\n",
|
||||
@@ -396,7 +400,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"# Create the graph\n",
|
||||
"builder = StateGraph(state_schema=ToTState, config_schema=Configuration)\n",
|
||||
"builder = StateGraph(state_schema=ToTState, context_schema=Context)\n",
|
||||
"\n",
|
||||
"# Add nodes\n",
|
||||
"builder.add_node(expand)\n",
|
||||
@@ -412,7 +416,7 @@
|
||||
"builder.add_edge(\"__start__\", \"expand\")\n",
|
||||
"\n",
|
||||
"# Compile the graph\n",
|
||||
"graph = builder.compile(checkpointer=MemorySaver())"
|
||||
"graph = builder.compile(checkpointer=InMemorySaver())"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -467,13 +471,11 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"config = {\n",
|
||||
" \"configurable\": {\n",
|
||||
" \"thread_id\": \"test_1\",\n",
|
||||
" \"depth\": 10,\n",
|
||||
" }\n",
|
||||
"}\n",
|
||||
"for step in graph.stream({\"problem\": puzzles[42]}, config):\n",
|
||||
"for step in graph.stream(\n",
|
||||
" {\"problem\": puzzles[42]},\n",
|
||||
" config={\"configurable\": {\"thread_id\": \"test_1\"}},\n",
|
||||
" context={\"depth\": 10},\n",
|
||||
"):\n",
|
||||
" print(step)"
|
||||
]
|
||||
},
|
||||
@@ -491,7 +493,7 @@
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"final_state = graph.get_state(config)\n",
|
||||
"final_state = graph.get_state({\"configurable\": {\"thread_id\": \"test_1\"}})\n",
|
||||
"winning_solution = final_state.values[\"candidates\"][0]\n",
|
||||
"search_depth = final_state.values[\"depth\"]\n",
|
||||
"if winning_solution[1] == 1:\n",
|
||||
|
||||
@@ -1029,7 +1029,7 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
"from langgraph.graph import END, StateGraph, START\n",
|
||||
"\n",
|
||||
"builder = StateGraph(State)\n",
|
||||
@@ -1053,7 +1053,7 @@
|
||||
"builder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"checkpointer = MemorySaver()\n",
|
||||
"checkpointer = InMemorySaver()\n",
|
||||
"graph = builder.compile(checkpointer=checkpointer)"
|
||||
]
|
||||
},
|
||||
@@ -1327,7 +1327,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# This is all the same as before\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.checkpoint.memory import InMemorySaver\n",
|
||||
"from langgraph.graph import END, StateGraph, START\n",
|
||||
"\n",
|
||||
"builder = StateGraph(State)\n",
|
||||
@@ -1353,7 +1353,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"builder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\n",
|
||||
"checkpointer = MemorySaver()"
|
||||
"checkpointer = InMemorySaver()"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -250,6 +250,7 @@ nav:
|
||||
- Storage: reference/store.md
|
||||
- Caching: reference/cache.md
|
||||
- Types: reference/types.md
|
||||
- Runtime: reference/runtime.md
|
||||
- Config: reference/config.md
|
||||
- Errors: reference/errors.md
|
||||
- Constants: reference/constants.md
|
||||
|
||||
Generated
+20
-20
@@ -15,16 +15,16 @@ name = "ag2"
|
||||
version = "0.9.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "asyncer" },
|
||||
{ name = "diskcache" },
|
||||
{ name = "docker" },
|
||||
{ name = "httpx" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "termcolor" },
|
||||
{ name = "tiktoken" },
|
||||
{ name = "anyio", marker = "python_full_version < '3.13'" },
|
||||
{ name = "asyncer", marker = "python_full_version < '3.13'" },
|
||||
{ name = "diskcache", marker = "python_full_version < '3.13'" },
|
||||
{ name = "docker", marker = "python_full_version < '3.13'" },
|
||||
{ name = "httpx", marker = "python_full_version < '3.13'" },
|
||||
{ name = "packaging", marker = "python_full_version < '3.13'" },
|
||||
{ name = "pydantic", marker = "python_full_version < '3.13'" },
|
||||
{ name = "python-dotenv", marker = "python_full_version < '3.13'" },
|
||||
{ name = "termcolor", marker = "python_full_version < '3.13'" },
|
||||
{ name = "tiktoken", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/15/edfbbf217e19ea647225b3ab72a6e3755d2677665f1a7f8e5108da3feabd/ag2-0.9.6.tar.gz", hash = "sha256:d6f7812b1a49654d14113fa3c13ccb593115dee1193744ca428d7178d2b32090", size = 3356270, upload-time = "2025-07-08T14:56:21.63Z" }
|
||||
wheels = [
|
||||
@@ -267,7 +267,7 @@ name = "asyncer"
|
||||
version = "0.0.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "anyio", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ff/67/7ea59c3e69eaeee42e7fc91a5be67ca5849c8979acac2b920249760c6af2/asyncer-0.0.8.tar.gz", hash = "sha256:a589d980f57e20efb07ed91d0dbe67f1d2fd343e7142c66d3a099f05c620739c", size = 18217, upload-time = "2024-08-24T23:15:36.449Z" }
|
||||
wheels = [
|
||||
@@ -288,7 +288,7 @@ name = "autogen"
|
||||
version = "0.9.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "ag2" },
|
||||
{ name = "ag2", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/67/b9/dc958031b7e08ee50e3d40f5991f4c0bc21538df8d53aa3e9a9f2e2f7818/autogen-0.9.6.tar.gz", hash = "sha256:dc2efbeef61002608983afb120e62f8a109815eb741bcbc9ef398dcff7424a30", size = 43422, upload-time = "2025-07-08T14:56:17.6Z" }
|
||||
wheels = [
|
||||
@@ -914,9 +914,9 @@ name = "docker"
|
||||
version = "7.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||
{ name = "requests" },
|
||||
{ name = "urllib3" },
|
||||
{ name = "pywin32", marker = "python_full_version < '3.13' and sys_platform == 'win32'" },
|
||||
{ name = "requests", marker = "python_full_version < '3.13'" },
|
||||
{ name = "urllib3", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" }
|
||||
wheels = [
|
||||
@@ -2337,7 +2337,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "0.5.2"
|
||||
version = "0.6.0a1"
|
||||
source = { editable = "../libs/langgraph" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -2365,7 +2365,7 @@ dev = [
|
||||
{ name = "langgraph-checkpoint", editable = "../libs/checkpoint" },
|
||||
{ name = "langgraph-checkpoint-postgres", editable = "../libs/checkpoint-postgres" },
|
||||
{ name = "langgraph-checkpoint-sqlite", editable = "../libs/checkpoint-sqlite" },
|
||||
{ name = "langgraph-cli", extras = ["inmem"] },
|
||||
{ name = "langgraph-cli", extras = ["inmem"], editable = "../libs/cli" },
|
||||
{ name = "langgraph-prebuilt", editable = "../libs/prebuilt" },
|
||||
{ name = "langgraph-sdk", editable = "../libs/sdk-py" },
|
||||
{ name = "mypy" },
|
||||
@@ -2388,7 +2388,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.1.0"
|
||||
version = "2.1.1"
|
||||
source = { editable = "../libs/checkpoint" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -2433,7 +2433,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "2.0.21"
|
||||
version = "2.0.23"
|
||||
source = { editable = "../libs/checkpoint-postgres" }
|
||||
dependencies = [
|
||||
{ name = "langgraph-checkpoint" },
|
||||
@@ -2674,7 +2674,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.1.72"
|
||||
version = "0.2.0a1"
|
||||
source = { editable = "../libs/sdk-py" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
|
||||
+112
-3
@@ -152,6 +152,14 @@ base64-js@^1.5.1:
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
|
||||
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
function-bind "^1.1.2"
|
||||
|
||||
camelcase@6:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
|
||||
@@ -201,6 +209,42 @@ delayed-stream@~1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
dunder-proto@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
|
||||
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
|
||||
dependencies:
|
||||
call-bind-apply-helpers "^1.0.1"
|
||||
es-errors "^1.3.0"
|
||||
gopd "^1.2.0"
|
||||
|
||||
es-define-property@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
|
||||
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
|
||||
|
||||
es-errors@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
||||
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
||||
|
||||
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
|
||||
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
|
||||
es-set-tostringtag@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
|
||||
integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
get-intrinsic "^1.2.6"
|
||||
has-tostringtag "^1.0.2"
|
||||
hasown "^2.0.2"
|
||||
|
||||
event-lite@^0.1.1:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/event-lite/-/event-lite-0.1.3.tgz#3dfe01144e808ac46448f0c19b4ab68e403a901d"
|
||||
@@ -222,12 +266,14 @@ form-data-encoder@1.7.2:
|
||||
integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48"
|
||||
integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
|
||||
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
es-set-tostringtag "^2.1.0"
|
||||
hasown "^2.0.2"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
formdata-node@^4.3.2:
|
||||
@@ -238,11 +284,69 @@ formdata-node@^4.3.2:
|
||||
node-domexception "1.0.0"
|
||||
web-streams-polyfill "4.0.0-beta.3"
|
||||
|
||||
function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
|
||||
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
||||
|
||||
get-intrinsic@^1.2.6:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
|
||||
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
|
||||
dependencies:
|
||||
call-bind-apply-helpers "^1.0.2"
|
||||
es-define-property "^1.0.1"
|
||||
es-errors "^1.3.0"
|
||||
es-object-atoms "^1.1.1"
|
||||
function-bind "^1.1.2"
|
||||
get-proto "^1.0.1"
|
||||
gopd "^1.2.0"
|
||||
has-symbols "^1.1.0"
|
||||
hasown "^2.0.2"
|
||||
math-intrinsics "^1.1.0"
|
||||
|
||||
get-proto@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
|
||||
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
|
||||
dependencies:
|
||||
dunder-proto "^1.0.1"
|
||||
es-object-atoms "^1.0.0"
|
||||
|
||||
gopd@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
|
||||
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
|
||||
|
||||
has-flag@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
||||
|
||||
has-symbols@^1.0.3, has-symbols@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
|
||||
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
|
||||
|
||||
has-tostringtag@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
|
||||
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
|
||||
dependencies:
|
||||
has-symbols "^1.0.3"
|
||||
|
||||
hasown@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
|
||||
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
|
||||
dependencies:
|
||||
function-bind "^1.1.2"
|
||||
|
||||
he@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||
|
||||
humanize-ms@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
|
||||
@@ -295,6 +399,11 @@ json-stringify-safe@^5.0.1:
|
||||
semver "^7.6.3"
|
||||
uuid "^10.0.0"
|
||||
|
||||
math-intrinsics@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
|
||||
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
|
||||
|
||||
mime-db@1.52.0:
|
||||
version "1.52.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
"id": "2dff2209-44c7-4e2c-b607-ba6675f9e45f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["from langgraph.checkpoint.memory import MemorySaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(GraphState)\n\n# Define the nodes\nbuilder.add_node(\"generate\", generate) # generation solution\nbuilder.add_node(\"check_code\", code_check) # check code\n\n# Build graph\nbuilder.add_edge(START, \"generate\")\nbuilder.add_edge(\"generate\", \"check_code\")\nbuilder.add_conditional_edges(\n \"check_code\",\n decide_to_finish,\n {\n \"end\": END,\n \"generate\": \"generate\",\n },\n)\n\nmemory = MemorySaver()\ngraph = builder.compile(checkpointer=memory)"]
|
||||
"source": ["from langgraph.checkpoint.memory import InMemorySaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(GraphState)\n\n# Define the nodes\nbuilder.add_node(\"generate\", generate) # generation solution\nbuilder.add_node(\"check_code\", code_check) # check code\n\n# Build graph\nbuilder.add_edge(START, \"generate\")\nbuilder.add_edge(\"generate\", \"check_code\")\nbuilder.add_conditional_edges(\n \"check_code\",\n decide_to_finish,\n {\n \"end\": END,\n \"generate\": \"generate\",\n },\n)\n\nmemory = InMemorySaver()\ngraph = builder.compile(checkpointer=memory)"]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
|
||||
@@ -284,10 +284,7 @@ class PostgresSaver(BasePostgresSaver):
|
||||
configurable = config["configurable"].copy()
|
||||
thread_id = configurable.pop("thread_id")
|
||||
checkpoint_ns = configurable.pop("checkpoint_ns")
|
||||
checkpoint_id = configurable.pop(
|
||||
"checkpoint_id", configurable.pop("thread_ts", None)
|
||||
)
|
||||
|
||||
checkpoint_id = configurable.pop("checkpoint_id", None)
|
||||
copy = checkpoint.copy()
|
||||
copy["channel_values"] = copy["channel_values"].copy()
|
||||
next_config = {
|
||||
|
||||
@@ -240,9 +240,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
configurable = config["configurable"].copy()
|
||||
thread_id = configurable.pop("thread_id")
|
||||
checkpoint_ns = configurable.pop("checkpoint_ns")
|
||||
checkpoint_id = configurable.pop(
|
||||
"checkpoint_id", configurable.pop("thread_ts", None)
|
||||
)
|
||||
checkpoint_id = configurable.pop("checkpoint_id", None)
|
||||
|
||||
copy = checkpoint.copy()
|
||||
copy["channel_values"] = copy["channel_values"].copy()
|
||||
|
||||
@@ -191,7 +191,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
||||
) -> None:
|
||||
warnings.warn(
|
||||
"ShallowPostgresSaver is deprecated as of version 2.0.20 and will be removed in 3.0.0. "
|
||||
"Use PostgresSaver instead, and invoke the graph with `graph.invoke(..., checkpoint_during=False)`.",
|
||||
"Use PostgresSaver instead, and invoke the graph with `graph.invoke(..., durability='exit')`.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
@@ -547,7 +547,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
||||
) -> None:
|
||||
warnings.warn(
|
||||
"AsyncShallowPostgresSaver is deprecated as of version 2.0.20 and will be removed in 3.0.0. "
|
||||
"Use AsyncPostgresSaver instead, and invoke the graph with `await graph.ainvoke(..., checkpoint_during=False)`.",
|
||||
"Use AsyncPostgresSaver instead, and invoke the graph with `await graph.ainvoke(..., durability='exit')`.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
@@ -161,8 +161,7 @@ def test_data():
|
||||
config_1: RunnableConfig = {
|
||||
"configurable": {
|
||||
"thread_id": "thread-1",
|
||||
# for backwards compatibility testing
|
||||
"thread_ts": "1",
|
||||
"checkpoint_id": "1",
|
||||
"checkpoint_ns": "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,8 +143,7 @@ def test_data():
|
||||
config_1: RunnableConfig = {
|
||||
"configurable": {
|
||||
"thread_id": "thread-1",
|
||||
# for backwards compatibility testing
|
||||
"thread_ts": "1",
|
||||
"checkpoint_id": "1",
|
||||
"checkpoint_ns": "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import concurrent.futures
|
||||
import datetime
|
||||
import logging
|
||||
import re
|
||||
import sqlite3
|
||||
import threading
|
||||
from collections import defaultdict
|
||||
@@ -107,6 +108,23 @@ def _decode_ns_text(namespace: str) -> tuple[str, ...]:
|
||||
return tuple(namespace.split("."))
|
||||
|
||||
|
||||
def _validate_filter_key(key: str) -> None:
|
||||
"""Validate that a filter key is safe for use in SQL queries.
|
||||
|
||||
Args:
|
||||
key: The filter key to validate
|
||||
|
||||
Raises:
|
||||
ValueError: If the key contains invalid characters that could enable SQL injection
|
||||
"""
|
||||
# Allow alphanumeric characters, underscores, dots, and hyphens
|
||||
# This covers typical JSON property names while preventing SQL injection
|
||||
if not re.match(r"^[a-zA-Z0-9_.-]+$", key):
|
||||
raise ValueError(
|
||||
f"Invalid filter key: '{key}'. Filter keys must contain only alphanumeric characters, underscores, dots, and hyphens."
|
||||
)
|
||||
|
||||
|
||||
def _json_loads(content: bytes | str | orjson.Fragment) -> Any:
|
||||
if isinstance(content, orjson.Fragment):
|
||||
if hasattr(content, "buf"):
|
||||
@@ -372,6 +390,8 @@ class BaseSqliteStore:
|
||||
filter_conditions = []
|
||||
if op.filter:
|
||||
for key, value in op.filter.items():
|
||||
_validate_filter_key(key)
|
||||
|
||||
if isinstance(value, dict):
|
||||
for op_name, val in value.items():
|
||||
condition, filter_params_ = self._get_filter_condition(
|
||||
@@ -622,6 +642,8 @@ class BaseSqliteStore:
|
||||
|
||||
def _get_filter_condition(self, key: str, op: str, value: Any) -> tuple[str, list]:
|
||||
"""Helper to generate filter conditions."""
|
||||
_validate_filter_key(key)
|
||||
|
||||
# We need to properly format values for SQLite JSON extraction comparison
|
||||
if op == "$eq":
|
||||
if isinstance(value, str):
|
||||
@@ -858,6 +880,8 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
||||
|
||||
def _get_filter_condition(self, key: str, op: str, value: Any) -> tuple[str, list]:
|
||||
"""Helper to generate filter conditions."""
|
||||
_validate_filter_key(key)
|
||||
|
||||
# We need to properly format values for SQLite JSON extraction comparison
|
||||
if op == "$eq":
|
||||
if isinstance(value, str):
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "2.0.10"
|
||||
version = "2.0.11"
|
||||
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
requires-python = ">=3.9"
|
||||
|
||||
@@ -19,8 +19,7 @@ class TestAsyncSqliteSaver:
|
||||
self.config_1: RunnableConfig = {
|
||||
"configurable": {
|
||||
"thread_id": "thread-1",
|
||||
# for backwards compatibility testing
|
||||
"thread_ts": "1",
|
||||
"checkpoint_id": "1",
|
||||
"checkpoint_ns": "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class TestSqliteSaver:
|
||||
"configurable": {
|
||||
"thread_id": "thread-1",
|
||||
# for backwards compatibility testing
|
||||
"thread_ts": "1",
|
||||
"checkpoint_id": "1",
|
||||
"checkpoint_ns": "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1047,3 +1047,23 @@ def test_search_items(
|
||||
for ns in test_namespaces:
|
||||
key = f"item_{ns[-1]}"
|
||||
store.delete(ns, key)
|
||||
|
||||
|
||||
def test_sql_injection_vulnerability(store: SqliteStore) -> None:
|
||||
"""Test that SQL injection via malicious filter keys is prevented."""
|
||||
# Add public and private documents
|
||||
store.put(("docs",), "public", {"access": "public", "data": "public info"})
|
||||
store.put(
|
||||
("docs",), "private", {"access": "private", "data": "secret", "password": "123"}
|
||||
)
|
||||
|
||||
# Normal query - returns 1 public document
|
||||
normal = store.search(("docs",), filter={"access": "public"})
|
||||
assert len(normal) == 1
|
||||
assert normal[0].value["access"] == "public"
|
||||
|
||||
# SQL injection attempt via malicious key should raise ValueError
|
||||
malicious_key = "access') = 'public' OR '1'='1' OR json_extract(value, '$."
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid filter key"):
|
||||
store.search(("docs",), filter={malicious_key: "dummy"})
|
||||
|
||||
Generated
+1
-1
@@ -346,7 +346,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "2.0.10"
|
||||
version = "2.0.11"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiosqlite" },
|
||||
|
||||
@@ -36,7 +36,7 @@ Each checkpointer should conform to `langgraph.checkpoint.base.BaseCheckpointSav
|
||||
|
||||
- `.put` - Store a checkpoint with its configuration and metadata.
|
||||
- `.put_writes` - Store intermediate writes linked to a checkpoint (i.e. pending writes).
|
||||
- `.get_tuple` - Fetch a checkpoint tuple using for a given configuration (`thread_id` and `thread_ts`).
|
||||
- `.get_tuple` - Fetch a checkpoint tuple using for a given configuration (`thread_id` and `checkpoint_id`).
|
||||
- `.list` - List checkpoints that match a given configuration and filter criteria.
|
||||
|
||||
If the checkpointer will be used with asynchronous graph execution (i.e. executing the graph via `.ainvoke`, `.astream`, `.abatch`), checkpointer must implement asynchronous versions of the above methods (`.aput`, `.aput_writes`, `.aget_tuple`, `.alist`).
|
||||
@@ -44,12 +44,12 @@ If the checkpointer will be used with asynchronous graph execution (i.e. executi
|
||||
## Usage
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
write_config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
|
||||
read_config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
checkpointer = InMemorySaver()
|
||||
checkpoint = {
|
||||
"v": 4,
|
||||
"ts": "2024-07-31T20:14:19.804150+00:00",
|
||||
|
||||
@@ -375,10 +375,8 @@ class EmptyChannelError(Exception):
|
||||
|
||||
|
||||
def get_checkpoint_id(config: RunnableConfig) -> str | None:
|
||||
"""Get checkpoint ID in a backwards-compatible manner (fallback on thread_ts)."""
|
||||
return config["configurable"].get(
|
||||
"checkpoint_id", config["configurable"].get("thread_ts")
|
||||
)
|
||||
"""Get checkpoint ID."""
|
||||
return config["configurable"].get("checkpoint_id")
|
||||
|
||||
|
||||
def get_checkpoint_metadata(
|
||||
@@ -413,7 +411,6 @@ WRITES_IDX_MAP = {ERROR: -1, SCHEDULED: -2, INTERRUPT: -3, RESUME: -4}
|
||||
|
||||
EXCLUDED_METADATA_KEYS = {
|
||||
"thread_id",
|
||||
"thread_ts",
|
||||
"checkpoint_id",
|
||||
"checkpoint_ns",
|
||||
"checkpoint_map",
|
||||
|
||||
@@ -22,8 +22,7 @@ class TestMemorySaver:
|
||||
"configurable": {
|
||||
"thread_id": "thread-1",
|
||||
"checkpoint_ns": "",
|
||||
# for backwards compatibility testing
|
||||
"thread_ts": "1",
|
||||
"checkpoint_id": "1",
|
||||
}
|
||||
}
|
||||
self.config_2: RunnableConfig = {
|
||||
@@ -190,6 +189,6 @@ class TestMemorySaver:
|
||||
|
||||
|
||||
def test_memory_saver() -> None:
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
assert isinstance(MemorySaver(), InMemorySaver)
|
||||
assert isinstance(InMemorySaver(), InMemorySaver)
|
||||
|
||||
@@ -49,12 +49,12 @@ def call_model(state, config):
|
||||
tool_node = ToolNode(tools)
|
||||
|
||||
|
||||
class ConfigSchema(TypedDict):
|
||||
class ContextSchema(TypedDict):
|
||||
model: Literal["anthropic", "openai"]
|
||||
|
||||
|
||||
# Define a new graph
|
||||
workflow = StateGraph(AgentState, config_schema=ConfigSchema)
|
||||
workflow = StateGraph(AgentState, context_schema=ContextSchema)
|
||||
|
||||
# Define the two nodes we will cycle between
|
||||
workflow.add_node("agent", call_model)
|
||||
|
||||
@@ -153,6 +153,12 @@ OPT_POSTGRES_URI = click.option(
|
||||
help="Postgres URI to use for the database. Defaults to launching a local database",
|
||||
)
|
||||
|
||||
OPT_API_VERSION = click.option(
|
||||
"--api-version",
|
||||
type=str,
|
||||
help="API server version to use for the base image. If unspecified, the latest version will be used.",
|
||||
)
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version=__version__, prog_name="LangGraph CLI")
|
||||
@@ -170,6 +176,7 @@ def cli():
|
||||
@OPT_DEBUGGER_BASE_URL
|
||||
@OPT_WATCH
|
||||
@OPT_POSTGRES_URI
|
||||
@OPT_API_VERSION
|
||||
@click.option(
|
||||
"--image",
|
||||
type=str,
|
||||
@@ -203,6 +210,7 @@ def up(
|
||||
debugger_port: Optional[int],
|
||||
debugger_base_url: Optional[str],
|
||||
postgres_uri: Optional[str],
|
||||
api_version: Optional[str],
|
||||
image: Optional[str],
|
||||
base_image: Optional[str],
|
||||
):
|
||||
@@ -225,6 +233,7 @@ For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KE
|
||||
debugger_port=debugger_port,
|
||||
debugger_base_url=debugger_base_url,
|
||||
postgres_uri=postgres_uri,
|
||||
api_version=api_version,
|
||||
image=image,
|
||||
base_image=base_image,
|
||||
)
|
||||
@@ -290,6 +299,7 @@ def _build(
|
||||
config: pathlib.Path,
|
||||
config_json: dict,
|
||||
base_image: Optional[str],
|
||||
api_version: Optional[str],
|
||||
pull: bool,
|
||||
tag: str,
|
||||
passthrough: Sequence[str] = (),
|
||||
@@ -300,7 +310,7 @@ def _build(
|
||||
subp_exec(
|
||||
"docker",
|
||||
"pull",
|
||||
langgraph_cli.config.docker_tag(config_json, base_image),
|
||||
langgraph_cli.config.docker_tag(config_json, base_image, api_version),
|
||||
verbose=True,
|
||||
)
|
||||
)
|
||||
@@ -314,7 +324,7 @@ def _build(
|
||||
]
|
||||
# apply config
|
||||
stdin, additional_contexts = langgraph_cli.config.config_to_docker(
|
||||
config, config_json, base_image
|
||||
config, config_json, base_image, api_version
|
||||
)
|
||||
# add additional_contexts
|
||||
if additional_contexts:
|
||||
@@ -355,6 +365,7 @@ def _build(
|
||||
"\n\n \b\nExamples:\n --base-image langchain/langgraph-server:0.2.18 # Pin to a specific patch version"
|
||||
"\n --base-image langchain/langgraph-server:0.2 # Pin to a minor version (Python)",
|
||||
)
|
||||
@OPT_API_VERSION
|
||||
@click.argument("docker_build_args", nargs=-1, type=click.UNPROCESSED)
|
||||
@cli.command(
|
||||
help="📦 Build LangGraph API server Docker image.",
|
||||
@@ -367,6 +378,7 @@ def build(
|
||||
config: pathlib.Path,
|
||||
docker_build_args: Sequence[str],
|
||||
base_image: Optional[str],
|
||||
api_version: Optional[str],
|
||||
pull: bool,
|
||||
tag: str,
|
||||
):
|
||||
@@ -376,7 +388,15 @@ def build(
|
||||
config_json = langgraph_cli.config.validate_config_file(config)
|
||||
warn_non_wolfi_distro(config_json)
|
||||
_build(
|
||||
runner, set, config, config_json, base_image, pull, tag, docker_build_args
|
||||
runner,
|
||||
set,
|
||||
config,
|
||||
config_json,
|
||||
base_image,
|
||||
api_version,
|
||||
pull,
|
||||
tag,
|
||||
docker_build_args,
|
||||
)
|
||||
|
||||
|
||||
@@ -456,12 +476,14 @@ tests
|
||||
"\n\n \b\nExamples:\n --base-image langchain/langgraph-server:0.2.18 # Pin to a specific patch version"
|
||||
"\n --base-image langchain/langgraph-server:0.2 # Pin to a minor version (Python)",
|
||||
)
|
||||
@OPT_API_VERSION
|
||||
@log_command
|
||||
def dockerfile(
|
||||
save_path: str,
|
||||
config: pathlib.Path,
|
||||
add_docker_compose: bool,
|
||||
base_image: Optional[str] = None,
|
||||
api_version: Optional[str] = None,
|
||||
) -> None:
|
||||
save_path = pathlib.Path(save_path).absolute()
|
||||
secho(f"🔍 Validating configuration at path: {config}", fg="yellow")
|
||||
@@ -474,6 +496,7 @@ def dockerfile(
|
||||
config,
|
||||
config_json,
|
||||
base_image=base_image,
|
||||
api_version=api_version,
|
||||
)
|
||||
with open(str(save_path), "w", encoding="utf-8") as f:
|
||||
f.write(dockerfile)
|
||||
@@ -739,6 +762,7 @@ def prepare_args_and_stdin(
|
||||
debugger_port: Optional[int] = None,
|
||||
debugger_base_url: Optional[str] = None,
|
||||
postgres_uri: Optional[str] = None,
|
||||
api_version: Optional[str] = None,
|
||||
# Like "my-tag" (if you already built it locally)
|
||||
image: Optional[str] = None,
|
||||
# Like "langchain/langgraphjs-api" or "langchain/langgraph-api
|
||||
@@ -754,6 +778,7 @@ def prepare_args_and_stdin(
|
||||
postgres_uri=postgres_uri,
|
||||
image=image, # Pass image to compose YAML generator
|
||||
base_image=base_image,
|
||||
api_version=api_version,
|
||||
)
|
||||
args = [
|
||||
"--project-directory",
|
||||
@@ -769,6 +794,7 @@ def prepare_args_and_stdin(
|
||||
config,
|
||||
watch=watch,
|
||||
base_image=langgraph_cli.config.default_base_image(config),
|
||||
api_version=api_version,
|
||||
image=image,
|
||||
)
|
||||
return args, stdin
|
||||
@@ -787,6 +813,7 @@ def prepare(
|
||||
debugger_port: Optional[int] = None,
|
||||
debugger_base_url: Optional[str] = None,
|
||||
postgres_uri: Optional[str] = None,
|
||||
api_version: Optional[str] = None,
|
||||
image: Optional[str] = None,
|
||||
base_image: Optional[str] = None,
|
||||
) -> tuple[list[str], str]:
|
||||
@@ -799,7 +826,7 @@ def prepare(
|
||||
subp_exec(
|
||||
"docker",
|
||||
"pull",
|
||||
langgraph_cli.config.docker_tag(config_json, base_image),
|
||||
langgraph_cli.config.docker_tag(config_json, base_image, api_version),
|
||||
verbose=verbose,
|
||||
)
|
||||
)
|
||||
@@ -814,6 +841,7 @@ def prepare(
|
||||
debugger_port=debugger_port,
|
||||
debugger_base_url=debugger_base_url or f"http://127.0.0.1:{port}",
|
||||
postgres_uri=postgres_uri,
|
||||
api_version=api_version,
|
||||
image=image,
|
||||
base_image=base_image,
|
||||
)
|
||||
|
||||
@@ -1213,6 +1213,7 @@ def python_config_to_docker(
|
||||
config_path: pathlib.Path,
|
||||
config: Config,
|
||||
base_image: str,
|
||||
api_version: Optional[str] = None,
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
"""Generate a Dockerfile from the configuration."""
|
||||
pip_installer = config.get("pip_installer", "auto")
|
||||
@@ -1360,7 +1361,7 @@ ADD {relpath} /deps/{name}
|
||||
"# -- End of JS dependencies install --",
|
||||
]
|
||||
)
|
||||
image_str = docker_tag(config, base_image)
|
||||
image_str = docker_tag(config, base_image, api_version)
|
||||
docker_file_contents = [
|
||||
f"FROM {image_str}",
|
||||
"",
|
||||
@@ -1402,10 +1403,11 @@ def node_config_to_docker(
|
||||
config_path: pathlib.Path,
|
||||
config: Config,
|
||||
base_image: str,
|
||||
api_version: Optional[str] = None,
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
faux_path = f"/deps/{config_path.parent.name}"
|
||||
install_cmd = _get_node_pm_install_cmd(config_path, config)
|
||||
image_str = docker_tag(config, base_image)
|
||||
image_str = docker_tag(config, base_image, api_version)
|
||||
|
||||
env_vars: list[str] = []
|
||||
|
||||
@@ -1461,6 +1463,7 @@ def default_base_image(config: Config) -> str:
|
||||
def docker_tag(
|
||||
config: Config,
|
||||
base_image: Optional[str] = None,
|
||||
api_version: Optional[str] = None,
|
||||
) -> str:
|
||||
base_image = base_image or default_base_image(config)
|
||||
|
||||
@@ -1473,28 +1476,43 @@ def docker_tag(
|
||||
if "/langgraph-server" in base_image:
|
||||
return f"{base_image}-py{config['python_version']}"
|
||||
|
||||
# Build the standard tag format
|
||||
language, version = None, None
|
||||
if config.get("node_version") and not config.get("python_version"):
|
||||
return f"{base_image}:{config['node_version']}{distro_tag}"
|
||||
return f"{base_image}:{config['python_version']}{distro_tag}"
|
||||
language, version = "node", config["node_version"]
|
||||
else:
|
||||
language, version = "py", config["python_version"]
|
||||
|
||||
version_distro_tag = f"{version}{distro_tag}"
|
||||
|
||||
# Prepend API version if provided
|
||||
if api_version:
|
||||
full_tag = f"{api_version}-{language}{version_distro_tag}"
|
||||
else:
|
||||
full_tag = version_distro_tag
|
||||
|
||||
return f"{base_image}:{full_tag}"
|
||||
|
||||
|
||||
def config_to_docker(
|
||||
config_path: pathlib.Path,
|
||||
config: Config,
|
||||
base_image: Optional[str] = None,
|
||||
api_version: Optional[str] = None,
|
||||
) -> tuple[str, dict[str, str]]:
|
||||
base_image = base_image or default_base_image(config)
|
||||
|
||||
if config.get("node_version") and not config.get("python_version"):
|
||||
return node_config_to_docker(config_path, config, base_image)
|
||||
return node_config_to_docker(config_path, config, base_image, api_version)
|
||||
|
||||
return python_config_to_docker(config_path, config, base_image)
|
||||
return python_config_to_docker(config_path, config, base_image, api_version)
|
||||
|
||||
|
||||
def config_to_compose(
|
||||
config_path: pathlib.Path,
|
||||
config: Config,
|
||||
base_image: Optional[str] = None,
|
||||
api_version: Optional[str] = None,
|
||||
image: Optional[str] = None,
|
||||
watch: bool = False,
|
||||
) -> str:
|
||||
@@ -1531,7 +1549,7 @@ def config_to_compose(
|
||||
|
||||
else:
|
||||
dockerfile, additional_contexts = config_to_docker(
|
||||
config_path, config, base_image
|
||||
config_path, config, base_image, api_version
|
||||
)
|
||||
|
||||
additional_contexts_str = "\n".join(
|
||||
|
||||
@@ -147,6 +147,8 @@ def compose_as_dict(
|
||||
image: Optional[str] = None,
|
||||
# Base image to use for the LangGraph API server
|
||||
base_image: Optional[str] = None,
|
||||
# API version of the base image
|
||||
api_version: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Create a docker compose file as a dictionary in YML style."""
|
||||
if postgres_uri is None:
|
||||
@@ -252,6 +254,7 @@ def compose(
|
||||
postgres_uri: Optional[str] = None,
|
||||
image: Optional[str] = None,
|
||||
base_image: Optional[str] = None,
|
||||
api_version: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Create a docker compose file as a string."""
|
||||
compose_content = compose_as_dict(
|
||||
@@ -262,6 +265,7 @@ def compose(
|
||||
postgres_uri=postgres_uri,
|
||||
image=image,
|
||||
base_image=base_image,
|
||||
api_version=api_version,
|
||||
)
|
||||
compose_str = dict_to_yaml(compose_content)
|
||||
return compose_str
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph-cli"
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
description = "CLI for interacting with LangGraph API"
|
||||
authors = []
|
||||
requires-python = ">=3.9"
|
||||
|
||||
@@ -574,3 +574,248 @@ def test_build_generate_proper_build_context():
|
||||
assert len(build_contexts) == 2, (
|
||||
f"Expected 2 build contexts, but found {len(build_contexts)}"
|
||||
)
|
||||
|
||||
|
||||
def test_dockerfile_command_with_api_version() -> None:
|
||||
"""Test the 'dockerfile' command with --api-version flag."""
|
||||
runner = CliRunner()
|
||||
config_content = {
|
||||
"python_version": "3.11",
|
||||
"graphs": {"agent": "agent.py:graph"},
|
||||
"dependencies": ["."],
|
||||
}
|
||||
|
||||
with temporary_config_folder(config_content) as temp_dir:
|
||||
save_path = temp_dir / "Dockerfile"
|
||||
agent_path = temp_dir / "agent.py"
|
||||
agent_path.touch()
|
||||
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"dockerfile",
|
||||
str(save_path),
|
||||
"--config",
|
||||
str(temp_dir / "config.json"),
|
||||
"--api-version",
|
||||
"0.2.74",
|
||||
],
|
||||
)
|
||||
|
||||
# Assert command was successful
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "✅ Created: Dockerfile" in result.output
|
||||
|
||||
# Check if Dockerfile was created and contains correct FROM line
|
||||
assert save_path.exists()
|
||||
with open(save_path) as f:
|
||||
dockerfile = f.read()
|
||||
assert "FROM langchain/langgraph-api:0.2.74-py3.11" in dockerfile
|
||||
|
||||
|
||||
def test_dockerfile_command_with_api_version_and_base_image() -> None:
|
||||
"""Test the 'dockerfile' command with both --api-version and --base-image flags."""
|
||||
runner = CliRunner()
|
||||
config_content = {
|
||||
"python_version": "3.12",
|
||||
"graphs": {"agent": "agent.py:graph"},
|
||||
"dependencies": ["."],
|
||||
"image_distro": "wolfi",
|
||||
}
|
||||
|
||||
with temporary_config_folder(config_content) as temp_dir:
|
||||
save_path = temp_dir / "Dockerfile"
|
||||
agent_path = temp_dir / "agent.py"
|
||||
agent_path.touch()
|
||||
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"dockerfile",
|
||||
str(save_path),
|
||||
"--config",
|
||||
str(temp_dir / "config.json"),
|
||||
"--api-version",
|
||||
"1.0.0",
|
||||
"--base-image",
|
||||
"my-registry/custom-api",
|
||||
],
|
||||
)
|
||||
|
||||
# Assert command was successful
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "✅ Created: Dockerfile" in result.output
|
||||
|
||||
# Check if Dockerfile was created and contains correct FROM line
|
||||
assert save_path.exists()
|
||||
with open(save_path) as f:
|
||||
dockerfile = f.read()
|
||||
assert "FROM my-registry/custom-api:1.0.0-py3.12-wolfi" in dockerfile
|
||||
|
||||
|
||||
def test_dockerfile_command_with_api_version_nodejs() -> None:
|
||||
"""Test the 'dockerfile' command with --api-version flag for Node.js config."""
|
||||
runner = CliRunner()
|
||||
config_content = {
|
||||
"node_version": "20",
|
||||
"graphs": {"agent": "agent.js:graph"},
|
||||
}
|
||||
|
||||
with temporary_config_folder(config_content) as temp_dir:
|
||||
save_path = temp_dir / "Dockerfile"
|
||||
agent_path = temp_dir / "agent.js"
|
||||
agent_path.touch()
|
||||
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"dockerfile",
|
||||
str(save_path),
|
||||
"--config",
|
||||
str(temp_dir / "config.json"),
|
||||
"--api-version",
|
||||
"0.2.74",
|
||||
],
|
||||
)
|
||||
|
||||
# Assert command was successful
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "✅ Created: Dockerfile" in result.output
|
||||
|
||||
# Check if Dockerfile was created and contains correct FROM line
|
||||
assert save_path.exists()
|
||||
with open(save_path) as f:
|
||||
dockerfile = f.read()
|
||||
assert "FROM langchain/langgraphjs-api:0.2.74-node20" in dockerfile
|
||||
|
||||
|
||||
def test_build_command_with_api_version() -> None:
|
||||
"""Test the 'build' command with --api-version flag."""
|
||||
runner = CliRunner()
|
||||
config_content = {
|
||||
"python_version": "3.11",
|
||||
"graphs": {"agent": "agent.py:graph"},
|
||||
"dependencies": ["."],
|
||||
"image_distro": "wolfi", # Use wolfi to avoid warning messages
|
||||
}
|
||||
|
||||
with temporary_config_folder(config_content) as temp_dir:
|
||||
agent_path = temp_dir / "agent.py"
|
||||
agent_path.touch()
|
||||
|
||||
# Mock docker command since we don't want to actually build
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"build",
|
||||
"--tag",
|
||||
"test-image",
|
||||
"--config",
|
||||
str(temp_dir / "config.json"),
|
||||
"--api-version",
|
||||
"0.2.74",
|
||||
"--no-pull", # Avoid pulling non-existent images
|
||||
],
|
||||
catch_exceptions=True,
|
||||
)
|
||||
|
||||
# Check that the build command is called with the correct tag
|
||||
# The output should contain the docker build command with the api_version tag
|
||||
assert "langchain/langgraph-api:0.2.74-py3.11-wolfi" in result.output
|
||||
|
||||
|
||||
def test_build_command_with_api_version_and_base_image() -> None:
|
||||
"""Test the 'build' command with both --api-version and --base-image flags."""
|
||||
runner = CliRunner()
|
||||
config_content = {
|
||||
"python_version": "3.12",
|
||||
"graphs": {"agent": "agent.py:graph"},
|
||||
"dependencies": ["."],
|
||||
"image_distro": "wolfi", # Use wolfi to avoid warning messages
|
||||
}
|
||||
|
||||
with temporary_config_folder(config_content) as temp_dir:
|
||||
agent_path = temp_dir / "agent.py"
|
||||
agent_path.touch()
|
||||
|
||||
# Mock docker command since we don't want to actually build
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"build",
|
||||
"--tag",
|
||||
"test-image",
|
||||
"--config",
|
||||
str(temp_dir / "config.json"),
|
||||
"--api-version",
|
||||
"1.0.0",
|
||||
"--base-image",
|
||||
"my-registry/custom-api",
|
||||
"--no-pull", # Avoid pulling non-existent images
|
||||
],
|
||||
catch_exceptions=True,
|
||||
)
|
||||
|
||||
# Check that the build command includes the api_version
|
||||
assert "my-registry/custom-api:1.0.0-py3.12-wolfi" in result.output
|
||||
|
||||
|
||||
def test_prepare_args_and_stdin_with_api_version() -> None:
|
||||
"""Test prepare_args_and_stdin function with api_version parameter."""
|
||||
config_path = pathlib.Path(__file__).parent / "langgraph.json"
|
||||
config = validate_config(
|
||||
Config(dependencies=["."], graphs={"agent": "agent.py:graph"})
|
||||
)
|
||||
port = 8000
|
||||
api_version = "0.2.74"
|
||||
|
||||
actual_args, actual_stdin = prepare_args_and_stdin(
|
||||
capabilities=DEFAULT_DOCKER_CAPABILITIES,
|
||||
config_path=config_path,
|
||||
config=config,
|
||||
docker_compose=None,
|
||||
port=port,
|
||||
watch=False,
|
||||
api_version=api_version,
|
||||
)
|
||||
|
||||
expected_args = [
|
||||
"--project-directory",
|
||||
str(pathlib.Path(__file__).parent.absolute()),
|
||||
"-f",
|
||||
"-",
|
||||
]
|
||||
|
||||
# Check that the args are correct
|
||||
assert actual_args == expected_args
|
||||
|
||||
# Check that the stdin contains the correct FROM line with api_version
|
||||
assert "FROM langchain/langgraph-api:0.2.74-py3.11" in actual_stdin
|
||||
|
||||
|
||||
def test_prepare_args_and_stdin_with_api_version_and_image() -> None:
|
||||
"""Test prepare_args_and_stdin function with both api_version and image parameters."""
|
||||
config_path = pathlib.Path(__file__).parent / "langgraph.json"
|
||||
config = validate_config(
|
||||
Config(dependencies=["."], graphs={"agent": "agent.py:graph"})
|
||||
)
|
||||
port = 8000
|
||||
api_version = "0.2.74"
|
||||
image = "my-custom-image:latest"
|
||||
|
||||
actual_args, actual_stdin = prepare_args_and_stdin(
|
||||
capabilities=DEFAULT_DOCKER_CAPABILITIES,
|
||||
config_path=config_path,
|
||||
config=config,
|
||||
docker_compose=None,
|
||||
port=port,
|
||||
watch=False,
|
||||
api_version=api_version,
|
||||
image=image,
|
||||
)
|
||||
|
||||
# When image is provided, api_version should be ignored for the image
|
||||
# but the stdin should not contain a build section (since image is provided)
|
||||
assert "pull_policy: build" not in actual_stdin
|
||||
|
||||
@@ -1337,3 +1337,195 @@ def test_docker_tag_different_node_versions_with_distro():
|
||||
)
|
||||
tag = docker_tag(config)
|
||||
assert tag == expected_tag, f"Failed for Node.js {node_version}"
|
||||
|
||||
|
||||
def test_docker_tag_with_api_version():
|
||||
"""Test docker_tag function with api_version parameter."""
|
||||
|
||||
# Test 1: Python config with api_version and default distro
|
||||
config = validate_config(
|
||||
{
|
||||
"python_version": "3.11",
|
||||
"dependencies": ["."],
|
||||
"graphs": {"agent": "./agent.py:graph"},
|
||||
}
|
||||
)
|
||||
tag = docker_tag(config, api_version="0.2.74")
|
||||
assert tag == "langchain/langgraph-api:0.2.74-py3.11"
|
||||
|
||||
# Test 2: Python config with api_version and wolfi distro
|
||||
config = validate_config(
|
||||
{
|
||||
"python_version": "3.12",
|
||||
"dependencies": ["."],
|
||||
"graphs": {"agent": "./agent.py:graph"},
|
||||
"image_distro": "wolfi",
|
||||
}
|
||||
)
|
||||
tag = docker_tag(config, api_version="0.2.74")
|
||||
assert tag == "langchain/langgraph-api:0.2.74-py3.12-wolfi"
|
||||
|
||||
# Test 3: Node.js config with api_version and default distro
|
||||
config = validate_config(
|
||||
{
|
||||
"node_version": "20",
|
||||
"graphs": {"agent": "./agent.js:graph"},
|
||||
}
|
||||
)
|
||||
tag = docker_tag(config, api_version="0.2.74")
|
||||
assert tag == "langchain/langgraphjs-api:0.2.74-node20"
|
||||
|
||||
# Test 4: Node.js config with api_version and wolfi distro
|
||||
config = validate_config(
|
||||
{
|
||||
"node_version": "20",
|
||||
"graphs": {"agent": "./agent.js:graph"},
|
||||
"image_distro": "wolfi",
|
||||
}
|
||||
)
|
||||
tag = docker_tag(config, api_version="0.2.74")
|
||||
assert tag == "langchain/langgraphjs-api:0.2.74-node20-wolfi"
|
||||
|
||||
# Test 5: Custom base image with api_version
|
||||
config = validate_config(
|
||||
{
|
||||
"python_version": "3.11",
|
||||
"dependencies": ["."],
|
||||
"graphs": {"agent": "./agent.py:graph"},
|
||||
"base_image": "my-registry/custom-image",
|
||||
}
|
||||
)
|
||||
tag = docker_tag(config, base_image="my-registry/custom-image", api_version="1.0.0")
|
||||
assert tag == "my-registry/custom-image:1.0.0-py3.11"
|
||||
|
||||
# Test 6: api_version with different Python versions
|
||||
for python_version in ["3.11", "3.12", "3.13"]:
|
||||
config = validate_config(
|
||||
{
|
||||
"python_version": python_version,
|
||||
"dependencies": ["."],
|
||||
"graphs": {"agent": "./agent.py:graph"},
|
||||
}
|
||||
)
|
||||
tag = docker_tag(config, api_version="0.2.74")
|
||||
assert tag == f"langchain/langgraph-api:0.2.74-py{python_version}"
|
||||
|
||||
# Test 7: Without api_version should work as before
|
||||
config = validate_config(
|
||||
{
|
||||
"python_version": "3.11",
|
||||
"dependencies": ["."],
|
||||
"graphs": {"agent": "./agent.py:graph"},
|
||||
}
|
||||
)
|
||||
tag = docker_tag(config)
|
||||
assert tag == "langchain/langgraph-api:3.11"
|
||||
|
||||
# Test 8: api_version with multiplatform config (should default to Python)
|
||||
config = validate_config(
|
||||
{
|
||||
"python_version": "3.11",
|
||||
"node_version": "20",
|
||||
"dependencies": ["."],
|
||||
"graphs": {"python": "./agent.py:graph", "js": "./agent.js:graph"},
|
||||
}
|
||||
)
|
||||
tag = docker_tag(config, api_version="0.2.74")
|
||||
assert tag == "langchain/langgraph-api:0.2.74-py3.11"
|
||||
|
||||
# Test 9: api_version with _INTERNAL_docker_tag should ignore api_version
|
||||
config = validate_config(
|
||||
{
|
||||
"python_version": "3.11",
|
||||
"dependencies": ["."],
|
||||
"graphs": {"agent": "./agent.py:graph"},
|
||||
"_INTERNAL_docker_tag": "internal-tag",
|
||||
}
|
||||
)
|
||||
tag = docker_tag(config, api_version="0.2.74")
|
||||
assert tag == "langchain/langgraph-api:internal-tag"
|
||||
|
||||
# Test 10: api_version with langgraph-server base image should follow special format
|
||||
config = validate_config(
|
||||
{
|
||||
"python_version": "3.11",
|
||||
"dependencies": ["."],
|
||||
"graphs": {"agent": "./agent.py:graph"},
|
||||
}
|
||||
)
|
||||
tag = docker_tag(
|
||||
config, base_image="langchain/langgraph-server:0.2", api_version="0.2.74"
|
||||
)
|
||||
assert tag == "langchain/langgraph-server:0.2-py3.11"
|
||||
|
||||
|
||||
def test_config_to_docker_with_api_version():
|
||||
"""Test config_to_docker function with api_version parameter."""
|
||||
|
||||
# Test Python config with api_version
|
||||
graphs = {"agent": "./agent.py:graph"}
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config({"dependencies": ["."], "graphs": graphs}),
|
||||
"langchain/langgraph-api",
|
||||
api_version="0.2.74",
|
||||
)
|
||||
|
||||
# Check that the FROM line uses the api_version
|
||||
lines = actual_docker_stdin.split("\n")
|
||||
from_line = lines[0]
|
||||
assert from_line == "FROM langchain/langgraph-api:0.2.74-py3.11"
|
||||
|
||||
# Test Node.js config with api_version
|
||||
graphs = {"agent": "./agent.js:graph"}
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config({"node_version": "20", "graphs": graphs}),
|
||||
"langchain/langgraphjs-api",
|
||||
api_version="0.2.74",
|
||||
)
|
||||
|
||||
# Check that the FROM line uses the api_version
|
||||
lines = actual_docker_stdin.split("\n")
|
||||
from_line = lines[0]
|
||||
assert from_line == "FROM langchain/langgraphjs-api:0.2.74-node20"
|
||||
|
||||
|
||||
def test_config_to_compose_with_api_version():
|
||||
"""Test config_to_compose function with api_version parameter."""
|
||||
|
||||
# Test Python config with api_version
|
||||
config = validate_config(
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {"agent": "./agent.py:graph"},
|
||||
}
|
||||
)
|
||||
|
||||
actual_compose_str = config_to_compose(
|
||||
PATH_TO_CONFIG,
|
||||
config,
|
||||
"langchain/langgraph-api",
|
||||
api_version="0.2.74",
|
||||
)
|
||||
|
||||
# Check that the compose file includes the correct FROM line with api_version
|
||||
assert "FROM langchain/langgraph-api:0.2.74-py3.11" in actual_compose_str
|
||||
|
||||
# Test Node.js config with api_version
|
||||
config = validate_config(
|
||||
{
|
||||
"node_version": "20",
|
||||
"graphs": {"agent": "./agent.js:graph"},
|
||||
}
|
||||
)
|
||||
|
||||
actual_compose_str = config_to_compose(
|
||||
PATH_TO_CONFIG,
|
||||
config,
|
||||
"langchain/langgraphjs-api",
|
||||
api_version="0.2.74",
|
||||
)
|
||||
|
||||
# Check that the compose file includes the correct FROM line with api_version
|
||||
assert "FROM langchain/langgraphjs-api:0.2.74-node20" in actual_compose_str
|
||||
|
||||
@@ -146,3 +146,220 @@ services:
|
||||
REDIS_URI: redis://langgraph-redis:6379
|
||||
POSTGRES_URI: {DEFAULT_POSTGRES_URI}"""
|
||||
assert clean_empty_lines(actual_compose_str) == expected_compose_str
|
||||
|
||||
|
||||
def test_compose_with_api_version():
|
||||
"""Test compose function with api_version parameter."""
|
||||
port = 8123
|
||||
api_version = "0.2.74"
|
||||
|
||||
actual_compose_str = compose(
|
||||
DEFAULT_DOCKER_CAPABILITIES, port=port, api_version=api_version
|
||||
)
|
||||
|
||||
# The compose function should generate a compose file that doesn't directly
|
||||
# reference the api_version, since it's handled in the docker tag creation
|
||||
# when building the image. The compose function mainly sets up services.
|
||||
expected_compose_str = f"""volumes:
|
||||
langgraph-data:
|
||||
driver: local
|
||||
services:
|
||||
langgraph-redis:
|
||||
image: redis:6
|
||||
healthcheck:
|
||||
test: redis-cli ping
|
||||
interval: 5s
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
langgraph-postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
ports:
|
||||
- "5433:5432"
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
command:
|
||||
- postgres
|
||||
- -c
|
||||
- shared_preload_libraries=vector
|
||||
volumes:
|
||||
- langgraph-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: pg_isready -U postgres
|
||||
start_period: 10s
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
interval: 5s
|
||||
langgraph-api:
|
||||
ports:
|
||||
- "{port}:8000"
|
||||
depends_on:
|
||||
langgraph-redis:
|
||||
condition: service_healthy
|
||||
langgraph-postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
REDIS_URI: redis://langgraph-redis:6379
|
||||
POSTGRES_URI: {DEFAULT_POSTGRES_URI}"""
|
||||
assert clean_empty_lines(actual_compose_str) == expected_compose_str
|
||||
|
||||
|
||||
def test_compose_with_api_version_and_base_image():
|
||||
"""Test compose function with both api_version and base_image parameters."""
|
||||
port = 8123
|
||||
api_version = "1.0.0"
|
||||
base_image = "my-registry/custom-api"
|
||||
|
||||
actual_compose_str = compose(
|
||||
DEFAULT_DOCKER_CAPABILITIES,
|
||||
port=port,
|
||||
api_version=api_version,
|
||||
base_image=base_image,
|
||||
)
|
||||
|
||||
# Similar to the previous test - the compose function doesn't directly embed
|
||||
# the api_version or base_image into the compose file since those are handled
|
||||
# during the docker build process
|
||||
expected_compose_str = f"""volumes:
|
||||
langgraph-data:
|
||||
driver: local
|
||||
services:
|
||||
langgraph-redis:
|
||||
image: redis:6
|
||||
healthcheck:
|
||||
test: redis-cli ping
|
||||
interval: 5s
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
langgraph-postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
ports:
|
||||
- "5433:5432"
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
command:
|
||||
- postgres
|
||||
- -c
|
||||
- shared_preload_libraries=vector
|
||||
volumes:
|
||||
- langgraph-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: pg_isready -U postgres
|
||||
start_period: 10s
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
interval: 5s
|
||||
langgraph-api:
|
||||
ports:
|
||||
- "{port}:8000"
|
||||
depends_on:
|
||||
langgraph-redis:
|
||||
condition: service_healthy
|
||||
langgraph-postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
REDIS_URI: redis://langgraph-redis:6379
|
||||
POSTGRES_URI: {DEFAULT_POSTGRES_URI}"""
|
||||
assert clean_empty_lines(actual_compose_str) == expected_compose_str
|
||||
|
||||
|
||||
def test_compose_with_api_version_and_custom_postgres():
|
||||
"""Test compose function with api_version and custom postgres URI."""
|
||||
port = 8123
|
||||
api_version = "0.2.74"
|
||||
custom_postgres_uri = "postgresql://user:pass@external-db:5432/mydb"
|
||||
|
||||
actual_compose_str = compose(
|
||||
DEFAULT_DOCKER_CAPABILITIES,
|
||||
port=port,
|
||||
api_version=api_version,
|
||||
postgres_uri=custom_postgres_uri,
|
||||
)
|
||||
|
||||
expected_compose_str = f"""services:
|
||||
langgraph-redis:
|
||||
image: redis:6
|
||||
healthcheck:
|
||||
test: redis-cli ping
|
||||
interval: 5s
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
langgraph-api:
|
||||
ports:
|
||||
- "{port}:8000"
|
||||
depends_on:
|
||||
langgraph-redis:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
REDIS_URI: redis://langgraph-redis:6379
|
||||
POSTGRES_URI: {custom_postgres_uri}"""
|
||||
assert clean_empty_lines(actual_compose_str) == expected_compose_str
|
||||
|
||||
|
||||
def test_compose_with_api_version_and_debugger():
|
||||
"""Test compose function with api_version and debugger port."""
|
||||
port = 8123
|
||||
debugger_port = 8001
|
||||
api_version = "0.2.74"
|
||||
|
||||
actual_compose_str = compose(
|
||||
DEFAULT_DOCKER_CAPABILITIES,
|
||||
port=port,
|
||||
api_version=api_version,
|
||||
debugger_port=debugger_port,
|
||||
)
|
||||
|
||||
expected_compose_str = f"""volumes:
|
||||
langgraph-data:
|
||||
driver: local
|
||||
services:
|
||||
langgraph-redis:
|
||||
image: redis:6
|
||||
healthcheck:
|
||||
test: redis-cli ping
|
||||
interval: 5s
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
langgraph-postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
ports:
|
||||
- "5433:5432"
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
command:
|
||||
- postgres
|
||||
- -c
|
||||
- shared_preload_libraries=vector
|
||||
volumes:
|
||||
- langgraph-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: pg_isready -U postgres
|
||||
start_period: 10s
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
interval: 5s
|
||||
langgraph-debugger:
|
||||
image: langchain/langgraph-debugger
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
langgraph-postgres:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "{debugger_port}:3968"
|
||||
langgraph-api:
|
||||
ports:
|
||||
- "{port}:8000"
|
||||
depends_on:
|
||||
langgraph-redis:
|
||||
condition: service_healthy
|
||||
langgraph-postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
REDIS_URI: redis://langgraph-redis:6379
|
||||
POSTGRES_URI: {DEFAULT_POSTGRES_URI}"""
|
||||
assert clean_empty_lines(actual_compose_str) == expected_compose_str
|
||||
|
||||
Generated
+1
-1
@@ -531,7 +531,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-cli"
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
|
||||
@@ -11,7 +11,7 @@ from bench.react_agent import react_agent
|
||||
from bench.sequential import create_sequential
|
||||
from bench.wide_dict import wide_dict
|
||||
from bench.wide_state import wide_state
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.pregel import Pregel
|
||||
|
||||
@@ -26,7 +26,7 @@ async def arun(graph: Pregel, input: dict):
|
||||
"configurable": {"thread_id": str(uuid4())},
|
||||
"recursion_limit": 1000000000,
|
||||
},
|
||||
checkpoint_during=False,
|
||||
durability="exit",
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -43,7 +43,7 @@ async def arun_first_event_latency(graph: Pregel, input: dict) -> None:
|
||||
"configurable": {"thread_id": str(uuid4())},
|
||||
"recursion_limit": 1000000000,
|
||||
},
|
||||
checkpoint_during=False,
|
||||
durability="exit",
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -63,7 +63,7 @@ def run(graph: Pregel, input: dict):
|
||||
"configurable": {"thread_id": str(uuid4())},
|
||||
"recursion_limit": 1000000000,
|
||||
},
|
||||
checkpoint_during=False,
|
||||
durability="exit",
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -80,7 +80,7 @@ def run_first_event_latency(graph: Pregel, input: dict) -> None:
|
||||
"configurable": {"thread_id": str(uuid4())},
|
||||
"recursion_limit": 1000000000,
|
||||
},
|
||||
checkpoint_during=False,
|
||||
durability="exit",
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -108,8 +108,8 @@ benchmarks = (
|
||||
),
|
||||
(
|
||||
"fanout_to_subgraph_10x_checkpoint",
|
||||
fanout_to_subgraph().compile(checkpointer=MemorySaver()),
|
||||
fanout_to_subgraph_sync().compile(checkpointer=MemorySaver()),
|
||||
fanout_to_subgraph().compile(checkpointer=InMemorySaver()),
|
||||
fanout_to_subgraph_sync().compile(checkpointer=InMemorySaver()),
|
||||
{
|
||||
"subjects": [
|
||||
random.choices("abcdefghijklmnopqrstuvwxyz", k=1000) for _ in range(10)
|
||||
@@ -128,8 +128,8 @@ benchmarks = (
|
||||
),
|
||||
(
|
||||
"fanout_to_subgraph_100x_checkpoint",
|
||||
fanout_to_subgraph().compile(checkpointer=MemorySaver()),
|
||||
fanout_to_subgraph_sync().compile(checkpointer=MemorySaver()),
|
||||
fanout_to_subgraph().compile(checkpointer=InMemorySaver()),
|
||||
fanout_to_subgraph_sync().compile(checkpointer=InMemorySaver()),
|
||||
{
|
||||
"subjects": [
|
||||
random.choices("abcdefghijklmnopqrstuvwxyz", k=1000) for _ in range(100)
|
||||
@@ -144,8 +144,8 @@ benchmarks = (
|
||||
),
|
||||
(
|
||||
"react_agent_10x_checkpoint",
|
||||
react_agent(10, checkpointer=MemorySaver()),
|
||||
react_agent(10, checkpointer=MemorySaver()),
|
||||
react_agent(10, checkpointer=InMemorySaver()),
|
||||
react_agent(10, checkpointer=InMemorySaver()),
|
||||
{"messages": [HumanMessage("hi?")]},
|
||||
),
|
||||
(
|
||||
@@ -156,8 +156,8 @@ benchmarks = (
|
||||
),
|
||||
(
|
||||
"react_agent_100x_checkpoint",
|
||||
react_agent(100, checkpointer=MemorySaver()),
|
||||
react_agent(100, checkpointer=MemorySaver()),
|
||||
react_agent(100, checkpointer=InMemorySaver()),
|
||||
react_agent(100, checkpointer=InMemorySaver()),
|
||||
{"messages": [HumanMessage("hi?")]},
|
||||
),
|
||||
(
|
||||
@@ -178,8 +178,8 @@ benchmarks = (
|
||||
),
|
||||
(
|
||||
"wide_state_25x300_checkpoint",
|
||||
wide_state(300).compile(checkpointer=MemorySaver()),
|
||||
wide_state(300).compile(checkpointer=MemorySaver()),
|
||||
wide_state(300).compile(checkpointer=InMemorySaver()),
|
||||
wide_state(300).compile(checkpointer=InMemorySaver()),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
@@ -210,8 +210,8 @@ benchmarks = (
|
||||
),
|
||||
(
|
||||
"wide_state_15x600_checkpoint",
|
||||
wide_state(600).compile(checkpointer=MemorySaver()),
|
||||
wide_state(600).compile(checkpointer=MemorySaver()),
|
||||
wide_state(600).compile(checkpointer=InMemorySaver()),
|
||||
wide_state(600).compile(checkpointer=InMemorySaver()),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
@@ -242,8 +242,8 @@ benchmarks = (
|
||||
),
|
||||
(
|
||||
"wide_state_9x1200_checkpoint",
|
||||
wide_state(1200).compile(checkpointer=MemorySaver()),
|
||||
wide_state(1200).compile(checkpointer=MemorySaver()),
|
||||
wide_state(1200).compile(checkpointer=InMemorySaver()),
|
||||
wide_state(1200).compile(checkpointer=InMemorySaver()),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
@@ -274,8 +274,8 @@ benchmarks = (
|
||||
),
|
||||
(
|
||||
"wide_dict_25x300_checkpoint",
|
||||
wide_dict(300).compile(checkpointer=MemorySaver()),
|
||||
wide_dict(300).compile(checkpointer=MemorySaver()),
|
||||
wide_dict(300).compile(checkpointer=InMemorySaver()),
|
||||
wide_dict(300).compile(checkpointer=InMemorySaver()),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
@@ -306,8 +306,8 @@ benchmarks = (
|
||||
),
|
||||
(
|
||||
"wide_dict_15x600_checkpoint",
|
||||
wide_dict(600).compile(checkpointer=MemorySaver()),
|
||||
wide_dict(600).compile(checkpointer=MemorySaver()),
|
||||
wide_dict(600).compile(checkpointer=InMemorySaver()),
|
||||
wide_dict(600).compile(checkpointer=InMemorySaver()),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
@@ -338,8 +338,8 @@ benchmarks = (
|
||||
),
|
||||
(
|
||||
"wide_dict_9x1200_checkpoint",
|
||||
wide_dict(1200).compile(checkpointer=MemorySaver()),
|
||||
wide_dict(1200).compile(checkpointer=MemorySaver()),
|
||||
wide_dict(1200).compile(checkpointer=InMemorySaver()),
|
||||
wide_dict(1200).compile(checkpointer=InMemorySaver()),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
@@ -382,8 +382,8 @@ benchmarks = (
|
||||
),
|
||||
(
|
||||
"pydantic_state_25x300_checkpoint",
|
||||
pydantic_state(300).compile(checkpointer=MemorySaver()),
|
||||
pydantic_state(300).compile(checkpointer=MemorySaver()),
|
||||
pydantic_state(300).compile(checkpointer=InMemorySaver()),
|
||||
pydantic_state(300).compile(checkpointer=InMemorySaver()),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
@@ -414,8 +414,8 @@ benchmarks = (
|
||||
),
|
||||
(
|
||||
"pydantic_state_15x600_checkpoint",
|
||||
pydantic_state(600).compile(checkpointer=MemorySaver()),
|
||||
pydantic_state(600).compile(checkpointer=MemorySaver()),
|
||||
pydantic_state(600).compile(checkpointer=InMemorySaver()),
|
||||
pydantic_state(600).compile(checkpointer=InMemorySaver()),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
@@ -446,8 +446,8 @@ benchmarks = (
|
||||
),
|
||||
(
|
||||
"pydantic_state_9x1200_checkpoint",
|
||||
pydantic_state(1200).compile(checkpointer=MemorySaver()),
|
||||
pydantic_state(1200).compile(checkpointer=MemorySaver()),
|
||||
pydantic_state(1200).compile(checkpointer=InMemorySaver()),
|
||||
pydantic_state(1200).compile(checkpointer=InMemorySaver()),
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
|
||||
@@ -3,8 +3,9 @@ from typing import Annotated
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from langgraph.constants import END, START, Send
|
||||
from langgraph.constants import END, START
|
||||
from langgraph.graph.state import StateGraph
|
||||
from langgraph.types import Send
|
||||
|
||||
|
||||
def fanout_to_subgraph() -> StateGraph:
|
||||
@@ -114,9 +115,9 @@ if __name__ == "__main__":
|
||||
|
||||
import uvloop
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
graph = fanout_to_subgraph().compile(checkpointer=MemorySaver())
|
||||
graph = fanout_to_subgraph().compile(checkpointer=InMemorySaver())
|
||||
input = {
|
||||
"subjects": [
|
||||
random.choices("abcdefghijklmnopqrstuvwxyz", k=1000) for _ in range(1000)
|
||||
|
||||
@@ -304,9 +304,9 @@ if __name__ == "__main__":
|
||||
|
||||
import uvloop
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
graph = pydantic_state(1000).compile(checkpointer=MemorySaver())
|
||||
graph = pydantic_state(1000).compile(checkpointer=InMemorySaver())
|
||||
input = {
|
||||
"messages": [
|
||||
{
|
||||
|
||||
@@ -68,9 +68,9 @@ if __name__ == "__main__":
|
||||
|
||||
import uvloop
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
graph = react_agent(100, checkpointer=MemorySaver())
|
||||
graph = react_agent(100, checkpointer=InMemorySaver())
|
||||
input = {"messages": [HumanMessage("hi?")]}
|
||||
config = {"configurable": {"thread_id": "1"}, "recursion_limit": 20000000000}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Create a sequential no-op graph consisting of a few hundred nodes."""
|
||||
|
||||
from langgraph._internal._runnable import RunnableCallable
|
||||
from langgraph.graph import MessagesState, StateGraph
|
||||
from langgraph.utils.runnable import RunnableCallable
|
||||
|
||||
|
||||
def create_sequential(number_nodes: int) -> StateGraph:
|
||||
|
||||
@@ -130,9 +130,9 @@ if __name__ == "__main__":
|
||||
|
||||
import uvloop
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
graph = wide_dict(1000).compile(checkpointer=MemorySaver())
|
||||
graph = wide_dict(1000).compile(checkpointer=InMemorySaver())
|
||||
input = {
|
||||
"messages": [
|
||||
{
|
||||
|
||||
@@ -140,9 +140,9 @@ if __name__ == "__main__":
|
||||
|
||||
import uvloop
|
||||
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
graph = wide_state(1000).compile(checkpointer=MemorySaver())
|
||||
graph = wide_state(1000).compile(checkpointer=InMemorySaver())
|
||||
input = {
|
||||
"messages": [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"""Internal modules for LangGraph.
|
||||
|
||||
This module is not part of the public API, and thus stability is not guaranteed.
|
||||
"""
|
||||
@@ -0,0 +1,322 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import ChainMap
|
||||
from collections.abc import Sequence
|
||||
from os import getenv
|
||||
from typing import Any, cast
|
||||
|
||||
from langchain_core.callbacks import (
|
||||
AsyncCallbackManager,
|
||||
BaseCallbackManager,
|
||||
CallbackManager,
|
||||
Callbacks,
|
||||
)
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langchain_core.runnables.config import (
|
||||
CONFIG_KEYS,
|
||||
COPIABLE_KEYS,
|
||||
var_child_runnable_config,
|
||||
)
|
||||
|
||||
from langgraph._internal._constants import (
|
||||
CONF,
|
||||
CONFIG_KEY_CHECKPOINT_ID,
|
||||
CONFIG_KEY_CHECKPOINT_MAP,
|
||||
CONFIG_KEY_CHECKPOINT_NS,
|
||||
NS_END,
|
||||
NS_SEP,
|
||||
)
|
||||
from langgraph.checkpoint.base import CheckpointMetadata
|
||||
|
||||
DEFAULT_RECURSION_LIMIT = int(getenv("LANGGRAPH_DEFAULT_RECURSION_LIMIT", "25"))
|
||||
|
||||
|
||||
def recast_checkpoint_ns(ns: str) -> str:
|
||||
"""Remove task IDs from checkpoint namespace.
|
||||
|
||||
Args:
|
||||
ns: The checkpoint namespace with task IDs.
|
||||
|
||||
Returns:
|
||||
str: The checkpoint namespace without task IDs.
|
||||
"""
|
||||
return NS_SEP.join(
|
||||
part.split(NS_END)[0] for part in ns.split(NS_SEP) if not part.isdigit()
|
||||
)
|
||||
|
||||
|
||||
def patch_configurable(
|
||||
config: RunnableConfig | None, patch: dict[str, Any]
|
||||
) -> RunnableConfig:
|
||||
if config is None:
|
||||
return {CONF: patch}
|
||||
elif CONF not in config:
|
||||
return {**config, CONF: patch}
|
||||
else:
|
||||
return {**config, CONF: {**config[CONF], **patch}}
|
||||
|
||||
|
||||
def patch_checkpoint_map(
|
||||
config: RunnableConfig | None, metadata: CheckpointMetadata | None
|
||||
) -> RunnableConfig:
|
||||
if config is None:
|
||||
return config
|
||||
elif parents := (metadata.get("parents") if metadata else None):
|
||||
conf = config[CONF]
|
||||
return patch_configurable(
|
||||
config,
|
||||
{
|
||||
CONFIG_KEY_CHECKPOINT_MAP: {
|
||||
**parents,
|
||||
conf[CONFIG_KEY_CHECKPOINT_NS]: conf[CONFIG_KEY_CHECKPOINT_ID],
|
||||
},
|
||||
},
|
||||
)
|
||||
else:
|
||||
return config
|
||||
|
||||
|
||||
def merge_configs(*configs: RunnableConfig | None) -> RunnableConfig:
|
||||
"""Merge multiple configs into one.
|
||||
|
||||
Args:
|
||||
*configs: The configs to merge.
|
||||
|
||||
Returns:
|
||||
RunnableConfig: The merged config.
|
||||
"""
|
||||
base: RunnableConfig = {}
|
||||
# Even though the keys aren't literals, this is correct
|
||||
# because both dicts are the same type
|
||||
for config in configs:
|
||||
if config is None:
|
||||
continue
|
||||
for key, value in config.items():
|
||||
if not value:
|
||||
continue
|
||||
if key == "metadata":
|
||||
if base_value := base.get(key):
|
||||
base[key] = {**base_value, **value} # type: ignore
|
||||
else:
|
||||
base[key] = value # type: ignore[literal-required]
|
||||
elif key == "tags":
|
||||
if base_value := base.get(key):
|
||||
base[key] = [*base_value, *value] # type: ignore
|
||||
else:
|
||||
base[key] = value # type: ignore[literal-required]
|
||||
elif key == CONF:
|
||||
if base_value := base.get(key):
|
||||
base[key] = {**base_value, **value} # type: ignore[dict-item]
|
||||
else:
|
||||
base[key] = value
|
||||
elif key == "callbacks":
|
||||
base_callbacks = base.get("callbacks")
|
||||
# callbacks can be either None, list[handler] or manager
|
||||
# so merging two callbacks values has 6 cases
|
||||
if isinstance(value, list):
|
||||
if base_callbacks is None:
|
||||
base["callbacks"] = value.copy()
|
||||
elif isinstance(base_callbacks, list):
|
||||
base["callbacks"] = base_callbacks + value
|
||||
else:
|
||||
# base_callbacks is a manager
|
||||
mngr = base_callbacks.copy()
|
||||
for callback in value:
|
||||
mngr.add_handler(callback, inherit=True)
|
||||
base["callbacks"] = mngr
|
||||
elif isinstance(value, BaseCallbackManager):
|
||||
# value is a manager
|
||||
if base_callbacks is None:
|
||||
base["callbacks"] = value.copy()
|
||||
elif isinstance(base_callbacks, list):
|
||||
mngr = value.copy()
|
||||
for callback in base_callbacks:
|
||||
mngr.add_handler(callback, inherit=True)
|
||||
base["callbacks"] = mngr
|
||||
else:
|
||||
# base_callbacks is also a manager
|
||||
base["callbacks"] = base_callbacks.merge(value)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
elif key == "recursion_limit":
|
||||
if config["recursion_limit"] != DEFAULT_RECURSION_LIMIT:
|
||||
base["recursion_limit"] = config["recursion_limit"]
|
||||
else:
|
||||
base[key] = config[key] # type: ignore[literal-required]
|
||||
if CONF not in base:
|
||||
base[CONF] = {}
|
||||
return base
|
||||
|
||||
|
||||
def patch_config(
|
||||
config: RunnableConfig | None,
|
||||
*,
|
||||
callbacks: Callbacks = None,
|
||||
recursion_limit: int | None = None,
|
||||
max_concurrency: int | None = None,
|
||||
run_name: str | None = None,
|
||||
configurable: dict[str, Any] | None = None,
|
||||
) -> RunnableConfig:
|
||||
"""Patch a config with new values.
|
||||
|
||||
Args:
|
||||
config: The config to patch.
|
||||
callbacks: The callbacks to set.
|
||||
Defaults to None.
|
||||
recursion_limit: The recursion limit to set.
|
||||
Defaults to None.
|
||||
max_concurrency: The max concurrency to set.
|
||||
Defaults to None.
|
||||
run_name: The run name to set. Defaults to None.
|
||||
configurable: The configurable to set.
|
||||
Defaults to None.
|
||||
|
||||
Returns:
|
||||
RunnableConfig: The patched config.
|
||||
"""
|
||||
config = config.copy() if config is not None else {}
|
||||
if callbacks is not None:
|
||||
# If we're replacing callbacks, we need to unset run_name
|
||||
# As that should apply only to the same run as the original callbacks
|
||||
config["callbacks"] = callbacks
|
||||
if "run_name" in config:
|
||||
del config["run_name"]
|
||||
if "run_id" in config:
|
||||
del config["run_id"]
|
||||
if recursion_limit is not None:
|
||||
config["recursion_limit"] = recursion_limit
|
||||
if max_concurrency is not None:
|
||||
config["max_concurrency"] = max_concurrency
|
||||
if run_name is not None:
|
||||
config["run_name"] = run_name
|
||||
if configurable is not None:
|
||||
config[CONF] = {**config.get(CONF, {}), **configurable}
|
||||
return config
|
||||
|
||||
|
||||
def get_callback_manager_for_config(
|
||||
config: RunnableConfig, tags: Sequence[str] | None = None
|
||||
) -> CallbackManager:
|
||||
"""Get a callback manager for a config.
|
||||
|
||||
Args:
|
||||
config: The config.
|
||||
|
||||
Returns:
|
||||
CallbackManager: The callback manager.
|
||||
"""
|
||||
from langchain_core.callbacks.manager import CallbackManager
|
||||
|
||||
# merge tags
|
||||
all_tags = config.get("tags")
|
||||
if all_tags is not None and tags is not None:
|
||||
all_tags = [*all_tags, *tags]
|
||||
elif tags is not None:
|
||||
all_tags = list(tags)
|
||||
# use existing callbacks if they exist
|
||||
if (callbacks := config.get("callbacks")) and isinstance(
|
||||
callbacks, CallbackManager
|
||||
):
|
||||
if all_tags:
|
||||
callbacks.add_tags(all_tags)
|
||||
if metadata := config.get("metadata"):
|
||||
callbacks.add_metadata(metadata)
|
||||
return callbacks
|
||||
else:
|
||||
# otherwise create a new manager
|
||||
return CallbackManager.configure(
|
||||
inheritable_callbacks=config.get("callbacks"),
|
||||
inheritable_tags=all_tags,
|
||||
inheritable_metadata=config.get("metadata"),
|
||||
)
|
||||
|
||||
|
||||
def get_async_callback_manager_for_config(
|
||||
config: RunnableConfig,
|
||||
tags: Sequence[str] | None = None,
|
||||
) -> AsyncCallbackManager:
|
||||
"""Get an async callback manager for a config.
|
||||
|
||||
Args:
|
||||
config: The config.
|
||||
|
||||
Returns:
|
||||
AsyncCallbackManager: The async callback manager.
|
||||
"""
|
||||
from langchain_core.callbacks.manager import AsyncCallbackManager
|
||||
|
||||
# merge tags
|
||||
all_tags = config.get("tags")
|
||||
if all_tags is not None and tags is not None:
|
||||
all_tags = [*all_tags, *tags]
|
||||
elif tags is not None:
|
||||
all_tags = list(tags)
|
||||
# use existing callbacks if they exist
|
||||
if (callbacks := config.get("callbacks")) and isinstance(
|
||||
callbacks, AsyncCallbackManager
|
||||
):
|
||||
if all_tags:
|
||||
callbacks.add_tags(all_tags)
|
||||
if metadata := config.get("metadata"):
|
||||
callbacks.add_metadata(metadata)
|
||||
return callbacks
|
||||
else:
|
||||
# otherwise create a new manager
|
||||
return AsyncCallbackManager.configure(
|
||||
inheritable_callbacks=config.get("callbacks"),
|
||||
inheritable_tags=all_tags,
|
||||
inheritable_metadata=config.get("metadata"),
|
||||
)
|
||||
|
||||
|
||||
def _is_not_empty(value: Any) -> bool:
|
||||
if isinstance(value, (list, tuple, dict)):
|
||||
return len(value) > 0
|
||||
else:
|
||||
return value is not None
|
||||
|
||||
|
||||
def ensure_config(*configs: RunnableConfig | None) -> RunnableConfig:
|
||||
"""Return a config with all keys, merging any provided configs.
|
||||
|
||||
Args:
|
||||
*configs: Configs to merge before ensuring defaults.
|
||||
|
||||
Returns:
|
||||
RunnableConfig: The merged and ensured config.
|
||||
"""
|
||||
empty = RunnableConfig(
|
||||
tags=[],
|
||||
metadata=ChainMap(),
|
||||
callbacks=None,
|
||||
recursion_limit=DEFAULT_RECURSION_LIMIT,
|
||||
configurable={},
|
||||
)
|
||||
if var_config := var_child_runnable_config.get():
|
||||
empty.update(
|
||||
{
|
||||
k: v.copy() if k in COPIABLE_KEYS else v # type: ignore[attr-defined]
|
||||
for k, v in var_config.items()
|
||||
if _is_not_empty(v)
|
||||
},
|
||||
)
|
||||
for config in configs:
|
||||
if config is None:
|
||||
continue
|
||||
for k, v in config.items():
|
||||
if _is_not_empty(v) and k in CONFIG_KEYS:
|
||||
if k == CONF:
|
||||
empty[k] = cast(dict, v).copy()
|
||||
else:
|
||||
empty[k] = v # type: ignore[literal-required]
|
||||
for k, v in config.items():
|
||||
if _is_not_empty(v) and k not in CONFIG_KEYS:
|
||||
empty[CONF][k] = v
|
||||
for key, value in empty[CONF].items():
|
||||
if (
|
||||
not key.startswith("__")
|
||||
and isinstance(value, (str, int, float, bool))
|
||||
and key not in empty["metadata"]
|
||||
):
|
||||
empty["metadata"][key] = value
|
||||
return empty
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Constants used for Pregel operations."""
|
||||
|
||||
import sys
|
||||
from typing import Literal, cast
|
||||
|
||||
# --- Reserved write keys ---
|
||||
INPUT = sys.intern("__input__")
|
||||
# for values passed as input to the graph
|
||||
INTERRUPT = sys.intern("__interrupt__")
|
||||
# for dynamic interrupts raised by nodes
|
||||
RESUME = sys.intern("__resume__")
|
||||
# for values passed to resume a node after an interrupt
|
||||
ERROR = sys.intern("__error__")
|
||||
# for errors raised by nodes
|
||||
NO_WRITES = sys.intern("__no_writes__")
|
||||
# marker to signal node didn't write anything
|
||||
TASKS = sys.intern("__pregel_tasks")
|
||||
# for Send objects returned by nodes/edges, corresponds to PUSH below
|
||||
RETURN = sys.intern("__return__")
|
||||
# for writes of a task where we simply record the return value
|
||||
PREVIOUS = sys.intern("__previous__")
|
||||
# the implicit branch that handles each node's Control values
|
||||
|
||||
|
||||
# --- Reserved cache namespaces ---
|
||||
CACHE_NS_WRITES = sys.intern("__pregel_ns_writes")
|
||||
# cache namespace for node writes
|
||||
|
||||
# --- Reserved config.configurable keys ---
|
||||
CONFIG_KEY_SEND = sys.intern("__pregel_send")
|
||||
# holds the `write` function that accepts writes to state/edges/reserved keys
|
||||
CONFIG_KEY_READ = sys.intern("__pregel_read")
|
||||
# holds the `read` function that returns a copy of the current state
|
||||
CONFIG_KEY_CALL = sys.intern("__pregel_call")
|
||||
# holds the `call` function that accepts a node/func, args and returns a future
|
||||
CONFIG_KEY_CHECKPOINTER = sys.intern("__pregel_checkpointer")
|
||||
# holds a `BaseCheckpointSaver` passed from parent graph to child graphs
|
||||
CONFIG_KEY_STREAM = sys.intern("__pregel_stream")
|
||||
# holds a `StreamProtocol` passed from parent graph to child graphs
|
||||
CONFIG_KEY_CACHE = sys.intern("__pregel_cache")
|
||||
# holds a `BaseCache` made available to subgraphs
|
||||
CONFIG_KEY_RESUMING = sys.intern("__pregel_resuming")
|
||||
# holds a boolean indicating if subgraphs should resume from a previous checkpoint
|
||||
CONFIG_KEY_TASK_ID = sys.intern("__pregel_task_id")
|
||||
# holds the task ID for the current task
|
||||
CONFIG_KEY_THREAD_ID = sys.intern("thread_id")
|
||||
# holds the thread ID for the current invocation
|
||||
CONFIG_KEY_CHECKPOINT_MAP = sys.intern("checkpoint_map")
|
||||
# holds a mapping of checkpoint_ns -> checkpoint_id for parent graphs
|
||||
CONFIG_KEY_CHECKPOINT_ID = sys.intern("checkpoint_id")
|
||||
# holds the current checkpoint_id, if any
|
||||
CONFIG_KEY_CHECKPOINT_NS = sys.intern("checkpoint_ns")
|
||||
# holds the current checkpoint_ns, "" for root graph
|
||||
CONFIG_KEY_NODE_FINISHED = sys.intern("__pregel_node_finished")
|
||||
# holds a callback to be called when a node is finished
|
||||
CONFIG_KEY_SCRATCHPAD = sys.intern("__pregel_scratchpad")
|
||||
# holds a mutable dict for temporary storage scoped to the current task
|
||||
CONFIG_KEY_RUNNER_SUBMIT = sys.intern("__pregel_runner_submit")
|
||||
# holds a function that receives tasks from runner, executes them and returns results
|
||||
CONFIG_KEY_DURABILITY = sys.intern("__pregel_durability")
|
||||
# holds the durability mode, one of "sync", "async", or "exit"
|
||||
CONFIG_KEY_RUNTIME = sys.intern("__pregel_runtime")
|
||||
# holds a `Runtime` instance with context, store, stream writer, etc.
|
||||
CONFIG_KEY_RESUME_MAP = sys.intern("__pregel_resume_map")
|
||||
# holds a mapping of task ns -> resume value for resuming tasks
|
||||
|
||||
# --- Other constants ---
|
||||
PUSH = sys.intern("__pregel_push")
|
||||
# denotes push-style tasks, ie. those created by Send objects
|
||||
PULL = sys.intern("__pregel_pull")
|
||||
# denotes pull-style tasks, ie. those triggered by edges
|
||||
NS_SEP = sys.intern("|")
|
||||
# for checkpoint_ns, separates each level (ie. graph|subgraph|subsubgraph)
|
||||
NS_END = sys.intern(":")
|
||||
# for checkpoint_ns, for each level, separates the namespace from the task_id
|
||||
CONF = cast(Literal["configurable"], sys.intern("configurable"))
|
||||
# key for the configurable dict in RunnableConfig
|
||||
NULL_TASK_ID = sys.intern("00000000-0000-0000-0000-000000000000")
|
||||
# the task_id to use for writes that are not associated with a task
|
||||
|
||||
# redefined to avoid circular import with langgraph.constants
|
||||
_TAG_HIDDEN = sys.intern("langsmith:hidden")
|
||||
|
||||
RESERVED = {
|
||||
_TAG_HIDDEN,
|
||||
# reserved write keys
|
||||
INPUT,
|
||||
INTERRUPT,
|
||||
RESUME,
|
||||
ERROR,
|
||||
NO_WRITES,
|
||||
# reserved config.configurable keys
|
||||
CONFIG_KEY_SEND,
|
||||
CONFIG_KEY_READ,
|
||||
CONFIG_KEY_CHECKPOINTER,
|
||||
CONFIG_KEY_STREAM,
|
||||
CONFIG_KEY_CHECKPOINT_MAP,
|
||||
CONFIG_KEY_RESUMING,
|
||||
CONFIG_KEY_TASK_ID,
|
||||
CONFIG_KEY_CHECKPOINT_MAP,
|
||||
CONFIG_KEY_CHECKPOINT_ID,
|
||||
CONFIG_KEY_CHECKPOINT_NS,
|
||||
CONFIG_KEY_RESUME_MAP,
|
||||
# other constants
|
||||
PUSH,
|
||||
PULL,
|
||||
NS_SEP,
|
||||
NS_END,
|
||||
CONF,
|
||||
}
|
||||
+1
-3
@@ -9,9 +9,7 @@ from typing import Annotated, Any, Optional, Union, get_type_hints
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import NotRequired, ReadOnly, Required, get_origin
|
||||
|
||||
# NOTE: this is redefined here separately from langgraph.constants
|
||||
# to avoid a circular import
|
||||
MISSING = object()
|
||||
from langgraph._internal._typing import MISSING
|
||||
|
||||
|
||||
def _is_optional_type(type_: Any) -> bool:
|
||||
@@ -128,6 +128,3 @@ class SyncQueue:
|
||||
return len(self._queue)
|
||||
|
||||
__class_getitem__ = classmethod(types.GenericAlias)
|
||||
|
||||
|
||||
__all__ = ["AsyncQueue", "SyncQueue"]
|
||||
@@ -0,0 +1,29 @@
|
||||
def default_retry_on(exc: Exception) -> bool:
|
||||
import httpx
|
||||
import requests
|
||||
|
||||
if isinstance(exc, ConnectionError):
|
||||
return True
|
||||
if isinstance(exc, httpx.HTTPStatusError):
|
||||
return 500 <= exc.response.status_code < 600
|
||||
if isinstance(exc, requests.HTTPError):
|
||||
return 500 <= exc.response.status_code < 600 if exc.response else True
|
||||
if isinstance(
|
||||
exc,
|
||||
(
|
||||
ValueError,
|
||||
TypeError,
|
||||
ArithmeticError,
|
||||
ImportError,
|
||||
LookupError,
|
||||
NameError,
|
||||
SyntaxError,
|
||||
RuntimeError,
|
||||
ReferenceError,
|
||||
StopIteration,
|
||||
StopAsyncIteration,
|
||||
OSError,
|
||||
),
|
||||
):
|
||||
return False
|
||||
return True
|
||||
@@ -0,0 +1,898 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import enum
|
||||
import inspect
|
||||
import sys
|
||||
from collections.abc import (
|
||||
AsyncIterator,
|
||||
Awaitable,
|
||||
Coroutine,
|
||||
Generator,
|
||||
Iterator,
|
||||
Sequence,
|
||||
)
|
||||
from contextlib import AsyncExitStack, contextmanager
|
||||
from contextvars import Context, Token, copy_context
|
||||
from functools import partial, wraps
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Optional,
|
||||
Protocol,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
from langchain_core.runnables.base import (
|
||||
Runnable,
|
||||
RunnableConfig,
|
||||
RunnableLambda,
|
||||
RunnableParallel,
|
||||
RunnableSequence,
|
||||
)
|
||||
from langchain_core.runnables.base import (
|
||||
RunnableLike as LCRunnableLike,
|
||||
)
|
||||
from langchain_core.runnables.config import (
|
||||
run_in_executor,
|
||||
var_child_runnable_config,
|
||||
)
|
||||
from langchain_core.runnables.utils import Input, Output
|
||||
from langchain_core.tracers.langchain import LangChainTracer
|
||||
from typing_extensions import TypeGuard
|
||||
|
||||
from langgraph._internal._config import (
|
||||
ensure_config,
|
||||
get_async_callback_manager_for_config,
|
||||
get_callback_manager_for_config,
|
||||
patch_config,
|
||||
)
|
||||
from langgraph._internal._constants import (
|
||||
CONF,
|
||||
CONFIG_KEY_RUNTIME,
|
||||
)
|
||||
from langgraph._internal._typing import MISSING
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import StreamWriter
|
||||
|
||||
try:
|
||||
from langchain_core.tracers._streaming import _StreamingCallbackHandler
|
||||
except ImportError:
|
||||
_StreamingCallbackHandler = None # type: ignore
|
||||
|
||||
|
||||
def _set_config_context(
|
||||
config: RunnableConfig, run: Any = None
|
||||
) -> Token[RunnableConfig | None]:
|
||||
"""Set the child Runnable config + tracing context.
|
||||
|
||||
Args:
|
||||
config: The config to set.
|
||||
"""
|
||||
config_token = var_child_runnable_config.set(config)
|
||||
if run is not None:
|
||||
from langsmith.run_helpers import _set_tracing_context
|
||||
|
||||
_set_tracing_context({"parent": run})
|
||||
return config_token
|
||||
|
||||
|
||||
def _unset_config_context(token: Token[RunnableConfig | None], run: Any = None) -> None:
|
||||
"""Set the child Runnable config + tracing context.
|
||||
|
||||
Args:
|
||||
token: The config token to reset.
|
||||
"""
|
||||
var_child_runnable_config.reset(token)
|
||||
if run is not None:
|
||||
from langsmith.run_helpers import _set_tracing_context
|
||||
|
||||
_set_tracing_context(
|
||||
{
|
||||
"parent": None,
|
||||
"project_name": None,
|
||||
"tags": None,
|
||||
"metadata": None,
|
||||
"enabled": None,
|
||||
"client": None,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def set_config_context(
|
||||
config: RunnableConfig, run: Any = None
|
||||
) -> Generator[Context, None, None]:
|
||||
"""Set the child Runnable config + tracing context.
|
||||
|
||||
Args:
|
||||
config: The config to set.
|
||||
"""
|
||||
ctx = copy_context()
|
||||
config_token = ctx.run(_set_config_context, config, run)
|
||||
try:
|
||||
yield ctx
|
||||
finally:
|
||||
ctx.run(_unset_config_context, config_token, run)
|
||||
|
||||
|
||||
# Before Python 3.11 native StrEnum is not available
|
||||
class StrEnum(str, enum.Enum):
|
||||
"""A string enum."""
|
||||
|
||||
|
||||
# Special type to denote any type is accepted
|
||||
ANY_TYPE = object()
|
||||
|
||||
ASYNCIO_ACCEPTS_CONTEXT = sys.version_info >= (3, 11)
|
||||
|
||||
# List of keyword arguments that can be injected into nodes / tasks / tools at runtime.
|
||||
# A named argument may appear multiple times if it appears with distinct types.
|
||||
KWARGS_CONFIG_KEYS: tuple[tuple[str, tuple[Any, ...], str, Any], ...] = (
|
||||
(
|
||||
"config",
|
||||
(RunnableConfig, "RunnableConfig", inspect.Parameter.empty),
|
||||
# for now, use config directly, eventually, will pop off of Runtime
|
||||
"N/A",
|
||||
inspect.Parameter.empty,
|
||||
),
|
||||
(
|
||||
"writer",
|
||||
(StreamWriter, "StreamWriter", inspect.Parameter.empty),
|
||||
"stream_writer",
|
||||
lambda _: None,
|
||||
),
|
||||
(
|
||||
"store",
|
||||
(
|
||||
BaseStore,
|
||||
"BaseStore",
|
||||
inspect.Parameter.empty,
|
||||
),
|
||||
"store",
|
||||
inspect.Parameter.empty,
|
||||
),
|
||||
(
|
||||
"store",
|
||||
(
|
||||
Optional[BaseStore],
|
||||
"Optional[BaseStore]",
|
||||
),
|
||||
"store",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"previous",
|
||||
(ANY_TYPE,),
|
||||
"previous",
|
||||
inspect.Parameter.empty,
|
||||
),
|
||||
(
|
||||
"runtime",
|
||||
(ANY_TYPE,),
|
||||
# we never hit this block, we just inject runtime directly
|
||||
"N/A",
|
||||
inspect.Parameter.empty,
|
||||
),
|
||||
)
|
||||
"""List of kwargs that can be passed to functions, and their corresponding
|
||||
config keys, default values and type annotations.
|
||||
|
||||
Used to configure keyword arguments that can be injected at runtime
|
||||
from the `Runtime` object as kwargs to `invoke`, `ainvoke`, `stream` and `astream`.
|
||||
|
||||
For a keyword to be injected from the config object, the function signature
|
||||
must contain a kwarg with the same name and a matching type annotation.
|
||||
|
||||
Each tuple contains:
|
||||
- the name of the kwarg in the function signature
|
||||
- the type annotation(s) for the kwarg
|
||||
- the `Runtime` attribute for fetching the value (N/A if not applicable)
|
||||
|
||||
This is fully internal and should be further refactored to use `get_type_hints`
|
||||
to resolve forward references and optional types formatted like BaseStore | None.
|
||||
"""
|
||||
|
||||
VALID_KINDS = (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
|
||||
|
||||
|
||||
class _RunnableWithWriter(Protocol[Input, Output]):
|
||||
def __call__(self, state: Input, *, writer: StreamWriter) -> Output: ...
|
||||
|
||||
|
||||
class _RunnableWithStore(Protocol[Input, Output]):
|
||||
def __call__(self, state: Input, *, store: BaseStore) -> Output: ...
|
||||
|
||||
|
||||
class _RunnableWithWriterStore(Protocol[Input, Output]):
|
||||
def __call__(
|
||||
self, state: Input, *, writer: StreamWriter, store: BaseStore
|
||||
) -> Output: ...
|
||||
|
||||
|
||||
class _RunnableWithConfigWriter(Protocol[Input, Output]):
|
||||
def __call__(
|
||||
self, state: Input, *, config: RunnableConfig, writer: StreamWriter
|
||||
) -> Output: ...
|
||||
|
||||
|
||||
class _RunnableWithConfigStore(Protocol[Input, Output]):
|
||||
def __call__(
|
||||
self, state: Input, *, config: RunnableConfig, store: BaseStore
|
||||
) -> Output: ...
|
||||
|
||||
|
||||
class _RunnableWithConfigWriterStore(Protocol[Input, Output]):
|
||||
def __call__(
|
||||
self,
|
||||
state: Input,
|
||||
*,
|
||||
config: RunnableConfig,
|
||||
writer: StreamWriter,
|
||||
store: BaseStore,
|
||||
) -> Output: ...
|
||||
|
||||
|
||||
RunnableLike = Union[
|
||||
LCRunnableLike,
|
||||
_RunnableWithWriter[Input, Output],
|
||||
_RunnableWithStore[Input, Output],
|
||||
_RunnableWithWriterStore[Input, Output],
|
||||
_RunnableWithConfigWriter[Input, Output],
|
||||
_RunnableWithConfigStore[Input, Output],
|
||||
_RunnableWithConfigWriterStore[Input, Output],
|
||||
]
|
||||
|
||||
|
||||
class RunnableCallable(Runnable):
|
||||
"""A much simpler version of RunnableLambda that requires sync and async functions."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[..., Any | Runnable] | None,
|
||||
afunc: Callable[..., Awaitable[Any | Runnable]] | None = None,
|
||||
*,
|
||||
name: str | None = None,
|
||||
tags: Sequence[str] | None = None,
|
||||
trace: bool = True,
|
||||
recurse: bool = True,
|
||||
explode_args: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.name = name
|
||||
if self.name is None:
|
||||
if func:
|
||||
try:
|
||||
if func.__name__ != "<lambda>":
|
||||
self.name = func.__name__
|
||||
except AttributeError:
|
||||
pass
|
||||
elif afunc:
|
||||
try:
|
||||
self.name = afunc.__name__
|
||||
except AttributeError:
|
||||
pass
|
||||
self.func = func
|
||||
self.afunc = afunc
|
||||
self.tags = tags
|
||||
self.kwargs = kwargs
|
||||
self.trace = trace
|
||||
self.recurse = recurse
|
||||
self.explode_args = explode_args
|
||||
# check signature
|
||||
if func is None and afunc is None:
|
||||
raise ValueError("At least one of func or afunc must be provided.")
|
||||
|
||||
self.func_accepts: dict[str, tuple[str, Any]] = {}
|
||||
params = inspect.signature(cast(Callable, func or afunc)).parameters
|
||||
|
||||
for kw, typ, runtime_key, default in KWARGS_CONFIG_KEYS:
|
||||
p = params.get(kw)
|
||||
|
||||
if p is None or p.kind not in VALID_KINDS:
|
||||
# If parameter is not found or is not a valid kind, skip
|
||||
continue
|
||||
|
||||
if typ != (ANY_TYPE,) and p.annotation not in typ:
|
||||
# A specific type is required, but the function annotation does
|
||||
# not match the expected type.
|
||||
continue
|
||||
|
||||
# If the kwarg is accepted by the function, store the key / runtime attribute to inject
|
||||
self.func_accepts[kw] = (runtime_key, default)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
repr_args = {
|
||||
k: v
|
||||
for k, v in self.__dict__.items()
|
||||
if k not in {"name", "func", "afunc", "config", "kwargs", "trace"}
|
||||
}
|
||||
return f"{self.get_name()}({', '.join(f'{k}={v!r}' for k, v in repr_args.items())})"
|
||||
|
||||
def invoke(
|
||||
self, input: Any, config: RunnableConfig | None = None, **kwargs: Any
|
||||
) -> Any:
|
||||
if self.func is None:
|
||||
raise TypeError(
|
||||
f'No synchronous function provided to "{self.name}".'
|
||||
"\nEither initialize with a synchronous function or invoke"
|
||||
" via the async API (ainvoke, astream, etc.)"
|
||||
)
|
||||
if config is None:
|
||||
config = ensure_config()
|
||||
if self.explode_args:
|
||||
args, _kwargs = input
|
||||
kwargs = {**self.kwargs, **_kwargs, **kwargs}
|
||||
else:
|
||||
args = (input,)
|
||||
kwargs = {**self.kwargs, **kwargs}
|
||||
|
||||
runtime = config[CONF].get(CONFIG_KEY_RUNTIME)
|
||||
|
||||
for kw, (runtime_key, default) in self.func_accepts.items():
|
||||
# If the kwarg is already set, use the set value
|
||||
if kw in kwargs:
|
||||
continue
|
||||
|
||||
kw_value: Any = MISSING
|
||||
if kw == "config":
|
||||
kw_value = config
|
||||
elif runtime:
|
||||
if kw == "runtime":
|
||||
kw_value = runtime
|
||||
else:
|
||||
try:
|
||||
kw_value = getattr(runtime, runtime_key)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if kw_value is MISSING:
|
||||
if default is inspect.Parameter.empty:
|
||||
raise ValueError(
|
||||
f"Missing required config key '{runtime_key}' for '{self.name}'."
|
||||
)
|
||||
kw_value = default
|
||||
kwargs[kw] = kw_value
|
||||
|
||||
if self.trace:
|
||||
callback_manager = get_callback_manager_for_config(config, self.tags)
|
||||
run_manager = callback_manager.on_chain_start(
|
||||
None,
|
||||
input,
|
||||
name=config.get("run_name") or self.get_name(),
|
||||
run_id=config.pop("run_id", None),
|
||||
)
|
||||
try:
|
||||
child_config = patch_config(config, callbacks=run_manager.get_child())
|
||||
# get the run
|
||||
for h in run_manager.handlers:
|
||||
if isinstance(h, LangChainTracer):
|
||||
run = h.run_map.get(str(run_manager.run_id))
|
||||
break
|
||||
else:
|
||||
run = None
|
||||
# run in context
|
||||
with set_config_context(child_config, run) as context:
|
||||
ret = context.run(self.func, *args, **kwargs)
|
||||
except BaseException as e:
|
||||
run_manager.on_chain_error(e)
|
||||
raise
|
||||
else:
|
||||
run_manager.on_chain_end(ret)
|
||||
else:
|
||||
ret = self.func(*args, **kwargs)
|
||||
if self.recurse and isinstance(ret, Runnable):
|
||||
return ret.invoke(input, config)
|
||||
return ret
|
||||
|
||||
async def ainvoke(
|
||||
self, input: Any, config: RunnableConfig | None = None, **kwargs: Any
|
||||
) -> Any:
|
||||
if not self.afunc:
|
||||
return self.invoke(input, config)
|
||||
if config is None:
|
||||
config = ensure_config()
|
||||
if self.explode_args:
|
||||
args, _kwargs = input
|
||||
kwargs = {**self.kwargs, **_kwargs, **kwargs}
|
||||
else:
|
||||
args = (input,)
|
||||
kwargs = {**self.kwargs, **kwargs}
|
||||
|
||||
runtime = config[CONF].get(CONFIG_KEY_RUNTIME)
|
||||
|
||||
for kw, (runtime_key, default) in self.func_accepts.items():
|
||||
# If the kwarg has already been set, use the set value
|
||||
if kw in kwargs:
|
||||
continue
|
||||
|
||||
kw_value: Any = MISSING
|
||||
if kw == "config":
|
||||
kw_value = config
|
||||
elif runtime:
|
||||
if kw == "runtime":
|
||||
kw_value = runtime
|
||||
else:
|
||||
try:
|
||||
kw_value = getattr(runtime, runtime_key)
|
||||
except AttributeError:
|
||||
pass
|
||||
if kw_value is MISSING:
|
||||
if default is inspect.Parameter.empty:
|
||||
raise ValueError(
|
||||
f"Missing required config key '{runtime_key}' for '{self.name}'."
|
||||
)
|
||||
kw_value = default
|
||||
kwargs[kw] = kw_value
|
||||
|
||||
if self.trace:
|
||||
callback_manager = get_async_callback_manager_for_config(config, self.tags)
|
||||
run_manager = await callback_manager.on_chain_start(
|
||||
None,
|
||||
input,
|
||||
name=config.get("run_name") or self.name,
|
||||
run_id=config.pop("run_id", None),
|
||||
)
|
||||
try:
|
||||
child_config = patch_config(config, callbacks=run_manager.get_child())
|
||||
coro = cast(Coroutine[None, None, Any], self.afunc(*args, **kwargs))
|
||||
if ASYNCIO_ACCEPTS_CONTEXT:
|
||||
for h in run_manager.handlers:
|
||||
if isinstance(h, LangChainTracer):
|
||||
run = h.run_map.get(str(run_manager.run_id))
|
||||
break
|
||||
else:
|
||||
run = None
|
||||
with set_config_context(child_config, run) as context:
|
||||
ret = await asyncio.create_task(coro, context=context)
|
||||
else:
|
||||
ret = await coro
|
||||
except BaseException as e:
|
||||
await run_manager.on_chain_error(e)
|
||||
raise
|
||||
else:
|
||||
await run_manager.on_chain_end(ret)
|
||||
else:
|
||||
ret = await self.afunc(*args, **kwargs)
|
||||
if self.recurse and isinstance(ret, Runnable):
|
||||
return await ret.ainvoke(input, config)
|
||||
return ret
|
||||
|
||||
|
||||
def is_async_callable(
|
||||
func: Any,
|
||||
) -> TypeGuard[Callable[..., Awaitable]]:
|
||||
"""Check if a function is async."""
|
||||
return (
|
||||
asyncio.iscoroutinefunction(func)
|
||||
or hasattr(func, "__call__")
|
||||
and asyncio.iscoroutinefunction(func.__call__)
|
||||
)
|
||||
|
||||
|
||||
def is_async_generator(
|
||||
func: Any,
|
||||
) -> TypeGuard[Callable[..., AsyncIterator]]:
|
||||
"""Check if a function is an async generator."""
|
||||
return (
|
||||
inspect.isasyncgenfunction(func)
|
||||
or hasattr(func, "__call__")
|
||||
and inspect.isasyncgenfunction(func.__call__)
|
||||
)
|
||||
|
||||
|
||||
def coerce_to_runnable(
|
||||
thing: RunnableLike, *, name: str | None, trace: bool
|
||||
) -> Runnable:
|
||||
"""Coerce a runnable-like object into a Runnable.
|
||||
|
||||
Args:
|
||||
thing: A runnable-like object.
|
||||
|
||||
Returns:
|
||||
A Runnable.
|
||||
"""
|
||||
if isinstance(thing, Runnable):
|
||||
return thing
|
||||
elif is_async_generator(thing) or inspect.isgeneratorfunction(thing):
|
||||
return RunnableLambda(thing, name=name)
|
||||
elif callable(thing):
|
||||
if is_async_callable(thing):
|
||||
return RunnableCallable(None, thing, name=name, trace=trace)
|
||||
else:
|
||||
return RunnableCallable(
|
||||
thing,
|
||||
wraps(thing)(partial(run_in_executor, None, thing)), # type: ignore[arg-type]
|
||||
name=name,
|
||||
trace=trace,
|
||||
)
|
||||
elif isinstance(thing, dict):
|
||||
return RunnableParallel(thing)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Expected a Runnable, callable or dict."
|
||||
f"Instead got an unsupported type: {type(thing)}"
|
||||
)
|
||||
|
||||
|
||||
class RunnableSeq(Runnable):
|
||||
"""Sequence of Runnables, where the output of each is the input of the next.
|
||||
|
||||
RunnableSeq is a simpler version of RunnableSequence that is internal to
|
||||
LangGraph.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*steps: RunnableLike,
|
||||
name: str | None = None,
|
||||
trace_inputs: Callable[[Any], Any] | None = None,
|
||||
) -> None:
|
||||
"""Create a new RunnableSeq.
|
||||
|
||||
Args:
|
||||
steps: The steps to include in the sequence.
|
||||
name: The name of the Runnable. Defaults to None.
|
||||
|
||||
Raises:
|
||||
ValueError: If the sequence has less than 2 steps.
|
||||
"""
|
||||
steps_flat: list[Runnable] = []
|
||||
for step in steps:
|
||||
if isinstance(step, RunnableSequence):
|
||||
steps_flat.extend(step.steps)
|
||||
elif isinstance(step, RunnableSeq):
|
||||
steps_flat.extend(step.steps)
|
||||
else:
|
||||
steps_flat.append(coerce_to_runnable(step, name=None, trace=True))
|
||||
if len(steps_flat) < 2:
|
||||
raise ValueError(
|
||||
f"RunnableSeq must have at least 2 steps, got {len(steps_flat)}"
|
||||
)
|
||||
self.steps = steps_flat
|
||||
self.name = name
|
||||
self.trace_inputs = trace_inputs
|
||||
|
||||
def __or__(
|
||||
self,
|
||||
other: Any,
|
||||
) -> Runnable:
|
||||
if isinstance(other, RunnableSequence):
|
||||
return RunnableSeq(
|
||||
*self.steps,
|
||||
other.first,
|
||||
*other.middle,
|
||||
other.last,
|
||||
name=self.name or other.name,
|
||||
)
|
||||
elif isinstance(other, RunnableSeq):
|
||||
return RunnableSeq(
|
||||
*self.steps,
|
||||
*other.steps,
|
||||
name=self.name or other.name,
|
||||
)
|
||||
else:
|
||||
return RunnableSeq(
|
||||
*self.steps,
|
||||
coerce_to_runnable(other, name=None, trace=True),
|
||||
name=self.name,
|
||||
)
|
||||
|
||||
def __ror__(
|
||||
self,
|
||||
other: Any,
|
||||
) -> Runnable:
|
||||
if isinstance(other, RunnableSequence):
|
||||
return RunnableSequence(
|
||||
other.first,
|
||||
*other.middle,
|
||||
other.last,
|
||||
*self.steps,
|
||||
name=other.name or self.name,
|
||||
)
|
||||
elif isinstance(other, RunnableSeq):
|
||||
return RunnableSeq(
|
||||
*other.steps,
|
||||
*self.steps,
|
||||
name=other.name or self.name,
|
||||
)
|
||||
else:
|
||||
return RunnableSequence(
|
||||
coerce_to_runnable(other, name=None, trace=True),
|
||||
*self.steps,
|
||||
name=self.name,
|
||||
)
|
||||
|
||||
def invoke(
|
||||
self, input: Input, config: RunnableConfig | None = None, **kwargs: Any
|
||||
) -> Any:
|
||||
if config is None:
|
||||
config = ensure_config()
|
||||
# setup callbacks and context
|
||||
callback_manager = get_callback_manager_for_config(config)
|
||||
# start the root run
|
||||
run_manager = callback_manager.on_chain_start(
|
||||
None,
|
||||
self.trace_inputs(input) if self.trace_inputs is not None else input,
|
||||
name=config.get("run_name") or self.get_name(),
|
||||
run_id=config.pop("run_id", None),
|
||||
)
|
||||
# invoke all steps in sequence
|
||||
try:
|
||||
for i, step in enumerate(self.steps):
|
||||
# mark each step as a child run
|
||||
config = patch_config(
|
||||
config, callbacks=run_manager.get_child(f"seq:step:{i + 1}")
|
||||
)
|
||||
# 1st step is the actual node,
|
||||
# others are writers which don't need to be run in context
|
||||
if i == 0:
|
||||
# get the run object
|
||||
for h in run_manager.handlers:
|
||||
if isinstance(h, LangChainTracer):
|
||||
run = h.run_map.get(str(run_manager.run_id))
|
||||
break
|
||||
else:
|
||||
run = None
|
||||
# run in context
|
||||
with set_config_context(config, run) as context:
|
||||
input = context.run(step.invoke, input, config, **kwargs)
|
||||
else:
|
||||
input = step.invoke(input, config)
|
||||
# finish the root run
|
||||
except BaseException as e:
|
||||
run_manager.on_chain_error(e)
|
||||
raise
|
||||
else:
|
||||
run_manager.on_chain_end(input)
|
||||
return input
|
||||
|
||||
async def ainvoke(
|
||||
self,
|
||||
input: Input,
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any | None,
|
||||
) -> Any:
|
||||
if config is None:
|
||||
config = ensure_config()
|
||||
# setup callbacks
|
||||
callback_manager = get_async_callback_manager_for_config(config)
|
||||
# start the root run
|
||||
run_manager = await callback_manager.on_chain_start(
|
||||
None,
|
||||
self.trace_inputs(input) if self.trace_inputs is not None else input,
|
||||
name=config.get("run_name") or self.get_name(),
|
||||
run_id=config.pop("run_id", None),
|
||||
)
|
||||
|
||||
# invoke all steps in sequence
|
||||
try:
|
||||
for i, step in enumerate(self.steps):
|
||||
# mark each step as a child run
|
||||
config = patch_config(
|
||||
config, callbacks=run_manager.get_child(f"seq:step:{i + 1}")
|
||||
)
|
||||
# 1st step is the actual node,
|
||||
# others are writers which don't need to be run in context
|
||||
if i == 0:
|
||||
if ASYNCIO_ACCEPTS_CONTEXT:
|
||||
# get the run object
|
||||
for h in run_manager.handlers:
|
||||
if isinstance(h, LangChainTracer):
|
||||
run = h.run_map.get(str(run_manager.run_id))
|
||||
break
|
||||
else:
|
||||
run = None
|
||||
# run in context
|
||||
with set_config_context(config, run) as context:
|
||||
input = await asyncio.create_task(
|
||||
step.ainvoke(input, config, **kwargs), context=context
|
||||
)
|
||||
else:
|
||||
input = await step.ainvoke(input, config, **kwargs)
|
||||
else:
|
||||
input = await step.ainvoke(input, config)
|
||||
# finish the root run
|
||||
except BaseException as e:
|
||||
await run_manager.on_chain_error(e)
|
||||
raise
|
||||
else:
|
||||
await run_manager.on_chain_end(input)
|
||||
return input
|
||||
|
||||
def stream(
|
||||
self,
|
||||
input: Input,
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any | None,
|
||||
) -> Iterator[Any]:
|
||||
if config is None:
|
||||
config = ensure_config()
|
||||
# setup callbacks
|
||||
callback_manager = get_callback_manager_for_config(config)
|
||||
# start the root run
|
||||
run_manager = callback_manager.on_chain_start(
|
||||
None,
|
||||
self.trace_inputs(input) if self.trace_inputs is not None else input,
|
||||
name=config.get("run_name") or self.get_name(),
|
||||
run_id=config.pop("run_id", None),
|
||||
)
|
||||
# get the run object
|
||||
for h in run_manager.handlers:
|
||||
if isinstance(h, LangChainTracer):
|
||||
run = h.run_map.get(str(run_manager.run_id))
|
||||
break
|
||||
else:
|
||||
run = None
|
||||
# create first step config
|
||||
config = patch_config(
|
||||
config,
|
||||
callbacks=run_manager.get_child(f"seq:step:{1}"),
|
||||
)
|
||||
# run all in context
|
||||
with set_config_context(config, run) as context:
|
||||
try:
|
||||
# stream the last steps
|
||||
# transform the input stream of each step with the next
|
||||
# steps that don't natively support transforming an input stream will
|
||||
# buffer input in memory until all available, and then start emitting output
|
||||
for idx, step in enumerate(self.steps):
|
||||
if idx == 0:
|
||||
iterator = step.stream(input, config, **kwargs)
|
||||
else:
|
||||
config = patch_config(
|
||||
config,
|
||||
callbacks=run_manager.get_child(f"seq:step:{idx + 1}"),
|
||||
)
|
||||
iterator = step.transform(iterator, config)
|
||||
# populates streamed_output in astream_log() output if needed
|
||||
if _StreamingCallbackHandler is not None:
|
||||
for h in run_manager.handlers:
|
||||
if isinstance(h, _StreamingCallbackHandler):
|
||||
iterator = h.tap_output_iter(run_manager.run_id, iterator)
|
||||
# consume into final output
|
||||
output = context.run(_consume_iter, iterator)
|
||||
# sequence doesn't emit output, yield to mark as generator
|
||||
yield
|
||||
except BaseException as e:
|
||||
run_manager.on_chain_error(e)
|
||||
raise
|
||||
else:
|
||||
run_manager.on_chain_end(output)
|
||||
|
||||
async def astream(
|
||||
self,
|
||||
input: Input,
|
||||
config: RunnableConfig | None = None,
|
||||
**kwargs: Any | None,
|
||||
) -> AsyncIterator[Any]:
|
||||
if config is None:
|
||||
config = ensure_config()
|
||||
# setup callbacks
|
||||
callback_manager = get_async_callback_manager_for_config(config)
|
||||
# start the root run
|
||||
run_manager = await callback_manager.on_chain_start(
|
||||
None,
|
||||
self.trace_inputs(input) if self.trace_inputs is not None else input,
|
||||
name=config.get("run_name") or self.get_name(),
|
||||
run_id=config.pop("run_id", None),
|
||||
)
|
||||
# stream the last steps
|
||||
# transform the input stream of each step with the next
|
||||
# steps that don't natively support transforming an input stream will
|
||||
# buffer input in memory until all available, and then start emitting output
|
||||
if ASYNCIO_ACCEPTS_CONTEXT:
|
||||
# get the run object
|
||||
for h in run_manager.handlers:
|
||||
if isinstance(h, LangChainTracer):
|
||||
run = h.run_map.get(str(run_manager.run_id))
|
||||
break
|
||||
else:
|
||||
run = None
|
||||
# create first step config
|
||||
config = patch_config(
|
||||
config,
|
||||
callbacks=run_manager.get_child(f"seq:step:{1}"),
|
||||
)
|
||||
# run all in context
|
||||
with set_config_context(config, run) as context:
|
||||
try:
|
||||
async with AsyncExitStack() as stack:
|
||||
for idx, step in enumerate(self.steps):
|
||||
if idx == 0:
|
||||
aiterator = step.astream(input, config, **kwargs)
|
||||
else:
|
||||
config = patch_config(
|
||||
config,
|
||||
callbacks=run_manager.get_child(
|
||||
f"seq:step:{idx + 1}"
|
||||
),
|
||||
)
|
||||
aiterator = step.atransform(aiterator, config)
|
||||
if hasattr(aiterator, "aclose"):
|
||||
stack.push_async_callback(aiterator.aclose)
|
||||
# populates streamed_output in astream_log() output if needed
|
||||
if _StreamingCallbackHandler is not None:
|
||||
for h in run_manager.handlers:
|
||||
if isinstance(h, _StreamingCallbackHandler):
|
||||
aiterator = h.tap_output_aiter(
|
||||
run_manager.run_id, aiterator
|
||||
)
|
||||
# consume into final output
|
||||
output = await asyncio.create_task(
|
||||
_consume_aiter(aiterator), context=context
|
||||
)
|
||||
# sequence doesn't emit output, yield to mark as generator
|
||||
yield
|
||||
except BaseException as e:
|
||||
await run_manager.on_chain_error(e)
|
||||
raise
|
||||
else:
|
||||
await run_manager.on_chain_end(output)
|
||||
else:
|
||||
try:
|
||||
async with AsyncExitStack() as stack:
|
||||
for idx, step in enumerate(self.steps):
|
||||
config = patch_config(
|
||||
config,
|
||||
callbacks=run_manager.get_child(f"seq:step:{idx + 1}"),
|
||||
)
|
||||
if idx == 0:
|
||||
aiterator = step.astream(input, config, **kwargs)
|
||||
else:
|
||||
aiterator = step.atransform(aiterator, config)
|
||||
if hasattr(aiterator, "aclose"):
|
||||
stack.push_async_callback(aiterator.aclose)
|
||||
# populates streamed_output in astream_log() output if needed
|
||||
if _StreamingCallbackHandler is not None:
|
||||
for h in run_manager.handlers:
|
||||
if isinstance(h, _StreamingCallbackHandler):
|
||||
aiterator = h.tap_output_aiter(
|
||||
run_manager.run_id, aiterator
|
||||
)
|
||||
# consume into final output
|
||||
output = await _consume_aiter(aiterator)
|
||||
# sequence doesn't emit output, yield to mark as generator
|
||||
yield
|
||||
except BaseException as e:
|
||||
await run_manager.on_chain_error(e)
|
||||
raise
|
||||
else:
|
||||
await run_manager.on_chain_end(output)
|
||||
|
||||
|
||||
def _consume_iter(it: Iterator[Any]) -> Any:
|
||||
"""Consume an iterator."""
|
||||
output: Any = None
|
||||
add_supported = False
|
||||
for chunk in it:
|
||||
# collect final output
|
||||
if output is None:
|
||||
output = chunk
|
||||
elif add_supported:
|
||||
try:
|
||||
output = output + chunk
|
||||
except TypeError:
|
||||
output = chunk
|
||||
add_supported = False
|
||||
else:
|
||||
output = chunk
|
||||
return output
|
||||
|
||||
|
||||
async def _consume_aiter(it: AsyncIterator[Any]) -> Any:
|
||||
"""Consume an async iterator."""
|
||||
output: Any = None
|
||||
add_supported = False
|
||||
async for chunk in it:
|
||||
# collect final output
|
||||
if add_supported:
|
||||
try:
|
||||
output = output + chunk
|
||||
except TypeError:
|
||||
output = chunk
|
||||
add_supported = False
|
||||
else:
|
||||
output = chunk
|
||||
return output
|
||||
@@ -42,13 +42,13 @@ It can either be a `TypedDict`, `dataclass`, or Pydantic `BaseModel`.
|
||||
Note: we cannot use either `TypedDict` or `dataclass` directly due to limitations in type checking.
|
||||
"""
|
||||
|
||||
|
||||
class Unset:
|
||||
"""A sentinel value to represent an unset type."""
|
||||
|
||||
|
||||
UNSET: Unset = Unset()
|
||||
MISSING = object()
|
||||
"""Unset sentinel value."""
|
||||
|
||||
|
||||
class DeprecatedKwargs(TypedDict):
|
||||
"""TypedDict to use for extra keyword arguments, enabling type checking warnings for deprecated arguments."""
|
||||
|
||||
|
||||
EMPTY_SEQ: tuple[str, ...] = tuple()
|
||||
"""An empty sequence of strings."""
|
||||
@@ -1,15 +1,27 @@
|
||||
from langgraph.channels.any_value import AnyValue
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.channels.binop import BinaryOperatorAggregate
|
||||
from langgraph.channels.ephemeral_value import EphemeralValue
|
||||
from langgraph.channels.last_value import LastValue
|
||||
from langgraph.channels.last_value import LastValue, LastValueAfterFinish
|
||||
from langgraph.channels.named_barrier_value import (
|
||||
NamedBarrierValue,
|
||||
NamedBarrierValueAfterFinish,
|
||||
)
|
||||
from langgraph.channels.topic import Topic
|
||||
from langgraph.channels.untracked_value import UntrackedValue
|
||||
|
||||
__all__ = [
|
||||
__all__ = (
|
||||
# base
|
||||
"BaseChannel",
|
||||
# value types
|
||||
"AnyValue",
|
||||
"LastValue",
|
||||
"Topic",
|
||||
"BinaryOperatorAggregate",
|
||||
"LastValueAfterFinish",
|
||||
"UntrackedValue",
|
||||
"EphemeralValue",
|
||||
"AnyValue",
|
||||
]
|
||||
"BinaryOperatorAggregate",
|
||||
"NamedBarrierValue",
|
||||
"NamedBarrierValueAfterFinish",
|
||||
# topics
|
||||
"Topic",
|
||||
)
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Generic
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph._internal._typing import MISSING
|
||||
from langgraph.channels.base import BaseChannel, Value
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError
|
||||
|
||||
__all__ = ("AnyValue",)
|
||||
|
||||
|
||||
class AnyValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
"""Stores the last value received, assumes that if multiple values are
|
||||
@@ -14,6 +18,8 @@ class AnyValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
|
||||
__slots__ = ("typ", "value")
|
||||
|
||||
value: Value | Any
|
||||
|
||||
def __init__(self, typ: Any, key: str = "") -> None:
|
||||
super().__init__(typ, key)
|
||||
self.value = MISSING
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError, InvalidUpdateError
|
||||
from langgraph._internal._typing import MISSING
|
||||
from langgraph.errors import EmptyChannelError
|
||||
|
||||
Value = TypeVar("Value")
|
||||
Update = TypeVar("Update")
|
||||
C = TypeVar("C")
|
||||
Checkpoint = TypeVar("Checkpoint")
|
||||
|
||||
__all__ = ("BaseChannel",)
|
||||
|
||||
|
||||
class BaseChannel(Generic[Value, Update, C], ABC):
|
||||
class BaseChannel(Generic[Value, Update, Checkpoint], ABC):
|
||||
"""Base class for all channels."""
|
||||
|
||||
__slots__ = ("key", "typ")
|
||||
@@ -39,7 +43,7 @@ class BaseChannel(Generic[Value, Update, C], ABC):
|
||||
Subclasses can override this method with a more efficient implementation."""
|
||||
return self.from_checkpoint(self.checkpoint())
|
||||
|
||||
def checkpoint(self) -> C:
|
||||
def checkpoint(self) -> Checkpoint | Any:
|
||||
"""Return a serializable representation of the channel's current state.
|
||||
Raises EmptyChannelError if the channel is empty (never updated yet),
|
||||
or doesn't support checkpoints."""
|
||||
@@ -49,7 +53,7 @@ class BaseChannel(Generic[Value, Update, C], ABC):
|
||||
return MISSING
|
||||
|
||||
@abstractmethod
|
||||
def from_checkpoint(self, checkpoint: C) -> Self:
|
||||
def from_checkpoint(self, checkpoint: Checkpoint | Any) -> Self:
|
||||
"""Return a new identical channel, optionally initialized from a checkpoint.
|
||||
If the checkpoint contains complex data structures, they should be copied."""
|
||||
|
||||
@@ -99,10 +103,3 @@ class BaseChannel(Generic[Value, Update, C], ABC):
|
||||
Returns True if the channel was updated, False otherwise.
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BaseChannel",
|
||||
"EmptyChannelError",
|
||||
"InvalidUpdateError",
|
||||
]
|
||||
|
||||
@@ -4,10 +4,12 @@ from typing import Callable, Generic
|
||||
|
||||
from typing_extensions import NotRequired, Required, Self
|
||||
|
||||
from langgraph._internal._typing import MISSING
|
||||
from langgraph.channels.base import BaseChannel, Value
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError
|
||||
|
||||
__all__ = ("BinaryOperatorAggregate",)
|
||||
|
||||
|
||||
# Adapted from typing_extensions
|
||||
def _strip_extras(t): # type: ignore[no-untyped-def]
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Generic
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph._internal._typing import MISSING
|
||||
from langgraph.channels.base import BaseChannel, Value
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError, InvalidUpdateError
|
||||
|
||||
__all__ = ("EphemeralValue",)
|
||||
|
||||
|
||||
class EphemeralValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
"""Stores the value received in the step immediately preceding, clears after."""
|
||||
|
||||
__slots__ = ("value", "guard")
|
||||
|
||||
value: Value | Any
|
||||
guard: bool
|
||||
|
||||
def __init__(self, typ: Any, guard: bool = True) -> None:
|
||||
super().__init__(typ)
|
||||
self.guard = guard
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user