mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-18 05:35:43 +02:00
Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c403f34c8 | ||
|
|
9bd78ed483 | ||
|
|
a77db7d73d | ||
|
|
e85b7e6cd9 | ||
|
|
94fa46f9fa | ||
|
|
e50110ba91 | ||
|
|
fd64ada9de | ||
|
|
b15ec09c3b | ||
|
|
5e9e7b79fe | ||
|
|
c49a077789 | ||
|
|
881b07cf7f | ||
|
|
0425d4e65d | ||
|
|
118016a21c | ||
|
|
bda3c3add9 | ||
|
|
067b99c789 | ||
|
|
673cc5ad1a | ||
|
|
e9e9a96a0d | ||
|
|
cf7136297e | ||
|
|
e5aae80e3a | ||
|
|
f629f68ec3 | ||
|
|
575de221fe | ||
|
|
d2fc5f0a0f | ||
|
|
8e4b8b11ff | ||
|
|
2808a7859a | ||
|
|
fb3c61ea4f | ||
|
|
69cecd872c | ||
|
|
9abc1c8174 | ||
|
|
de90ced29d | ||
|
|
b8b973fc0c | ||
|
|
89e3709a2a | ||
|
|
a7f012a19c | ||
|
|
9b05ab6453 | ||
|
|
5401d2ea81 | ||
|
|
4401612aa6 | ||
|
|
49bb08a3f9 | ||
|
|
4f2e9b838f | ||
|
|
d30da72f6e | ||
|
|
7f079adfee | ||
|
|
4c74af606f | ||
|
|
900824089b | ||
|
|
522caa643f | ||
|
|
ef71656f05 | ||
|
|
3ba7c7fbed | ||
|
|
5cdab86d48 | ||
|
|
f5fe7e5195 | ||
|
|
2b728410e9 | ||
|
|
30221da4a8 | ||
|
|
1c5a354a7d | ||
|
|
bfd271e00e | ||
|
|
9647b1e55f | ||
|
|
e4aa204110 | ||
|
|
2c29edadec | ||
|
|
96847e644b | ||
|
|
4b102638c2 | ||
|
|
7021ce3742 | ||
|
|
d0c0aa9697 | ||
|
|
520de30350 | ||
|
|
81c0d47363 | ||
|
|
87603d8a00 | ||
|
|
7ad7329c7d | ||
|
|
e981d27f84 | ||
|
|
71db4f2ad5 | ||
|
|
4ced277e2d | ||
|
|
34738fa566 | ||
|
|
0286c38784 | ||
|
|
dd733a3389 |
@@ -99,7 +99,7 @@ jobs:
|
||||
env:
|
||||
LANGCHAIN_API_KEY: test
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "schedule" ] || [ "${{ github.event_name }}" == "workflow_dispatch" ] || ([ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == "refs/heads/main" ]); then
|
||||
if [ "${{ github.event_name }}" == "schedule" ]; then
|
||||
echo "Running link check on all HTML files matching notebooks in docs directory..."
|
||||
poetry run pytest -v \
|
||||
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
|
||||
|
||||
@@ -36,3 +36,6 @@ packages:
|
||||
- name: "langgraph-reflection"
|
||||
repo: "langchain-ai/langgraph-reflection"
|
||||
description: "LangGraph agent that runs a reflection step."
|
||||
- name: "langgraph-codeact"
|
||||
repo: "langchain-ai/langgraph-codeact"
|
||||
description: "LangGraph implementation of CodeAct agent that generates and executes code instead of tool calling."
|
||||
|
||||
@@ -10,14 +10,17 @@ This list of companies using LangGraph and their success stories is compiled fro
|
||||
| [Athena Intelligence](https://www.athenaintel.com/) | Software & Technology (GenAI Native) | Research & summarization | [Case study, 2024](https://blog.langchain.dev/customers-athena-intelligence/) |
|
||||
| [Captide](https://www.captide.co/) | Software & Technology (GenAI Native) | Data extraction | [Case study, 2025](https://blog.langchain.dev/how-captide-is-redefining-equity-research-with-agentic-workflows-built-on-langgraph-and-langsmith/) |
|
||||
| [Cisco Outshift](https://outshift.cisco.com/) | Software & Technology | DevOps | [Blog post, 2025](https://outshift.cisco.com/blog/build-react-agent-application-for-devops-tasks-using-rest-apis) |
|
||||
| [C.H. Robinson](https://www.chrobinson.com/en-us/) | Logistics | Automation | [Case study, 2025](https://blog.langchain.dev/customers-chrobinson/) |
|
||||
| [Elastic](https://www.elastic.co/) | Software & Technology | Copilot for domain-specific task | [Blog post, 2025](https://www.elastic.co/blog/elastic-security-generative-ai-features) |
|
||||
| [GitLab](https://about.gitlab.com/) | Software & Technology | Code generation | [Duo workflow docs](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/duo_workflow/) |
|
||||
| [Inconvo](https://inconvo.ai/?ref=blog.langchain.dev) | Software & Technology | Code generation | [Case study, 2025](https://blog.langchain.dev/customers-inconvo/) |
|
||||
| [Infor](https://infor.com/) | Software & Technology | GenAI embedded product experiences; customer support; copilot | [Case study, 2025](https://blog.langchain.dev/customers-infor/) |
|
||||
| [Klarna](https://www.klarna.com/) | Fintech | Copilot for domain-specific task | [Case study, 2025](https://blog.langchain.dev/customers-klarna/) |
|
||||
| [Komodo Health](https://www.komodohealth.com/) | Healthcare | Copilot for domain-specific task | [Blog post](https://www.komodohealth.com/perspectives/new-gen-ai-assistant-empowers-the-enterprise/) |
|
||||
| [LinkedIn](https://www.linkedin.com/) | Social Media | Code generation; Search & discovery | [Blog post, 2025](https://www.linkedin.com/blog/engineering/ai/practical-text-to-sql-for-data-analytics); [Blog post, 2024](https://www.linkedin.com/blog/engineering/generative-ai/behind-the-platform-the-journey-to-create-the-linkedin-genai-application-tech-stack) |
|
||||
| [Minimal](https://gominimal.ai/) | E-commerce | Customer support | [Case study, 2025](https://blog.langchain.dev/how-minimal-built-a-multi-agent-customer-support-system-with-langgraph-langsmith/) |
|
||||
| [OpenRecovery](https://www.openrecovery.com/) | Healthcare | Copilot for domain-specific task | [Case study, 2024](https://blog.langchain.dev/customers-openrecovery/) |
|
||||
| [Qodo](https://www.qodo.ai/) | Software & Technology (GenAI Native) | Code generation | [Blog post, 2025](https://www.qodo.ai/blog/why-we-chose-langgraph-to-build-our-coding-agent/) |
|
||||
| [Rakuten](https://www.rakuten.com/) | E-commerce / Fintech | Copilot for domain-specific task | [Blog post, 2025](https://rakuten.today/blog/from-ai-hype-to-real-world-tools-rakuten-teams-up-with-langchain.html) |
|
||||
| [Replit](https://replit.com/) | Software & Technology | Code generation | [Blog post, 2024](https://blog.langchain.dev/customers-replit/); [Breakout agent story, 2024](https://www.langchain.com/breakoutagents/replit); [Fireside chat video, 2024](https://www.youtube.com/watch?v=ViykMqljjxU) |
|
||||
| [Rexera](https://www.rexera.com/) | Real Estate (GenAI Native) | Copilot for domain-specific task | [Case study, 2024](https://blog.langchain.dev/customers-rexera/) |
|
||||
@@ -25,3 +28,4 @@ This list of companies using LangGraph and their success stories is compiled fro
|
||||
| [Uber](https://www.uber.com/) | Transportation | Developer productivity; Code generation | [Presentation, 2024](https://dpe.org/sessions/ty-smith-adam-huda/this-year-in-ubers-ai-driven-developer-productivity-revolution/); [Video, 2024](https://www.youtube.com/watch?v=8rkA5vWUE4Y) |
|
||||
| [Unify](https://www.unifygtm.com/) | Software & Technology (GenAI Native) | Copilot for domain-specific task | [Blog post, 2024](https://blog.langchain.dev/unify-launches-agents-for-account-qualification-using-langgraph-and-langsmith/) |
|
||||
| [Vizient](https://www.vizientinc.com/) | Healthcare | Copilot for domain-specific task | [Case study, 2025](https://blog.langchain.dev/p/3d2cd58c-13a5-4df9-bd84-7d54ed0ed82c/) |
|
||||
| [Vodafone](https://www.vodafone.com/) | Telecommunications | Code generation; internal search | [Case study, 2025](https://blog.langchain.dev/customers-vodafone/) |
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Testing local agents with remote traces
|
||||
|
||||
## Overview
|
||||
|
||||
A common workflow when debugging production-deployed agents is to test the same thread against a local version of the same agent, which may have modifications.
|
||||
|
||||
To support this, LangGraph Studio, in combination with LangSmith, allows you to clone remote threads traced in LangSmith into your locally running agent. This cloned thread can then be used to re-run specific nodes within Studio.
|
||||
|
||||
## Requirements
|
||||
|
||||
!!! info "Prerequisites"
|
||||
|
||||
- langgraph>=0.3.18
|
||||
- langgraph-api>=0.0.32
|
||||
|
||||
- A thread traced in LangSmith.
|
||||
- A locally running agent. See [here](../../how-tos/local-studio.md) for setup instructions.
|
||||
- Note that your local agent must be using the above specified `langgraph` and `langgraph-api` versions.
|
||||
- The nodes present in the remote trace must exist in at least one of the graphs in your local agent.
|
||||
|
||||
## Cloning Thread
|
||||
|
||||
First navigate to the LangSmith trace. Here you should see a button to "Run in Studio".
|
||||
|
||||
{width=1200}
|
||||
|
||||
This will prompt you to enter the url that your locally running agent is accessible at. Once provided, select "Clone thread locally". If you have multiple graphs in your agent, you will also be prompted to select a graph to clone this thread under.
|
||||
|
||||
Once selected, a will a new thread in your local agent will be created and the thread history will be reconstruced to reflect the original trace.
|
||||
|
||||
Alternatively, if your trace originates from an agent deployed on LangGraph Platform, you can "View original thread" to open Studio with the actual deployed thread.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
@@ -1,8 +1,6 @@
|
||||
# How to integrate LangGraph into your React application
|
||||
|
||||
!!! info "Prerequisites"
|
||||
- [LangGraph Platform](../../concepts/langgraph_platform.md)
|
||||
- [LangGraph Server](../../concepts/langgraph_server.md)
|
||||
!!! info "Prerequisites" - [LangGraph Platform](../../concepts/langgraph_platform.md) - [LangGraph Server](../../concepts/langgraph_server.md)
|
||||
|
||||
The `useStream()` React hook provides a seamless way to integrate LangGraph into your React applications. It handles all the complexities of streaming, state management, and branching logic, letting you focus on building great chat experiences.
|
||||
|
||||
@@ -169,10 +167,7 @@ The `useStream()` hook exposes the `interrupt` property, which will be filled wi
|
||||
Learn more about interrupts in the [How to handle interrupts](../../how-tos/human_in_the_loop/wait-user-input.ipynb) guide.
|
||||
|
||||
```tsx
|
||||
const thread = useStream<
|
||||
{ messages: Message[] },
|
||||
{ InterruptType: string }
|
||||
>({
|
||||
const thread = useStream<{ messages: Message[] }, { InterruptType: string }>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
@@ -182,7 +177,6 @@ if (thread.interrupt) {
|
||||
return (
|
||||
<div>
|
||||
Interrupted! {thread.interrupt.value}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -313,7 +307,7 @@ export default function App() {
|
||||
onEdit={(message) =>
|
||||
thread.submit(
|
||||
{ messages: [message] },
|
||||
{ checkpoint: parentCheckpoint },
|
||||
{ checkpoint: parentCheckpoint }
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -370,6 +364,33 @@ export default function App() {
|
||||
|
||||
For advanced use cases you can use the `experimental_branchTree` property to get the tree representation of the thread, which can be used to render branching controls for non-message based graphs.
|
||||
|
||||
### Optimistic Updates
|
||||
|
||||
You can optimistically update the client state before performing a network request to the agent, allowing you to provide immediate feedback to the user, such as showing the user message immediately before the agent has seen the request.
|
||||
|
||||
```tsx
|
||||
const stream = useStream({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
});
|
||||
|
||||
const handleSubmit = (text: string) => {
|
||||
const newMessage = { type: "human" as const, content: text };
|
||||
|
||||
stream.submit(
|
||||
{ messages: [newMessage] },
|
||||
{
|
||||
optimisticValues(prev) {
|
||||
const prevMessages = prev.messages ?? [];
|
||||
const newMessages = [...prevMessages, newMessage];
|
||||
return { ...prev, messages: newMessages };
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### TypeScript
|
||||
|
||||
The `useStream()` hook is friendly for apps written in TypeScript and you can specify types for the state to get better type safety and IDE support.
|
||||
@@ -397,21 +418,23 @@ You can also optionally specify types for different scenarios, such as:
|
||||
- `UpdateType`: Type for the submit function (default: `Partial<State>`)
|
||||
|
||||
```tsx
|
||||
|
||||
const thread = useStream<State, {
|
||||
UpdateType: {
|
||||
messages: Message[] | Message;
|
||||
context?: Record<string, unknown>;
|
||||
};
|
||||
InterruptType: string;
|
||||
CustomEventType: {
|
||||
type: "progress" | "debug";
|
||||
payload: unknown;
|
||||
};
|
||||
ConfigurableType: {
|
||||
model: string;
|
||||
};
|
||||
}>({
|
||||
const thread = useStream<
|
||||
State,
|
||||
{
|
||||
UpdateType: {
|
||||
messages: Message[] | Message;
|
||||
context?: Record<string, unknown>;
|
||||
};
|
||||
InterruptType: string;
|
||||
CustomEventType: {
|
||||
type: "progress" | "debug";
|
||||
payload: unknown;
|
||||
};
|
||||
ConfigurableType: {
|
||||
model: string;
|
||||
};
|
||||
}
|
||||
>({
|
||||
apiUrl: "http://localhost:2024",
|
||||
assistantId: "agent",
|
||||
messagesKey: "messages",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"description": "A run is an invocation of a graph / assistant, with no state or memory persistence."
|
||||
},
|
||||
{
|
||||
"name": "Crons (Enterprise-only)",
|
||||
"name": "Crons (Plus tier)",
|
||||
"description": "A cron is a periodic run that recurs on a given schedule. The repeats can be isolated, or share state in a thread"
|
||||
},
|
||||
{
|
||||
@@ -805,6 +805,58 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/threads/state/bulk": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Threads"
|
||||
],
|
||||
"summary": "Bulk Update Thread State",
|
||||
"description": "Create a new thread from a batch of state updates.",
|
||||
"operationId": "bulk_update_thread_state_post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ThreadStateBulkUpdate"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Thread"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/threads/{thread_id}/state": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -1342,6 +1394,21 @@
|
||||
},
|
||||
"name": "offset",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"pending",
|
||||
"error",
|
||||
"success",
|
||||
"timeout",
|
||||
"interrupted"
|
||||
]
|
||||
},
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -1458,7 +1525,7 @@
|
||||
"/threads/{thread_id}/runs/crons": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Crons (Enterprise-only)"
|
||||
"Crons (Plus tier)"
|
||||
],
|
||||
"summary": "Create Thread Cron",
|
||||
"description": "Create a cron to schedule runs on a thread.",
|
||||
@@ -1836,6 +1903,17 @@
|
||||
},
|
||||
"name": "run_id",
|
||||
"in": "path"
|
||||
},
|
||||
{
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"title": "Cancel on Disconnect",
|
||||
"description": "If true, the run will be cancelled if the client disconnects.",
|
||||
"default": false
|
||||
},
|
||||
"name": "cancel_on_disconnect",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -2032,7 +2110,7 @@
|
||||
"/runs/crons": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Crons (Enterprise-only)"
|
||||
"Crons (Plus tier)"
|
||||
],
|
||||
"summary": "Create Cron",
|
||||
"description": "Create a cron to schedule runs on new threads.",
|
||||
@@ -2084,7 +2162,7 @@
|
||||
"/runs/crons/search": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Crons (Enterprise-only)"
|
||||
"Crons (Plus tier)"
|
||||
],
|
||||
"summary": "Search Crons",
|
||||
"description": "Search all active crons",
|
||||
@@ -2190,6 +2268,68 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/runs/cancel": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Thread Runs"
|
||||
],
|
||||
"summary": "Cancel Runs",
|
||||
"description": "Cancel one or more runs. Can cancel runs by thread ID and run IDs, or by status filter.",
|
||||
"operationId": "cancel_runs_post",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Action to take when cancelling the run. Possible values are `interrupt` or `rollback`. `interrupt` will simply cancel the run. `rollback` will cancel the run and delete the run and associated checkpoints afterwards.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"interrupt",
|
||||
"rollback"
|
||||
],
|
||||
"title": "Action",
|
||||
"default": "interrupt"
|
||||
},
|
||||
"name": "action",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RunsCancel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Success - Runs cancelled"
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/runs/wait": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -2373,7 +2513,7 @@
|
||||
"/runs/crons/{cron_id}": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Crons (Enterprise-only)"
|
||||
"Crons (Plus tier)"
|
||||
],
|
||||
"summary": "Delete Cron",
|
||||
"description": "Delete a cron by ID.",
|
||||
@@ -2936,7 +3076,7 @@
|
||||
"type": "string",
|
||||
"maxLength": 65536,
|
||||
"minLength": 1,
|
||||
"format": "uri",
|
||||
"format": "uri-reference",
|
||||
"title": "Webhook",
|
||||
"description": "Webhook to call after LangGraph API call is done."
|
||||
},
|
||||
@@ -3216,7 +3356,11 @@
|
||||
"description": "The command to run.",
|
||||
"properties": {
|
||||
"update": {
|
||||
"type": "object",
|
||||
"type": [
|
||||
"object",
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"title": "Update",
|
||||
"description": "An update to the state."
|
||||
},
|
||||
@@ -3226,12 +3370,13 @@
|
||||
"array",
|
||||
"number",
|
||||
"string",
|
||||
"boolean",
|
||||
"null"
|
||||
],
|
||||
"title": "Resume",
|
||||
"description": "A value to pass to an interrupted node."
|
||||
},
|
||||
"send": {
|
||||
"goto": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Send"
|
||||
@@ -3242,10 +3387,21 @@
|
||||
"$ref": "#/components/schemas/Send"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
],
|
||||
"title": "Goto",
|
||||
"description": "Name of the node(s) to navigate to next or node(s) to be executed with a provided input."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3276,6 +3432,18 @@
|
||||
{
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
@@ -3326,7 +3494,7 @@
|
||||
"type": "string",
|
||||
"maxLength": 65536,
|
||||
"minLength": 1,
|
||||
"format": "uri",
|
||||
"format": "uri-reference",
|
||||
"title": "Webhook",
|
||||
"description": "Webhook to call after LangGraph API call is done."
|
||||
},
|
||||
@@ -3491,6 +3659,18 @@
|
||||
{
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
@@ -3541,7 +3721,7 @@
|
||||
"type": "string",
|
||||
"maxLength": 65536,
|
||||
"minLength": 1,
|
||||
"format": "uri",
|
||||
"format": "uri-reference",
|
||||
"title": "Webhook",
|
||||
"description": "Webhook to call after LangGraph API call is done."
|
||||
},
|
||||
@@ -3840,6 +4020,36 @@
|
||||
"title": "If Exists",
|
||||
"description": "How to handle duplicate creation. Must be either 'raise' (raise error if duplicate), or 'do_nothing' (return existing thread).",
|
||||
"default": "raise"
|
||||
},
|
||||
"ttl": {
|
||||
"type": "object",
|
||||
"title": "TTL",
|
||||
"description": "The time-to-live for the thread.",
|
||||
"properties": {
|
||||
"strategy": {
|
||||
"type": "string",
|
||||
"enum": ["delete"],
|
||||
"description": "The TTL strategy. 'delete' removes the entire thread.",
|
||||
"default": "delete"
|
||||
},
|
||||
"ttl": {
|
||||
"type": "number",
|
||||
"description": "The time-to-live in minutes from now until thread should be swept."
|
||||
}
|
||||
}
|
||||
},
|
||||
"supersteps": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"updates": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/ThreadSuperstepUpdate" }
|
||||
}
|
||||
},
|
||||
"required": ["updates"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -4028,6 +4238,43 @@
|
||||
"title": "ThreadStateUpdate",
|
||||
"description": "Payload for updating the state of a thread."
|
||||
},
|
||||
"ThreadSuperstepUpdate": {
|
||||
"properties": {
|
||||
"values": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"command": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Command"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "The command associated with the update."
|
||||
},
|
||||
"as_node": {
|
||||
"type": "string",
|
||||
"description": "Update the state as if this node had just executed."
|
||||
}
|
||||
},
|
||||
"required": ["as_node"],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadStateUpdateResponse": {
|
||||
"properties": {
|
||||
"checkpoint": {
|
||||
@@ -4230,6 +4477,42 @@
|
||||
},
|
||||
"description": "Represents a single document or data entry in the graph's Store. Items are used to store cross-thread memories."
|
||||
},
|
||||
"RunsCancel": {
|
||||
"type": "object",
|
||||
"title": "RunsCancel",
|
||||
"description": "Payload for cancelling runs.",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["pending", "running", "all"],
|
||||
"title": "Status",
|
||||
"description": "Filter runs by status to cancel. Must be one of 'pending', 'running', or 'all'."
|
||||
},
|
||||
"thread_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Thread Id",
|
||||
"description": "The ID of the thread containing runs to cancel."
|
||||
},
|
||||
"run_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"title": "Run Ids",
|
||||
"description": "List of run IDs to cancel."
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"required": ["status"]
|
||||
},
|
||||
{
|
||||
"required": ["thread_id", "run_ids"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"SearchItemsResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -42,12 +42,12 @@ The LangGraph CLI requires a JSON configuration file with the following keys:
|
||||
|
||||
| Key | Description |
|
||||
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <span style="white-space: nowrap;">`dependencies`</span> | **Required**. Array of dependencies for LangGraph Cloud API server. Dependencies can be one of the following: (1) `"."`, which will look for local Python packages, (2) `pyproject.toml`, `setup.py` or `requirements.txt` in the app directory `"./local_package"`, or (3) a package name. |
|
||||
| <span style="white-space: nowrap;">`dependencies`</span> | **Required**. Array of dependencies for LangGraph Cloud API server. Dependencies can be one of the following: <ul><li>A single period (`"."`), which will look for local Python packages.</li><li>The directory path where `pyproject.toml`, `setup.py` or `requirements.txt` is located.</br></br>For example, if `requirements.txt` is located in the root of the project directory, specify `"./"`. If it's located in a subdirectory called `local_package`, specify `"./local_package"`. Do not specify the string `"requirements.txt"` itself.</li><li>A Python package name.</li></ul> |
|
||||
| <span style="white-space: nowrap;">`graphs`</span> | **Required**. Mapping from graph ID to path where the compiled graph or a function that makes a graph is defined. Example: <ul><li>`./your_package/your_file.py:variable`, where `variable` is an instance of `langgraph.graph.state.CompiledStateGraph`</li><li>`./your_package/your_file.py:make_graph`, where `make_graph` is a function that takes a config dictionary (`langchain_core.runnables.RunnableConfig`) and creates an instance of `langgraph.graph.state.StateGraph` / `langgraph.graph.state.CompiledStateGraph`.</li></ul> |
|
||||
| <span style="white-space: nowrap;">`auth`</span> | _(Added in v0.0.11)_ Auth configuration containing the path to your authentication handler. Example: `./your_package/auth.py:auth`, where `auth` is an instance of `langgraph_sdk.Auth`. See [authentication guide](../../concepts/auth.md) for details. |
|
||||
| <span style="white-space: nowrap;">`env`</span> | Path to `.env` file or a mapping from environment variable to its value. |
|
||||
| <span style="white-space: nowrap;">`store`</span> | Configuration for adding semantic search to the BaseStore. Contains the following fields: <ul><li>`index`: Configuration for semantic search indexing with fields:<ul><li>`embed`: Embedding provider (e.g., "openai:text-embedding-3-small") or path to custom embedding function</li><li>`dims`: Dimension size of the embedding model. Used to initialize the vector table.</li><li>`fields` (optional): List of fields to index. Defaults to `["$"]`, which means to index entire documents. Can be specific fields like `["text", "summary", "some.value"]`</li></ul></li></ul> |
|
||||
| <span style="white-space: nowrap;">`python_version`</span> | `3.11` or `3.12`. Defaults to `3.11`. |
|
||||
| <span style="white-space: nowrap;">`python_version`</span> | `3.11`, `3.12`, or `3.13`. Defaults to `3.11`. |
|
||||
| <span style="white-space: nowrap;">`node_version`</span> | Specify `node_version: 20` to use LangGraph.js. |
|
||||
| <span style="white-space: nowrap;">`pip_config_file`</span> | Path to `pip` config file. |
|
||||
| <span style="white-space: nowrap;">`dockerfile_lines`</span> | Array of additional lines to add to Dockerfile following the import from parent image. |
|
||||
|
||||
@@ -2,6 +2,22 @@
|
||||
|
||||
The LangGraph Cloud Server supports specific environment variables for configuring a deployment.
|
||||
|
||||
## `BG_JOB_ISOLATED_LOOPS`
|
||||
|
||||
Set `BG_JOB_ISOLATED_LOOPS` to `True` to execute background runs in an isolated event loop separate from the serving API event loop.
|
||||
|
||||
This environment variable should be set to `True` if the implementation of a graph/node contains synchronous code. In this situation, the synchronous code will block the serving API event loop, which may cause the API to be unavailable. A symptom of an unavailable API is continuous application restarts due to failing health checks.
|
||||
|
||||
Defaults to `False`.
|
||||
|
||||
## `BG_JOB_TIMEOUT_SECS`
|
||||
|
||||
The timeout of a background run can be increased. However, the infrastructure for a Cloud SaaS deployment enforces a 1 hour timeout limit for API requests. This means the connection between client and server will timeout after 1 hour. This is not configurable.
|
||||
|
||||
A background run can execute for longer than 1 hour, but a client must reconnect to the server (e.g. join stream via `POST /threads/{thread_id}/runs/{run_id}/stream`) to retrieve output from the run if the run is taking longer than 1 hour.
|
||||
|
||||
Defaults to `3600`.
|
||||
|
||||
## `DD_API_KEY`
|
||||
|
||||
Specify `DD_API_KEY` (your [Datadog API Key](https://docs.datadoghq.com/account_management/api-app-keys/)) to automatically enable Datadog tracing for the deployment. Specify other [`DD_*` environment variables](https://ddtrace.readthedocs.io/en/stable/configuration.html) to configure the tracing instrumentation.
|
||||
@@ -28,6 +44,10 @@ Set this environment variable to have a BYOC deployment send traces to a self-ho
|
||||
|
||||
`SELF_HOSTED_LANGSMITH_HOSTNAME` is the hostname of the self-hosted LangSmith instance. It must be accessible to the BYOC deployment. `LANGSMITH_API_KEY` is a LangSmith API generated from the self-hosted LangSmith instance.
|
||||
|
||||
## `LOG_LEVEL`
|
||||
|
||||
Configure [log level](https://docs.python.org/3/library/logging.html#logging-levels). Defaults to `INFO`.
|
||||
|
||||
## `N_JOBS_PER_WORKER`
|
||||
|
||||
Number of jobs per worker for the LangGraph Cloud task queue. Defaults to `10`.
|
||||
@@ -55,3 +75,9 @@ Database Connectivity:
|
||||
|
||||
- The externally managed Postgres instance must be accessible by the LangGraph Server service in the ECS cluster. The BYOC user is responsible for ensuring connectivity.
|
||||
- For example, if an AWS RDS Postgres instance is provisioned, it can be provisioned in the same VPC (`langgraph-cloud-vpc`) as the ECS cluster with the `langgraph-cloud-service-sg` security group to ensure connectivity.
|
||||
|
||||
## `REDIS_URI_CUSTOM`
|
||||
|
||||
For [Bring Your Own Cloud (BYOC)](../../concepts/bring_your_own_cloud.md) deployments only.
|
||||
|
||||
Specify `REDIS_URI_CUSTOM` to use an externally managed Redis instance. The value of `REDIS_URI_CUSTOM` must be a valid [Redis connection URI](https://redis-py.readthedocs.io/en/stable/connections.html#redis.Redis.from_url).
|
||||
|
||||
@@ -19,6 +19,10 @@ Resource Allocation:
|
||||
| Development | 1 CPU | 1 GB | Up to 1 container |
|
||||
| Production | 2 CPU | 2 GB | Up to 10 containers |
|
||||
|
||||
CPU and memory resources are per container.
|
||||
|
||||
For **Production type** deployments, resources can be manually increased on a case-by-case basis depending on use case and capacity constraints. Contact support@langchain.dev to request an increase in resources.
|
||||
|
||||
See the [how-to guide](../cloud/deployment/cloud.md#create-new-deployment) for creating a new deployment.
|
||||
|
||||
## Revision
|
||||
@@ -35,6 +39,8 @@ When defining a graph to be deployed to LangGraph Cloud SaaS, a [checkpointer](.
|
||||
|
||||
There is no direct access to the database. All access to the database occurs through the LangGraph Server APIs.
|
||||
|
||||
The database is never deleted until the deployment itself is deleted. See [Automatic Deletion](#automatic-deletion) for additional details.
|
||||
|
||||
## Autoscaling
|
||||
`Production` type deployments automatically scale up to 10 containers. Scaling is based on the current request load for a single container. Specifically, the autoscaling implementation scales the deployment so that each container is processing about 10 concurrent requests. For example...
|
||||
|
||||
@@ -57,7 +63,7 @@ Infrastructure for [deployments](#deployment) and [revisions](#revision) are pro
|
||||
|
||||
## LangSmith Integration
|
||||
|
||||
A [LangSmith](https://docs.smith.langchain.com/) tracing project is automatically created for each deployemnt. The tracing project has the same name as the deployment. When creating a deployment, the `LANGCHAIN_TRACING_V2` and `LANGCHAIN_API_KEY` environment variables do not need to be specified; they are set internally, automatically. Traces are created for each run and are emitted to the tracing project automatically.
|
||||
A [LangSmith](https://docs.smith.langchain.com/) tracing project is automatically created for each deployemnt. 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 internally, automatically. Traces are created for each run and are emitted to the tracing project automatically.
|
||||
|
||||
When a deployment is deleted, the traces and the tracing project are not deleted.
|
||||
|
||||
|
||||
@@ -300,6 +300,7 @@ LangGraph Studio is a built-in UI for visualizing, testing, and debugging your a
|
||||
- [How to interact with threads in LangGraph Studio](../cloud/how-tos/threads_studio.md)
|
||||
- [How to add nodes as dataset examples in LangGraph Studio](../cloud/how-tos/datasets_studio.md)
|
||||
- [How to engineer prompts in LangGraph Studio](../cloud/how-tos/iterate_graph_studio.md)
|
||||
- [How to test your agent against remote traces](../cloud/how-tos/clone_traces_studio.md)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
# LLMs-txt for LangGraph
|
||||
# LLMs-txt Overview
|
||||
|
||||
## Overview
|
||||
|
||||
LangGraph provides documentation files in the [`llms.txt`](https://llmstxt.org/) format, specifically `llms.txt` and `llms-full.txt`. These files allow large language models (LLMs) and agents to access programming documentation and APIs, particularly useful within integrated development environments (IDEs).
|
||||
Below you can find a list of documentation files in the [`llms.txt`](https://llmstxt.org/) format, specifically `llms.txt` and `llms-full.txt`. These files allow large language models (LLMs) and agents to access programming documentation and APIs, particularly useful within integrated development environments (IDEs).
|
||||
|
||||
| Language Version | llms.txt | llms-full.txt |
|
||||
|------------------|------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|
|
||||
| LangGraph Python | [https://langchain-ai.github.io/langgraph/llms.txt](https://langchain-ai.github.io/langgraph/llms.txt) | [https://langchain-ai.github.io/langgraph/llms-full.txt](https://langchain-ai.github.io/langgraph/llms-full.txt) |
|
||||
| LangGraph JS | [https://langchain-ai.github.io/langgraphjs/llms.txt](https://langchain-ai.github.io/langgraphjs/llms.txt) | [https://langchain-ai.github.io/langgraphjs/llms-full.txt](https://langchain-ai.github.io/langgraphjs/llms-full.txt) |
|
||||
| LangChain Python | [https://python.langchain.com/llms.txt](https://python.langchain.com/llms.txt) | N/A |
|
||||
| LangChain JS | [https://js.langchain.com/llms.txt](https://js.langchain.com/llms.txt) | N/A |
|
||||
|
||||
!!! info "Review the output"
|
||||
|
||||
Even with access to up-to-date documentation, current state-of-the-art models may not always generate correct code. Treat the generated code as a starting point, and always review it before shipping
|
||||
code to production.
|
||||
|
||||
## Differences Between `llms.txt` and `llms-full.txt`
|
||||
|
||||
@@ -19,9 +26,17 @@ A key consideration when using `llms-full.txt` is its size. For extensive docume
|
||||
|
||||
## Using `llms.txt` via an MCP Server
|
||||
|
||||
As of March 9, 2025, IDEs [do not yet have robust native support for `llms.txt`](https://x.com/jeremyphoward/status/1902109312216129905?t=1eHFv2vdNdAckajnug0_Vw&s=19). However, you can utilize `llms.txt` effectively through an MCP server.
|
||||
As of March 9, 2025, IDEs [do not yet have robust native support for `llms.txt`](https://x.com/jeremyphoward/status/1902109312216129905?t=1eHFv2vdNdAckajnug0_Vw&s=19). However, you can still use `llms.txt` effectively through an MCP server.
|
||||
|
||||
We provide an MCP server specifically designed to serve documentation, called [`mcpdoc`](https://github.com/langchain-ai/mcpdoc). This setup is compatible with IDEs and platforms such as Cursor, Windsurf, Claude, and Claude Code. Instructions for using `mcpdoc` with these tools are available in the repository.
|
||||
### 🚀 Use the `mcpdoc` Server
|
||||
|
||||
We provide an **MCP server** that was designed to serve documentation for LLMs and IDEs:
|
||||
|
||||
👉 **[langchain-ai/mcpdoc GitHub Repository](https://github.com/langchain-ai/mcpdoc)**
|
||||
|
||||
This MCP server allows integrating `llms.txt` into tools like **Cursor**, **Windsurf**, **Claude**, and **Claude Code**.
|
||||
|
||||
📘 **Setup instructions and usage examples** are available in the repository.
|
||||
|
||||
## Using `llms-full.txt`
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
:root {
|
||||
--md-admonition-icon--version-added: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h4l3 3 3-3h4c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2m0 16h-4.2l-.8.8-2 2-2-2-.8-.8H5V4h14z"/><path d="M11 15h2v2h-2v-2m0-10h2v8h-2V5"/></svg>');
|
||||
--md-admonition-icon--version-changed: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h4l3 3 3-3h4c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2m0 16h-4.2l-.8.8-2 2-2-2-.8-.8H5V4h14z"/><path d="M15 11h-2V9h-2v2H9v2h2v2h2v-2h2v-2Z"/></svg>');
|
||||
}
|
||||
|
||||
.md-typeset .admonition.version-added,
|
||||
.md-typeset details.version-added {
|
||||
border-color: rgb(0, 191, 165);
|
||||
}
|
||||
|
||||
.md-typeset .version-added > .admonition-title,
|
||||
.md-typeset .version-added > summary {
|
||||
background-color: rgba(0, 191, 165, 0.1);
|
||||
}
|
||||
|
||||
.md-typeset .version-added > .admonition-title::before,
|
||||
.md-typeset .version-added > summary::before {
|
||||
background-color: rgb(0, 191, 165);
|
||||
-webkit-mask-image: var(--md-admonition-icon--version-added);
|
||||
mask-image: var(--md-admonition-icon--version-added);
|
||||
}
|
||||
|
||||
.md-typeset .admonition.version-changed,
|
||||
.md-typeset details.version-changed {
|
||||
border-color: rgb(100, 221, 23);
|
||||
}
|
||||
|
||||
.md-typeset .version-changed > .admonition-title,
|
||||
.md-typeset .version-changed > summary {
|
||||
background-color: rgba(100, 221, 23, 0.1);
|
||||
}
|
||||
|
||||
.md-typeset .version-changed > .admonition-title::before,
|
||||
.md-typeset .version-changed > summary::before {
|
||||
background-color: rgb(100, 221, 23);
|
||||
-webkit-mask-image: var(--md-admonition-icon--version-changed);
|
||||
mask-image: var(--md-admonition-icon--version-changed);
|
||||
}
|
||||
@@ -256,6 +256,8 @@ nav:
|
||||
- cloud/how-tos/invoke_studio.md
|
||||
- cloud/how-tos/threads_studio.md
|
||||
- cloud/how-tos/datasets_studio.md
|
||||
- cloud/how-tos/iterate_graph_studio.md
|
||||
- cloud/how-tos/clone_traces_studio.md
|
||||
- Concepts:
|
||||
- concepts/index.md
|
||||
- LangGraph:
|
||||
@@ -503,3 +505,5 @@ validation:
|
||||
not_found: info
|
||||
copyright: >
|
||||
Copyright © 2025 LangChain, Inc | <a href="#__consent">Consent Preferences</a>
|
||||
extra_css:
|
||||
- stylesheets/version_admonitions.css
|
||||
|
||||
@@ -124,6 +124,7 @@ def copy_checkpoint(checkpoint: Checkpoint) -> Checkpoint:
|
||||
)
|
||||
|
||||
|
||||
# Kept for backwards compat, newer versions of LangGraph no longer use this.
|
||||
def create_checkpoint(
|
||||
checkpoint: Checkpoint,
|
||||
channels: Optional[Mapping[str, ChannelProtocol]],
|
||||
|
||||
@@ -30,6 +30,7 @@ from langgraph.checkpoint.serde.types import SendProtocol
|
||||
from langgraph.store.base import Item
|
||||
|
||||
LC_REVIVER = Reviver()
|
||||
EMPTY_BYTES = b""
|
||||
|
||||
|
||||
class JsonPlusSerializer(SerializerProtocol):
|
||||
@@ -194,7 +195,9 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
)
|
||||
|
||||
def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
|
||||
if isinstance(obj, bytes):
|
||||
if obj is None:
|
||||
return "null", EMPTY_BYTES
|
||||
elif isinstance(obj, bytes):
|
||||
return "bytes", obj
|
||||
elif isinstance(obj, bytearray):
|
||||
return "bytearray", obj
|
||||
@@ -211,7 +214,9 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
|
||||
def loads_typed(self, data: tuple[str, bytes]) -> Any:
|
||||
type_, data_ = data
|
||||
if type_ == "bytes":
|
||||
if type_ == "null":
|
||||
return None
|
||||
elif type_ == "bytes":
|
||||
return data_
|
||||
elif type_ == "bytearray":
|
||||
return bytearray(data_)
|
||||
|
||||
@@ -580,6 +580,12 @@ def dockerfile(save_path: str, config: pathlib.Path, add_docker_compose: bool) -
|
||||
default=None,
|
||||
help="URL of the LangGraph Studio instance to connect to. Defaults to https://smith.langchain.com",
|
||||
)
|
||||
@click.option(
|
||||
"--allow-blocking",
|
||||
is_flag=True,
|
||||
help="Don't raise errors for synchronous I/O blocking operations in your code.",
|
||||
default=False,
|
||||
)
|
||||
@cli.command(
|
||||
"dev",
|
||||
help="🏃♀️➡️ Run LangGraph API server in development mode with hot reloading and debugging support",
|
||||
@@ -595,6 +601,7 @@ def dev(
|
||||
debug_port: Optional[int],
|
||||
wait_for_client: bool,
|
||||
studio_url: Optional[str],
|
||||
allow_blocking: bool,
|
||||
):
|
||||
"""CLI entrypoint for running the LangGraph API server."""
|
||||
try:
|
||||
@@ -659,6 +666,7 @@ def dev(
|
||||
auth=config_json.get("auth"),
|
||||
http=config_json.get("http"),
|
||||
studio_url=studio_url,
|
||||
allow_blocking=allow_blocking,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -405,6 +405,7 @@ def validate_config(config: Config) -> Config:
|
||||
"auth": config.get("auth"),
|
||||
"http": config.get("http"),
|
||||
"ui": config.get("ui"),
|
||||
"ui_config": config.get("ui_config"),
|
||||
}
|
||||
if config.get("node_version")
|
||||
else {
|
||||
@@ -418,6 +419,7 @@ def validate_config(config: Config) -> Config:
|
||||
"auth": config.get("auth"),
|
||||
"http": config.get("http"),
|
||||
"ui": config.get("ui"),
|
||||
"ui_config": config.get("ui_config"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1098,6 +1100,7 @@ RUN cd {faux_path} && {install_cmd}
|
||||
{env_additional_config}
|
||||
ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
|
||||
{f"ENV LANGGRAPH_UI='{json.dumps(config['ui'])}'" if config.get("ui") else ""}
|
||||
{f"ENV LANGGRAPH_UI_CONFIG='{json.dumps(config['ui_config'])}'" if config.get("ui_config") else ""}
|
||||
|
||||
WORKDIR {faux_path}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-cli"
|
||||
version = "0.1.80"
|
||||
version = "0.1.82"
|
||||
description = "CLI for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -34,6 +34,7 @@ def test_validate_config():
|
||||
"auth": None,
|
||||
"http": None,
|
||||
"ui": None,
|
||||
"ui_config": None,
|
||||
**expected_config,
|
||||
}
|
||||
actual_config = validate_config(expected_config)
|
||||
@@ -54,6 +55,7 @@ def test_validate_config():
|
||||
"auth": None,
|
||||
"http": None,
|
||||
"ui": None,
|
||||
"ui_config": None,
|
||||
}
|
||||
actual_config = validate_config(expected_config)
|
||||
assert actual_config == expected_config
|
||||
@@ -470,6 +472,7 @@ def test_config_to_docker_nodejs():
|
||||
"graphs": graphs,
|
||||
"dockerfile_lines": ["ARG meow", "ARG foo"],
|
||||
"ui": {"agent": "./graphs/agent.ui.jsx"},
|
||||
"ui_config": {"shared": ["nuqs"]},
|
||||
}
|
||||
),
|
||||
"langchain/langgraphjs-api",
|
||||
@@ -481,6 +484,7 @@ ADD . /deps/unit_tests
|
||||
RUN cd /deps/unit_tests && npm i
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}'
|
||||
ENV LANGGRAPH_UI='{"agent": "./graphs/agent.ui.jsx"}'
|
||||
ENV LANGGRAPH_UI_CONFIG='{"shared": ["nuqs"]}'
|
||||
WORKDIR /deps/unit_tests
|
||||
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts"""
|
||||
|
||||
|
||||
@@ -252,27 +252,15 @@ benchmarks = (
|
||||
},
|
||||
),
|
||||
(
|
||||
"sequential_20",
|
||||
create_sequential(20).compile(),
|
||||
create_sequential(20).compile(),
|
||||
"sequential_10",
|
||||
create_sequential(10).compile(),
|
||||
create_sequential(10).compile(),
|
||||
{"messages": []}, # Empty list of messages
|
||||
),
|
||||
(
|
||||
"sequential_50",
|
||||
create_sequential(50).compile(),
|
||||
create_sequential(50).compile(),
|
||||
{"messages": []}, # Empty list of messages
|
||||
),
|
||||
(
|
||||
"sequential_100",
|
||||
create_sequential(100).compile(),
|
||||
create_sequential(100).compile(),
|
||||
{"messages": []}, # Empty list of messages
|
||||
),
|
||||
(
|
||||
"sequential_200",
|
||||
create_sequential(200).compile(),
|
||||
create_sequential(200).compile(),
|
||||
"sequential_1000",
|
||||
create_sequential(1000).compile(),
|
||||
create_sequential(1000).compile(),
|
||||
{"messages": []}, # Empty list of messages
|
||||
),
|
||||
(
|
||||
@@ -383,8 +371,17 @@ for name, agraph, graph, input in benchmarks:
|
||||
r.bench_func(name + "_sync", run, graph, input)
|
||||
|
||||
|
||||
# Pick a handful of graphs to measure the first event latency.
|
||||
# At the moment, limiting just due to the size of the annotation on github.
|
||||
GRAPHS_FOR_1st_EVENT_LATENCY = (
|
||||
"sequential_1000",
|
||||
"pydantic_state_25x300",
|
||||
)
|
||||
|
||||
# First event latency
|
||||
for name, agraph, graph, input in benchmarks:
|
||||
if graph not in GRAPHS_FOR_1st_EVENT_LATENCY:
|
||||
continue
|
||||
r.bench_async_func(
|
||||
name + "_first_event_latency",
|
||||
arun_first_event_latency,
|
||||
@@ -403,30 +400,14 @@ compilation_benchmarks = (
|
||||
"sequential_1000",
|
||||
create_sequential(1_000),
|
||||
),
|
||||
(
|
||||
"sequential_10000",
|
||||
create_sequential(10_000),
|
||||
),
|
||||
(
|
||||
"pydantic_state_25x300",
|
||||
pydantic_state(300),
|
||||
),
|
||||
(
|
||||
"pydantic_state_15x600",
|
||||
pydantic_state(600),
|
||||
),
|
||||
(
|
||||
"pydantic_state_9x1200",
|
||||
pydantic_state(1200),
|
||||
),
|
||||
(
|
||||
"wide_state_15x600",
|
||||
wide_state(600),
|
||||
),
|
||||
(
|
||||
"wide_state_9x1200",
|
||||
wide_state(1200),
|
||||
),
|
||||
)
|
||||
|
||||
for name, graph in compilation_benchmarks:
|
||||
|
||||
@@ -34,7 +34,7 @@ if __name__ == "__main__":
|
||||
|
||||
import uvloop
|
||||
|
||||
graph = create_sequential(2000).compile()
|
||||
graph = create_sequential(3000).compile()
|
||||
input = {"messages": []} # Empty list of messages
|
||||
config = {"recursion_limit": 20000000000}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Generic, Optional, Sequence, Type
|
||||
from typing import Any, Generic, Sequence, Type
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -30,10 +30,15 @@ class AnyValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def from_checkpoint(self, checkpoint: Optional[Value]) -> Self:
|
||||
empty = self.__class__(self.typ)
|
||||
empty.key = self.key
|
||||
if checkpoint is not None:
|
||||
def copy(self) -> Self:
|
||||
"""Return a copy of the channel."""
|
||||
empty = self.__class__(self.typ, self.key)
|
||||
empty.value = self.value
|
||||
return empty
|
||||
|
||||
def from_checkpoint(self, checkpoint: Value) -> Self:
|
||||
empty = self.__class__(self.typ, self.key)
|
||||
if checkpoint is not MISSING:
|
||||
empty.value = checkpoint
|
||||
return empty
|
||||
|
||||
@@ -55,3 +60,6 @@ class AnyValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return self.value is not MISSING
|
||||
|
||||
def checkpoint(self) -> Value:
|
||||
return self.value
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Generic, Optional, Sequence, TypeVar
|
||||
from typing import Any, Generic, Sequence, TypeVar
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError, InvalidUpdateError
|
||||
|
||||
Value = TypeVar("Value")
|
||||
@@ -29,14 +30,23 @@ class BaseChannel(Generic[Value, Update, C], ABC):
|
||||
|
||||
# serialize/deserialize methods
|
||||
|
||||
def checkpoint(self) -> Optional[C]:
|
||||
def copy(self) -> Self:
|
||||
"""Return a copy of the channel.
|
||||
By default, delegates to checkpoint() and from_checkpoint().
|
||||
Subclasses can override this method with a more efficient implementation."""
|
||||
return self.from_checkpoint(self.checkpoint())
|
||||
|
||||
def checkpoint(self) -> C:
|
||||
"""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."""
|
||||
return self.get()
|
||||
try:
|
||||
return self.get()
|
||||
except EmptyChannelError:
|
||||
return MISSING
|
||||
|
||||
@abstractmethod
|
||||
def from_checkpoint(self, checkpoint: Optional[C]) -> Self:
|
||||
def from_checkpoint(self, checkpoint: C) -> Self:
|
||||
"""Return a new identical channel, optionally initialized from a checkpoint.
|
||||
If the checkpoint contains complex data structures, they should be copied."""
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import collections.abc
|
||||
from typing import (
|
||||
Callable,
|
||||
Generic,
|
||||
Optional,
|
||||
Sequence,
|
||||
Type,
|
||||
)
|
||||
from typing import Callable, Generic, Sequence, Type
|
||||
|
||||
from typing_extensions import NotRequired, Required, Self
|
||||
|
||||
@@ -72,10 +66,17 @@ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def from_checkpoint(self, checkpoint: Optional[Value]) -> Self:
|
||||
def copy(self) -> Self:
|
||||
"""Return a copy of the channel."""
|
||||
empty = self.__class__(self.typ, self.operator)
|
||||
empty.key = self.key
|
||||
if checkpoint is not None:
|
||||
empty.value = self.value
|
||||
return empty
|
||||
|
||||
def from_checkpoint(self, checkpoint: Value) -> Self:
|
||||
empty = self.__class__(self.typ, self.operator)
|
||||
empty.key = self.key
|
||||
if checkpoint is not MISSING:
|
||||
empty.value = checkpoint
|
||||
return empty
|
||||
|
||||
@@ -96,3 +97,6 @@ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return self.value is not MISSING
|
||||
|
||||
def checkpoint(self) -> Value:
|
||||
return self.value
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Any, Generic, NamedTuple, Optional, Sequence, Type, Union
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.channels.base import BaseChannel, Value
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError, InvalidUpdateError
|
||||
|
||||
|
||||
@@ -45,16 +46,23 @@ class DynamicBarrierValue(
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def copy(self) -> Self:
|
||||
"""Return a copy of the channel."""
|
||||
empty = self.__class__(self.typ)
|
||||
empty.key = self.key
|
||||
empty.names = self.names
|
||||
empty.seen = self.seen.copy()
|
||||
return empty
|
||||
|
||||
def checkpoint(self) -> tuple[Optional[set[Value]], set[Value]]:
|
||||
return (self.names, self.seen)
|
||||
|
||||
def from_checkpoint(
|
||||
self,
|
||||
checkpoint: Optional[tuple[Optional[set[Value]], set[Value]]],
|
||||
self, checkpoint: tuple[Optional[set[Value]], set[Value]]
|
||||
) -> Self:
|
||||
empty = self.__class__(self.typ)
|
||||
empty.key = self.key
|
||||
if checkpoint is not None:
|
||||
if checkpoint is not MISSING:
|
||||
names, seen = checkpoint
|
||||
empty.names = names if names is not None else None
|
||||
empty.seen = seen
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Generic, Optional, Sequence, Type
|
||||
from typing import Any, Generic, Sequence, Type
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -30,10 +30,17 @@ class EphemeralValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def from_checkpoint(self, checkpoint: Optional[Value]) -> Self:
|
||||
def copy(self) -> Self:
|
||||
"""Return a copy of the channel."""
|
||||
empty = self.__class__(self.typ, self.guard)
|
||||
empty.key = self.key
|
||||
if checkpoint is not None:
|
||||
empty.value = self.value
|
||||
return empty
|
||||
|
||||
def from_checkpoint(self, checkpoint: Value) -> Self:
|
||||
empty = self.__class__(self.typ, self.guard)
|
||||
empty.key = self.key
|
||||
if checkpoint is not MISSING:
|
||||
empty.value = checkpoint
|
||||
return empty
|
||||
|
||||
@@ -59,3 +66,6 @@ class EphemeralValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return self.value is not MISSING
|
||||
|
||||
def checkpoint(self) -> Value:
|
||||
return self.value
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Generic, Optional, Sequence, Type
|
||||
from typing import Any, Generic, Sequence, Type
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -34,10 +34,15 @@ class LastValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def from_checkpoint(self, checkpoint: Optional[Value]) -> Self:
|
||||
empty = self.__class__(self.typ)
|
||||
empty.key = self.key
|
||||
if checkpoint is not None:
|
||||
def copy(self) -> Self:
|
||||
"""Return a copy of the channel."""
|
||||
empty = self.__class__(self.typ, self.key)
|
||||
empty.value = self.value
|
||||
return empty
|
||||
|
||||
def from_checkpoint(self, checkpoint: Value) -> Self:
|
||||
empty = self.__class__(self.typ, self.key)
|
||||
if checkpoint is not MISSING:
|
||||
empty.value = checkpoint
|
||||
return empty
|
||||
|
||||
@@ -61,3 +66,6 @@ class LastValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return self.value is not MISSING
|
||||
|
||||
def checkpoint(self) -> Value:
|
||||
return self.value
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from typing import Generic, Optional, Sequence, Type
|
||||
from typing import Generic, Sequence, Type
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.channels.base import BaseChannel, Value
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError, InvalidUpdateError
|
||||
|
||||
|
||||
@@ -32,13 +33,20 @@ class NamedBarrierValue(Generic[Value], BaseChannel[Value, Value, set[Value]]):
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def copy(self) -> Self:
|
||||
"""Return a copy of the channel."""
|
||||
empty = self.__class__(self.typ, self.names)
|
||||
empty.key = self.key
|
||||
empty.seen = self.seen.copy()
|
||||
return empty
|
||||
|
||||
def checkpoint(self) -> set[Value]:
|
||||
return self.seen
|
||||
|
||||
def from_checkpoint(self, checkpoint: Optional[set[Value]]) -> Self:
|
||||
def from_checkpoint(self, checkpoint: set[Value]) -> Self:
|
||||
empty = self.__class__(self.typ, self.names)
|
||||
empty.key = self.key
|
||||
if checkpoint is not None:
|
||||
if checkpoint is not MISSING:
|
||||
empty.seen = checkpoint
|
||||
return empty
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from typing import Any, Generic, Iterator, Optional, Sequence, Type, Union
|
||||
from typing import Any, Generic, Iterator, Sequence, Type, Union
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.channels.base import BaseChannel, Value
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError
|
||||
|
||||
|
||||
@@ -16,9 +17,7 @@ def flatten(values: Sequence[Union[Value, list[Value]]]) -> Iterator[Value]:
|
||||
|
||||
class Topic(
|
||||
Generic[Value],
|
||||
BaseChannel[
|
||||
Sequence[Value], Union[Value, list[Value]], tuple[set[Value], list[Value]]
|
||||
],
|
||||
BaseChannel[Sequence[Value], Union[Value, list[Value]], list[Value]],
|
||||
):
|
||||
"""A configurable PubSub Topic.
|
||||
|
||||
@@ -49,20 +48,28 @@ class Topic(
|
||||
"""The type of the update received by the channel."""
|
||||
return Union[self.typ, list[self.typ]] # type: ignore[name-defined]
|
||||
|
||||
def checkpoint(self) -> tuple[set[Value], list[Value]]:
|
||||
return self.values
|
||||
|
||||
def from_checkpoint(self, checkpoint: Optional[list[Value]]) -> Self:
|
||||
def copy(self) -> Self:
|
||||
"""Return a copy of the channel."""
|
||||
empty = self.__class__(self.typ, self.accumulate)
|
||||
empty.key = self.key
|
||||
if checkpoint is not None:
|
||||
empty.values = self.values.copy()
|
||||
return empty
|
||||
|
||||
def checkpoint(self) -> list[Value]:
|
||||
return self.values
|
||||
|
||||
def from_checkpoint(self, checkpoint: list[Value]) -> Self:
|
||||
empty = self.__class__(self.typ, self.accumulate)
|
||||
empty.key = self.key
|
||||
if checkpoint is not MISSING:
|
||||
if isinstance(checkpoint, tuple):
|
||||
# backwards compatibility
|
||||
empty.values = checkpoint[1]
|
||||
else:
|
||||
empty.values = checkpoint
|
||||
return empty
|
||||
|
||||
def update(self, values: Sequence[Union[Value, list[Value]]]) -> None:
|
||||
def update(self, values: Sequence[Union[Value, list[Value]]]) -> bool:
|
||||
current = list(self.values)
|
||||
if not self.accumulate:
|
||||
self.values = list[Value]()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Generic, Optional, Sequence, Type
|
||||
from typing import Generic, Sequence, Type
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -30,10 +30,17 @@ class UntrackedValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def checkpoint(self) -> Value:
|
||||
raise EmptyChannelError()
|
||||
def copy(self) -> Self:
|
||||
"""Return a copy of the channel."""
|
||||
empty = self.__class__(self.typ, self.guard)
|
||||
empty.key = self.key
|
||||
empty.value = self.value
|
||||
return empty
|
||||
|
||||
def from_checkpoint(self, checkpoint: Optional[Value]) -> Self:
|
||||
def checkpoint(self) -> Value:
|
||||
return MISSING
|
||||
|
||||
def from_checkpoint(self, checkpoint: Value) -> Self:
|
||||
empty = self.__class__(self.typ, self.guard)
|
||||
empty.key = self.key
|
||||
return empty
|
||||
|
||||
@@ -242,7 +242,7 @@ class StateGraph(Graph):
|
||||
metadata: Optional[dict[str, Any]] = None,
|
||||
input: Optional[Type[Any]] = None,
|
||||
retry: Optional[RetryPolicy] = None,
|
||||
destinations: Optional[Union[dict[str, str], tuple[str]]] = None,
|
||||
destinations: Optional[Union[dict[str, str], tuple[str, ...]]] = None,
|
||||
) -> Self:
|
||||
"""Adds a new node to the state graph.
|
||||
Will take the name of the function/runnable as the node name.
|
||||
@@ -267,7 +267,7 @@ class StateGraph(Graph):
|
||||
metadata: Optional[dict[str, Any]] = None,
|
||||
input: Optional[Type[Any]] = None,
|
||||
retry: Optional[RetryPolicy] = None,
|
||||
destinations: Optional[Union[dict[str, str], tuple[str]]] = None,
|
||||
destinations: Optional[Union[dict[str, str], tuple[str, ...]]] = None,
|
||||
) -> Self:
|
||||
"""Adds a new node to the state graph.
|
||||
|
||||
@@ -291,7 +291,7 @@ class StateGraph(Graph):
|
||||
metadata: Optional[dict[str, Any]] = None,
|
||||
input: Optional[Type[Any]] = None,
|
||||
retry: Optional[RetryPolicy] = None,
|
||||
destinations: Optional[Union[dict[str, str], tuple[str]]] = None,
|
||||
destinations: Optional[Union[dict[str, str], tuple[str, ...]]] = None,
|
||||
) -> Self:
|
||||
"""Adds a new node to the state graph.
|
||||
|
||||
@@ -303,7 +303,7 @@ class StateGraph(Graph):
|
||||
metadata (Optional[dict[str, Any]]): The metadata associated with the node. (default: None)
|
||||
input (Optional[Type[Any]]): The input schema for the node. (default: the graph's input schema)
|
||||
retry (Optional[RetryPolicy]): The policy for retrying the node. (default: None)
|
||||
destinations (Optional[Union[dict[str, str], tuple[str]]]): Destinations that indicate where a node can route to.
|
||||
destinations (Optional[Union[dict[str, str], tuple[str, ...]]]): Destinations that indicate where a node can route to.
|
||||
This is useful for edgeless graphs with nodes that return `Command` objects.
|
||||
If a dict is provided, the keys will be used as the target node names and the values will be used as the labels for the edges.
|
||||
If a tuple is provided, the values will be used as the target node names.
|
||||
@@ -799,11 +799,11 @@ class CompiledStateGraph(CompiledGraph):
|
||||
raise InvalidUpdateError(msg)
|
||||
|
||||
# state updaters
|
||||
write_entries: list[Union[ChannelWriteEntry, ChannelWriteTupleEntry]] = [
|
||||
write_entries: tuple[Union[ChannelWriteEntry, ChannelWriteTupleEntry], ...] = (
|
||||
ChannelWriteTupleEntry(
|
||||
mapper=_get_root if output_keys == ["__root__"] else _get_updates
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
# add node and output channel
|
||||
if key == START:
|
||||
@@ -811,20 +811,14 @@ class CompiledStateGraph(CompiledGraph):
|
||||
tags=[TAG_HIDDEN],
|
||||
triggers=[START],
|
||||
channels=[START],
|
||||
writers=[
|
||||
ChannelWrite(
|
||||
write_entries,
|
||||
tags=[TAG_HIDDEN],
|
||||
),
|
||||
],
|
||||
writers=[ChannelWrite(write_entries, tags=[TAG_HIDDEN])],
|
||||
)
|
||||
elif node is not None:
|
||||
input_schema = node.input if node else self.builder.schema
|
||||
input_values = {k: k for k in self.builder.schemas[input_schema]}
|
||||
is_single_input = len(input_values) == 1 and "__root__" in input_values
|
||||
|
||||
branch_channel = f"branch:to:{key}"
|
||||
self.channels[key] = EphemeralValue(Any, guard=False)
|
||||
branch_channel = CHANNEL_BRANCH_TO.format(key)
|
||||
self.channels[branch_channel] = EphemeralValue(Any, guard=False)
|
||||
self.nodes[key] = PregelNode(
|
||||
triggers=[branch_channel],
|
||||
@@ -836,13 +830,8 @@ class CompiledStateGraph(CompiledGraph):
|
||||
input_schema,
|
||||
self.builder.type_hints[input_schema],
|
||||
),
|
||||
writers=[
|
||||
# publish to this channel and state keys
|
||||
ChannelWrite(
|
||||
write_entries + [ChannelWriteEntry(key, key)],
|
||||
tags=[TAG_HIDDEN],
|
||||
),
|
||||
],
|
||||
# publish to state keys
|
||||
writers=[ChannelWrite(write_entries, tags=[TAG_HIDDEN])],
|
||||
metadata=node.metadata,
|
||||
retry_policy=node.retry_policy,
|
||||
bound=node.runnable,
|
||||
@@ -852,21 +841,13 @@ class CompiledStateGraph(CompiledGraph):
|
||||
|
||||
def attach_edge(self, starts: Union[str, Sequence[str]], end: str) -> None:
|
||||
if isinstance(starts, str):
|
||||
if starts == START:
|
||||
channel_name = f"start:{end}"
|
||||
# register channel
|
||||
self.channels[channel_name] = EphemeralValue(Any)
|
||||
# subscribe to channel
|
||||
self.nodes[end].triggers.append(channel_name)
|
||||
# publish to channel
|
||||
self.nodes[START].writers.append(
|
||||
# subscribe to start channel
|
||||
if end != END:
|
||||
self.nodes[starts].writers.append(
|
||||
ChannelWrite(
|
||||
[ChannelWriteEntry(channel_name, START)], tags=[TAG_HIDDEN]
|
||||
(ChannelWriteEntry(CHANNEL_BRANCH_TO.format(end), None),)
|
||||
)
|
||||
)
|
||||
elif end != END:
|
||||
# subscribe to start channel
|
||||
self.nodes[end].triggers.append(starts)
|
||||
elif end != END:
|
||||
channel_name = f"join:{'+'.join(starts)}:{end}"
|
||||
# register channel
|
||||
@@ -877,7 +858,7 @@ class CompiledStateGraph(CompiledGraph):
|
||||
for start in starts:
|
||||
self.nodes[start].writers.append(
|
||||
ChannelWrite(
|
||||
[ChannelWriteEntry(channel_name, start)], tags=[TAG_HIDDEN]
|
||||
(ChannelWriteEntry(channel_name, start),), tags=[TAG_HIDDEN]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -890,7 +871,7 @@ class CompiledStateGraph(CompiledGraph):
|
||||
if filtered := [p for p in packets if p != END]:
|
||||
writes = [
|
||||
(
|
||||
ChannelWriteEntry(f"branch:to:{p}", start)
|
||||
ChannelWriteEntry(CHANNEL_BRANCH_TO.format(p), None)
|
||||
if not isinstance(p, Send)
|
||||
else p
|
||||
)
|
||||
@@ -1166,3 +1147,6 @@ def _get_schema(
|
||||
if k in channels and isinstance(channels[k], BaseChannel)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
CHANNEL_BRANCH_TO = "branch:to:{}"
|
||||
|
||||
@@ -50,7 +50,6 @@ from langgraph.checkpoint.base import (
|
||||
BaseCheckpointSaver,
|
||||
CheckpointTuple,
|
||||
copy_checkpoint,
|
||||
create_checkpoint,
|
||||
empty_checkpoint,
|
||||
)
|
||||
from langgraph.constants import (
|
||||
@@ -91,6 +90,7 @@ from langgraph.pregel.algo import (
|
||||
local_write,
|
||||
prepare_next_tasks,
|
||||
)
|
||||
from langgraph.pregel.checkpoint import create_checkpoint
|
||||
from langgraph.pregel.debug import tasks_w_writes
|
||||
from langgraph.pregel.io import map_input, read_channels
|
||||
from langgraph.pregel.loop import AsyncPregelLoop, StreamProtocol, SyncPregelLoop
|
||||
@@ -1535,12 +1535,9 @@ class Pregel(PregelProtocol):
|
||||
),
|
||||
CONFIG_KEY_READ: partial(
|
||||
local_read,
|
||||
step + 1,
|
||||
checkpoint,
|
||||
channels,
|
||||
managed,
|
||||
task,
|
||||
config,
|
||||
),
|
||||
},
|
||||
),
|
||||
@@ -1944,12 +1941,9 @@ class Pregel(PregelProtocol):
|
||||
),
|
||||
CONFIG_KEY_READ: partial(
|
||||
local_read,
|
||||
step + 1,
|
||||
checkpoint,
|
||||
channels,
|
||||
managed,
|
||||
task,
|
||||
config,
|
||||
),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import binascii
|
||||
import itertools
|
||||
import sys
|
||||
import threading
|
||||
from collections import defaultdict, deque
|
||||
from functools import partial
|
||||
from hashlib import sha1
|
||||
@@ -32,7 +33,6 @@ from langgraph.checkpoint.base import (
|
||||
Checkpoint,
|
||||
PendingWrite,
|
||||
V,
|
||||
copy_checkpoint,
|
||||
)
|
||||
from langgraph.constants import (
|
||||
CONF,
|
||||
@@ -68,12 +68,10 @@ from langgraph.managed.base import ManagedValueMapping
|
||||
from langgraph.pregel.call import get_runnable_for_task
|
||||
from langgraph.pregel.io import read_channel, read_channels
|
||||
from langgraph.pregel.log import logger
|
||||
from langgraph.pregel.manager import ChannelsManager
|
||||
from langgraph.pregel.read import PregelNode
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import (
|
||||
All,
|
||||
LoopProtocol,
|
||||
PregelExecutableTask,
|
||||
PregelScratchpad,
|
||||
PregelTask,
|
||||
@@ -168,39 +166,39 @@ def should_interrupt(
|
||||
|
||||
|
||||
def local_read(
|
||||
step: int,
|
||||
checkpoint: Checkpoint,
|
||||
channels: Mapping[str, BaseChannel],
|
||||
managed: ManagedValueMapping,
|
||||
task: WritesProtocol,
|
||||
config: RunnableConfig,
|
||||
select: Union[list[str], str],
|
||||
fresh: bool = False,
|
||||
) -> Union[dict[str, Any], Any]:
|
||||
"""Function injected under CONFIG_KEY_READ in task config, to read current state.
|
||||
Used by conditional edges to read a copy of the state with reflecting the writes
|
||||
from that node only."""
|
||||
updated: dict[str, list[Any]] = defaultdict(list)
|
||||
if isinstance(select, str):
|
||||
managed_keys = []
|
||||
for c, _ in task.writes:
|
||||
for c, v in task.writes:
|
||||
if c == select:
|
||||
updated = {c}
|
||||
break
|
||||
else:
|
||||
updated = set()
|
||||
updated[c].append(v)
|
||||
else:
|
||||
managed_keys = [k for k in select if k in managed]
|
||||
select = [k for k in select if k not in managed]
|
||||
updated = set(select).intersection(c for c, _ in task.writes)
|
||||
for c, v in task.writes:
|
||||
if c in select:
|
||||
updated[c].append(v)
|
||||
if fresh and updated:
|
||||
with ChannelsManager(
|
||||
{k: v for k, v in channels.items() if k in updated},
|
||||
checkpoint,
|
||||
LoopProtocol(config=config, step=step, stop=step + 1),
|
||||
skip_context=True,
|
||||
) as (local_channels, _):
|
||||
apply_writes(copy_checkpoint(checkpoint), local_channels, [task], None)
|
||||
values = read_channels({**channels, **local_channels}, select)
|
||||
# apply writes
|
||||
local_channels: dict[str, BaseChannel] = {}
|
||||
for k in channels:
|
||||
if k in updated:
|
||||
cc = channels[k].copy()
|
||||
cc.update(updated[k])
|
||||
else:
|
||||
cc = channels[k]
|
||||
local_channels[k] = cc
|
||||
# read fresh values
|
||||
values = read_channels(local_channels, select)
|
||||
else:
|
||||
values = read_channels(channels, select)
|
||||
if managed_keys:
|
||||
@@ -334,6 +332,17 @@ def apply_writes(
|
||||
return pending_writes_by_managed, updated_channels
|
||||
|
||||
|
||||
def has_next_tasks(
|
||||
trigger_to_nodes: Mapping[str, Sequence[str]],
|
||||
updated_channels: set[str],
|
||||
checkpoint: Checkpoint,
|
||||
) -> bool:
|
||||
"""Check if there are any tasks that should be run in the next step."""
|
||||
return bool(checkpoint["pending_sends"]) or not updated_channels.isdisjoint(
|
||||
trigger_to_nodes
|
||||
)
|
||||
|
||||
|
||||
@overload
|
||||
def prepare_next_tasks(
|
||||
checkpoint: Checkpoint,
|
||||
@@ -561,12 +570,9 @@ def prepare_single_task(
|
||||
),
|
||||
CONFIG_KEY_READ: partial(
|
||||
local_read,
|
||||
step,
|
||||
checkpoint,
|
||||
channels,
|
||||
managed,
|
||||
PregelTaskWrites(task_path[:3], name, writes, triggers),
|
||||
config,
|
||||
),
|
||||
CONFIG_KEY_STORE: (store or configurable.get(CONFIG_KEY_STORE)),
|
||||
CONFIG_KEY_CHECKPOINTER: (
|
||||
@@ -666,14 +672,11 @@ def prepare_single_task(
|
||||
),
|
||||
CONFIG_KEY_READ: partial(
|
||||
local_read,
|
||||
step,
|
||||
checkpoint,
|
||||
channels,
|
||||
managed,
|
||||
PregelTaskWrites(
|
||||
task_path[:3], packet.node, writes, triggers
|
||||
),
|
||||
config,
|
||||
),
|
||||
CONFIG_KEY_STORE: (
|
||||
store or configurable.get(CONFIG_KEY_STORE)
|
||||
@@ -788,8 +791,6 @@ def prepare_single_task(
|
||||
),
|
||||
CONFIG_KEY_READ: partial(
|
||||
local_read,
|
||||
step,
|
||||
checkpoint,
|
||||
channels,
|
||||
managed,
|
||||
PregelTaskWrites(
|
||||
@@ -798,7 +799,6 @@ def prepare_single_task(
|
||||
writes,
|
||||
triggers,
|
||||
),
|
||||
config,
|
||||
),
|
||||
CONFIG_KEY_STORE: (
|
||||
store or configurable.get(CONFIG_KEY_STORE)
|
||||
@@ -869,11 +869,30 @@ def _scratchpad(
|
||||
pending_writes: list[PendingWrite],
|
||||
task_id: str,
|
||||
) -> PregelScratchpad:
|
||||
# None cannot be used as a resume value, because it would be difficult to
|
||||
# distinguish from missing when used over http
|
||||
null_resume_write = next(
|
||||
(w for w in pending_writes if w[0] == NULL_TASK_ID and w[1] == RESUME), None
|
||||
)
|
||||
if len(pending_writes) > 0:
|
||||
# find global resume value
|
||||
for w in pending_writes:
|
||||
if w[0] == NULL_TASK_ID and w[1] == RESUME:
|
||||
null_resume_write = w
|
||||
break
|
||||
else:
|
||||
# None cannot be used as a resume value, because it would be difficult to
|
||||
# distinguish from missing when used over http
|
||||
null_resume_write = None
|
||||
# find task-specific resume value
|
||||
for w in pending_writes:
|
||||
if w[0] == task_id and w[1] == RESUME:
|
||||
task_resume_write = w[2]
|
||||
if not isinstance(task_resume_write, list):
|
||||
task_resume_write = [task_resume_write]
|
||||
break
|
||||
else:
|
||||
task_resume_write = []
|
||||
# clear var
|
||||
del w
|
||||
else:
|
||||
null_resume_write = None
|
||||
task_resume_write = []
|
||||
|
||||
def get_null_resume(consume: bool = False) -> Any:
|
||||
if null_resume_write is None:
|
||||
@@ -891,15 +910,13 @@ def _scratchpad(
|
||||
# using itertools.count as an atomic counter (+= 1 is not thread-safe)
|
||||
return PregelScratchpad(
|
||||
# call
|
||||
call_counter=itertools.count(0).__next__,
|
||||
call_counter=LazyAtomicCounter(),
|
||||
# interrupt
|
||||
interrupt_counter=itertools.count(0).__next__,
|
||||
resume=next(
|
||||
(w[2] for w in pending_writes if w[0] == task_id and w[1] == RESUME), []
|
||||
),
|
||||
interrupt_counter=LazyAtomicCounter(),
|
||||
resume=task_resume_write,
|
||||
get_null_resume=get_null_resume,
|
||||
# subgraph
|
||||
subgraph_counter=itertools.count(0).__next__,
|
||||
subgraph_counter=LazyAtomicCounter(),
|
||||
)
|
||||
|
||||
|
||||
@@ -973,3 +990,22 @@ def task_path_str(tup: Union[str, int, tuple]) -> str:
|
||||
if isinstance(tup, int)
|
||||
else str(tup)
|
||||
)
|
||||
|
||||
|
||||
LAZY_ATOMIC_COUNTER_LOCK = threading.Lock()
|
||||
|
||||
|
||||
class LazyAtomicCounter:
|
||||
__slots__ = ("_counter",)
|
||||
|
||||
_counter: Optional[Callable[[], int]]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._counter = None
|
||||
|
||||
def __call__(self) -> int:
|
||||
if self._counter is None:
|
||||
with LAZY_ATOMIC_COUNTER_LOCK:
|
||||
if self._counter is None:
|
||||
self._counter = itertools.count(0).__next__
|
||||
return self._counter()
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
from datetime import datetime, timezone
|
||||
from typing import Mapping, Optional
|
||||
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.checkpoint.base import LATEST_VERSION, Checkpoint
|
||||
from langgraph.checkpoint.base.id import uuid6
|
||||
from langgraph.constants import MISSING
|
||||
|
||||
|
||||
def create_checkpoint(
|
||||
checkpoint: Checkpoint,
|
||||
channels: Optional[Mapping[str, BaseChannel]],
|
||||
step: int,
|
||||
*,
|
||||
id: Optional[str] = None,
|
||||
) -> Checkpoint:
|
||||
"""Create a checkpoint for the given channels."""
|
||||
ts = datetime.now(timezone.utc).isoformat()
|
||||
if channels is None:
|
||||
values = checkpoint["channel_values"]
|
||||
else:
|
||||
values = {}
|
||||
for k in channels:
|
||||
if k not in checkpoint["channel_versions"]:
|
||||
continue
|
||||
v = channels[k].checkpoint()
|
||||
if v is not MISSING:
|
||||
values[k] = v
|
||||
return Checkpoint(
|
||||
v=LATEST_VERSION,
|
||||
ts=ts,
|
||||
id=id or str(uuid6(clock_seq=step)),
|
||||
channel_values=values,
|
||||
channel_versions=checkpoint["channel_versions"],
|
||||
versions_seen=checkpoint["versions_seen"],
|
||||
pending_sends=checkpoint.get("pending_sends", []),
|
||||
)
|
||||
@@ -156,10 +156,16 @@ class AsyncBackgroundExecutor(AsyncContextManager):
|
||||
if self.semaphore:
|
||||
coro = gated(self.semaphore, coro)
|
||||
if CONTEXT_NOT_SUPPORTED:
|
||||
task = run_coroutine_threadsafe(coro, self.loop, name=__name__)
|
||||
task = run_coroutine_threadsafe(
|
||||
coro, self.loop, name=__name__, lazy=__next_tick__
|
||||
)
|
||||
else:
|
||||
task = run_coroutine_threadsafe(
|
||||
coro, self.loop, name=__name__, context=copy_context()
|
||||
coro,
|
||||
self.loop,
|
||||
name=__name__,
|
||||
context=copy_context(),
|
||||
lazy=__next_tick__,
|
||||
)
|
||||
self.tasks[task] = (__cancel_on_exit__, __reraise_on_exit__)
|
||||
task.add_done_callback(self.done)
|
||||
|
||||
@@ -38,7 +38,6 @@ from langgraph.checkpoint.base import (
|
||||
CheckpointTuple,
|
||||
PendingWrite,
|
||||
copy_checkpoint,
|
||||
create_checkpoint,
|
||||
empty_checkpoint,
|
||||
)
|
||||
from langgraph.constants import (
|
||||
@@ -88,6 +87,7 @@ from langgraph.pregel.algo import (
|
||||
should_interrupt,
|
||||
task_path_str,
|
||||
)
|
||||
from langgraph.pregel.checkpoint import create_checkpoint
|
||||
from langgraph.pregel.debug import (
|
||||
map_debug_checkpoint,
|
||||
map_debug_task_results,
|
||||
@@ -155,6 +155,8 @@ class PregelLoop(LoopProtocol):
|
||||
manager: Union[None, AsyncParentRunManager, ParentRunManager]
|
||||
interrupt_after: Union[All, Sequence[str]]
|
||||
interrupt_before: Union[All, Sequence[str]]
|
||||
checkpoint_every_step: bool
|
||||
debug: bool
|
||||
|
||||
checkpointer_get_next_version: GetNextVersion
|
||||
checkpointer_put_writes: Optional[
|
||||
@@ -211,6 +213,7 @@ class PregelLoop(LoopProtocol):
|
||||
input_model: Optional[Type[BaseModel]] = None,
|
||||
debug: bool = False,
|
||||
trigger_to_nodes: Optional[Mapping[str, Sequence[str]]] = None,
|
||||
checkpoint_every_step: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
step=0,
|
||||
@@ -235,6 +238,7 @@ class PregelLoop(LoopProtocol):
|
||||
or CONFIG_KEY_DEDUPE_TASKS in config[CONF]
|
||||
)
|
||||
self.trigger_to_nodes = trigger_to_nodes
|
||||
self.checkpoint_every_step = checkpoint_every_step
|
||||
self.debug = debug
|
||||
if self.stream is not None and CONFIG_KEY_STREAM in config[CONF]:
|
||||
self.stream = DuplexStream(self.stream, config[CONF][CONFIG_KEY_STREAM])
|
||||
@@ -703,8 +707,6 @@ class PregelLoop(LoopProtocol):
|
||||
return updated_channels
|
||||
|
||||
def _put_checkpoint(self, metadata: CheckpointMetadata) -> None:
|
||||
for k, v in self.config["metadata"].items():
|
||||
metadata.setdefault(k, v) # type: ignore
|
||||
# assign step and parents
|
||||
metadata["step"] = self.step
|
||||
metadata["parents"] = self.config[CONF].get(CONFIG_KEY_CHECKPOINT_MAP, {})
|
||||
@@ -719,10 +721,15 @@ class PregelLoop(LoopProtocol):
|
||||
else self.stream_keys
|
||||
),
|
||||
)
|
||||
# create new checkpoint
|
||||
self.checkpoint = create_checkpoint(self.checkpoint, self.channels, self.step)
|
||||
# bail if no checkpointer
|
||||
if self._checkpointer_put_after_previous is not None:
|
||||
for k, v in self.config["metadata"].items():
|
||||
metadata.setdefault(k, v) # type: ignore
|
||||
|
||||
# create new checkpoint
|
||||
self.checkpoint = create_checkpoint(
|
||||
self.checkpoint, self.channels, self.step
|
||||
)
|
||||
self.checkpoint_metadata = metadata
|
||||
|
||||
self.prev_checkpoint_config = (
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import AsyncIterator, Iterator, Mapping, Union
|
||||
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.checkpoint.base import Checkpoint
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.managed.base import (
|
||||
ConfiguredManagedValue,
|
||||
ManagedValueMapping,
|
||||
@@ -36,7 +37,7 @@ def ChannelsManager(
|
||||
with ExitStack() as stack:
|
||||
yield (
|
||||
{
|
||||
k: v.from_checkpoint(checkpoint["channel_values"].get(k))
|
||||
k: v.from_checkpoint(checkpoint["channel_values"].get(k, MISSING))
|
||||
for k, v in channel_specs.items()
|
||||
},
|
||||
ManagedValueMapping(
|
||||
@@ -90,7 +91,7 @@ async def AsyncChannelsManager(
|
||||
yield (
|
||||
# channels: enter each channel with checkpoint
|
||||
{
|
||||
k: v.from_checkpoint(checkpoint["channel_values"].get(k))
|
||||
k: v.from_checkpoint(checkpoint["channel_values"].get(k, MISSING))
|
||||
for k, v in channel_specs.items()
|
||||
},
|
||||
# managed: build mapping from spec to result
|
||||
|
||||
@@ -654,7 +654,9 @@ class RemoteGraph(PregelProtocol):
|
||||
# raise interrupt or errors
|
||||
if chunk.event.startswith("updates"):
|
||||
if isinstance(chunk.data, dict) and INTERRUPT in chunk.data:
|
||||
raise GraphInterrupt(chunk.data[INTERRUPT])
|
||||
raise GraphInterrupt(
|
||||
[Interrupt(**i) for i in chunk.data[INTERRUPT]]
|
||||
)
|
||||
elif chunk.event.startswith("error"):
|
||||
raise RemoteException(chunk.data)
|
||||
# filter for what was actually requested
|
||||
@@ -746,7 +748,9 @@ class RemoteGraph(PregelProtocol):
|
||||
# raise interrupt or errors
|
||||
if chunk.event.startswith("updates"):
|
||||
if isinstance(chunk.data, dict) and INTERRUPT in chunk.data:
|
||||
raise GraphInterrupt(chunk.data[INTERRUPT])
|
||||
raise GraphInterrupt(
|
||||
[Interrupt(**i) for i in chunk.data[INTERRUPT]]
|
||||
)
|
||||
elif chunk.event.startswith("error"):
|
||||
raise RemoteException(chunk.data)
|
||||
# filter for what was actually requested
|
||||
|
||||
@@ -44,6 +44,18 @@ from langgraph.utils.future import chain_future
|
||||
F = TypeVar("F", concurrent.futures.Future, asyncio.Future)
|
||||
E = TypeVar("E", threading.Event, asyncio.Event)
|
||||
|
||||
# List of filenames to exclude from exception traceback
|
||||
# Note: Frames will be removed if they are the last frame in traceback, recursively
|
||||
EXCLUDED_FRAME_FNAMES = (
|
||||
"langgraph/pregel/retry.py",
|
||||
"langgraph/pregel/runner.py",
|
||||
"langgraph/pregel/executor.py",
|
||||
"langgraph/utils/runnable.py",
|
||||
"langchain_core/runnables/config.py",
|
||||
"concurrent/futures/thread.py",
|
||||
"concurrent/futures/_base.py",
|
||||
)
|
||||
|
||||
|
||||
class FuturesDict(Generic[F, E], dict[F, Optional[PregelExecutableTask]]):
|
||||
event: E
|
||||
@@ -167,6 +179,13 @@ class PregelRunner:
|
||||
fut.set_exception(exc)
|
||||
futures.done.add(fut)
|
||||
elif reraise:
|
||||
if tb := exc.__traceback__:
|
||||
while tb.tb_next is not None and any(
|
||||
tb.tb_frame.f_code.co_filename.endswith(name)
|
||||
for name in EXCLUDED_FRAME_FNAMES
|
||||
):
|
||||
tb = tb.tb_next
|
||||
exc.__traceback__ = tb
|
||||
raise
|
||||
if not futures: # maybe `t` schuduled another task
|
||||
return
|
||||
@@ -229,10 +248,20 @@ class PregelRunner:
|
||||
# give control back to the caller
|
||||
yield
|
||||
# panic on failure or timeout
|
||||
_panic_or_proceed(
|
||||
futures.done.union(f for f, t in futures.items() if t is not None),
|
||||
panic=reraise,
|
||||
)
|
||||
try:
|
||||
_panic_or_proceed(
|
||||
futures.done.union(f for f, t in futures.items() if t is not None),
|
||||
panic=reraise,
|
||||
)
|
||||
except Exception as exc:
|
||||
if tb := exc.__traceback__:
|
||||
while tb.tb_next is not None and any(
|
||||
tb.tb_frame.f_code.co_filename.endswith(name)
|
||||
for name in EXCLUDED_FRAME_FNAMES
|
||||
):
|
||||
tb = tb.tb_next
|
||||
exc.__traceback__ = tb
|
||||
raise
|
||||
|
||||
async def atick(
|
||||
self,
|
||||
@@ -283,6 +312,13 @@ class PregelRunner:
|
||||
fut.set_exception(exc)
|
||||
futures.done.add(fut)
|
||||
elif reraise:
|
||||
if tb := exc.__traceback__:
|
||||
while tb.tb_next is not None and any(
|
||||
tb.tb_frame.f_code.co_filename.endswith(name)
|
||||
for name in EXCLUDED_FRAME_FNAMES
|
||||
):
|
||||
tb = tb.tb_next
|
||||
exc.__traceback__ = tb
|
||||
raise
|
||||
if not futures: # maybe `t` schuduled another task
|
||||
return
|
||||
@@ -357,11 +393,21 @@ class PregelRunner:
|
||||
for fut in futures:
|
||||
fut.cancel()
|
||||
# panic on failure or timeout
|
||||
_panic_or_proceed(
|
||||
futures.done.union(f for f, t in futures.items() if t is not None),
|
||||
timeout_exc_cls=asyncio.TimeoutError,
|
||||
panic=reraise,
|
||||
)
|
||||
try:
|
||||
_panic_or_proceed(
|
||||
futures.done.union(f for f, t in futures.items() if t is not None),
|
||||
timeout_exc_cls=asyncio.TimeoutError,
|
||||
panic=reraise,
|
||||
)
|
||||
except Exception as exc:
|
||||
if tb := exc.__traceback__:
|
||||
while tb.tb_next is not None and any(
|
||||
tb.tb_frame.f_code.co_filename.endswith(name)
|
||||
for name in EXCLUDED_FRAME_FNAMES
|
||||
):
|
||||
tb = tb.tb_next
|
||||
exc.__traceback__ = tb
|
||||
raise
|
||||
|
||||
def commit(
|
||||
self,
|
||||
|
||||
@@ -101,7 +101,10 @@ def default_retry_on(exc: Exception) -> bool:
|
||||
|
||||
|
||||
class RetryPolicy(NamedTuple):
|
||||
"""Configuration for retrying nodes."""
|
||||
"""Configuration for retrying nodes.
|
||||
|
||||
!!! version-added "Added in version 0.2.24."
|
||||
"""
|
||||
|
||||
initial_interval: float = 0.5
|
||||
"""Amount of time that must elapse before the first retry occurs. In seconds."""
|
||||
@@ -120,13 +123,20 @@ class RetryPolicy(NamedTuple):
|
||||
|
||||
|
||||
class CachePolicy(NamedTuple):
|
||||
"""Configuration for caching nodes."""
|
||||
"""Configuration for caching nodes.
|
||||
|
||||
!!! version-added "Added in version 0.2.24."
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@dataclasses.dataclass(**_DC_KWARGS)
|
||||
class Interrupt:
|
||||
"""
|
||||
!!! version-added "Added in version 0.2.24."
|
||||
"""
|
||||
|
||||
value: Any
|
||||
resumable: bool = False
|
||||
ns: Optional[Sequence[str]] = None
|
||||
@@ -268,6 +278,8 @@ N = TypeVar("N", bound=Hashable)
|
||||
class Command(Generic[N], ToolOutputMixin):
|
||||
"""One or more commands to update the graph's state and send messages to nodes.
|
||||
|
||||
!!! version-added "Added in version 0.2.24."
|
||||
|
||||
Args:
|
||||
graph: graph to send the command to. Supported values are:
|
||||
|
||||
@@ -357,7 +369,7 @@ class LoopProtocol:
|
||||
self.stop = stop
|
||||
|
||||
|
||||
@dataclasses.dataclass(**{**_DC_KWARGS, "frozen": False})
|
||||
@dataclasses.dataclass(**_DC_KWARGS)
|
||||
class PregelScratchpad:
|
||||
# call
|
||||
call_counter: Callable[[], int]
|
||||
|
||||
@@ -10,6 +10,7 @@ T = TypeVar("T")
|
||||
AnyFuture = Union[asyncio.Future, concurrent.futures.Future]
|
||||
|
||||
CONTEXT_NOT_SUPPORTED = sys.version_info < (3, 11)
|
||||
EAGER_NOT_SUPPORTED = sys.version_info < (3, 12)
|
||||
|
||||
|
||||
def _get_loop(fut: asyncio.Future) -> asyncio.AbstractEventLoop:
|
||||
@@ -142,6 +143,7 @@ def _ensure_future(
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
name: Optional[str] = None,
|
||||
context: Optional[contextvars.Context] = None,
|
||||
lazy: bool = True,
|
||||
) -> asyncio.Task[T]:
|
||||
called_wrap_awaitable = False
|
||||
if not asyncio.iscoroutine(coro_or_future):
|
||||
@@ -159,8 +161,12 @@ def _ensure_future(
|
||||
try:
|
||||
if CONTEXT_NOT_SUPPORTED:
|
||||
return loop.create_task(coro_or_future, name=name)
|
||||
else:
|
||||
elif EAGER_NOT_SUPPORTED or lazy:
|
||||
return loop.create_task(coro_or_future, name=name, context=context)
|
||||
else:
|
||||
return asyncio.eager_task_factory(
|
||||
loop, coro_or_future, name=name, context=context
|
||||
)
|
||||
except RuntimeError:
|
||||
if not called_wrap_awaitable:
|
||||
coro_or_future.close()
|
||||
@@ -180,6 +186,8 @@ def _wrap_awaitable(awaitable: Awaitable[T]) -> Generator[None, None, T]:
|
||||
def run_coroutine_threadsafe(
|
||||
coro: Coroutine[None, None, T],
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
*,
|
||||
lazy: bool,
|
||||
name: Optional[str] = None,
|
||||
context: Optional[contextvars.Context] = None,
|
||||
) -> asyncio.Future[T]:
|
||||
@@ -187,18 +195,23 @@ def run_coroutine_threadsafe(
|
||||
|
||||
Return a asyncio.Future to access the result.
|
||||
"""
|
||||
future: asyncio.Future[T] = asyncio.Future(loop=loop)
|
||||
|
||||
def callback() -> None:
|
||||
try:
|
||||
chain_future(
|
||||
_ensure_future(coro, loop=loop, name=name, context=context), future
|
||||
)
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
raise
|
||||
except BaseException as exc:
|
||||
future.set_exception(exc)
|
||||
raise
|
||||
if asyncio._get_running_loop() is loop:
|
||||
return _ensure_future(coro, loop=loop, name=name, context=context, lazy=lazy)
|
||||
else:
|
||||
future: asyncio.Future[T] = asyncio.Future(loop=loop)
|
||||
|
||||
loop.call_soon_threadsafe(callback, context=context)
|
||||
return future
|
||||
def callback() -> None:
|
||||
try:
|
||||
chain_future(
|
||||
_ensure_future(coro, loop=loop, name=name, context=context),
|
||||
future,
|
||||
)
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
raise
|
||||
except BaseException as exc:
|
||||
future.set_exception(exc)
|
||||
raise
|
||||
|
||||
loop.call_soon_threadsafe(callback, context=context)
|
||||
return future
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph"
|
||||
version = "0.3.20"
|
||||
version = "0.3.21"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -6,13 +6,14 @@ import pytest
|
||||
from langgraph.channels.binop import BinaryOperatorAggregate
|
||||
from langgraph.channels.last_value import LastValue
|
||||
from langgraph.channels.topic import Topic
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError, InvalidUpdateError
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
def test_last_value() -> None:
|
||||
channel = LastValue(int).from_checkpoint(None)
|
||||
channel = LastValue(int).from_checkpoint(MISSING)
|
||||
assert channel.ValueType is int
|
||||
assert channel.UpdateType is int
|
||||
|
||||
@@ -31,7 +32,7 @@ def test_last_value() -> None:
|
||||
|
||||
|
||||
def test_topic() -> None:
|
||||
channel = Topic(str).from_checkpoint(None)
|
||||
channel = Topic(str).from_checkpoint(MISSING)
|
||||
assert channel.ValueType is Sequence[str]
|
||||
assert channel.UpdateType is Union[str, list[str]]
|
||||
|
||||
@@ -55,7 +56,7 @@ def test_topic() -> None:
|
||||
|
||||
|
||||
def test_topic_accumulate() -> None:
|
||||
channel = Topic(str, accumulate=True).from_checkpoint(None)
|
||||
channel = Topic(str, accumulate=True).from_checkpoint(MISSING)
|
||||
assert channel.ValueType is Sequence[str]
|
||||
assert channel.UpdateType is Union[str, list[str]]
|
||||
|
||||
@@ -73,7 +74,7 @@ def test_topic_accumulate() -> None:
|
||||
|
||||
|
||||
def test_binop() -> None:
|
||||
channel = BinaryOperatorAggregate(int, operator.add).from_checkpoint(None)
|
||||
channel = BinaryOperatorAggregate(int, operator.add).from_checkpoint(MISSING)
|
||||
assert channel.ValueType is int
|
||||
assert channel.UpdateType is int
|
||||
|
||||
|
||||
@@ -2483,7 +2483,7 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
|
||||
{
|
||||
"langgraph_step": 1,
|
||||
"langgraph_node": "agent",
|
||||
"langgraph_triggers": ("branch:to:agent", "start:agent", "tools"),
|
||||
"langgraph_triggers": ("branch:to:agent",),
|
||||
"langgraph_path": (PULL, "agent"),
|
||||
"langgraph_checkpoint_ns": AnyStr("agent:"),
|
||||
"checkpoint_ns": AnyStr("agent:"),
|
||||
@@ -2542,7 +2542,7 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
|
||||
{
|
||||
"langgraph_step": 3,
|
||||
"langgraph_node": "agent",
|
||||
"langgraph_triggers": ("branch:to:agent", "start:agent", "tools"),
|
||||
"langgraph_triggers": ("branch:to:agent",),
|
||||
"langgraph_path": (PULL, "agent"),
|
||||
"langgraph_checkpoint_ns": AnyStr("agent:"),
|
||||
"checkpoint_ns": AnyStr("agent:"),
|
||||
@@ -2585,7 +2585,7 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
|
||||
{
|
||||
"langgraph_step": 5,
|
||||
"langgraph_node": "agent",
|
||||
"langgraph_triggers": ("branch:to:agent", "start:agent", "tools"),
|
||||
"langgraph_triggers": ("branch:to:agent",),
|
||||
"langgraph_path": (PULL, "agent"),
|
||||
"langgraph_checkpoint_ns": AnyStr("agent:"),
|
||||
"checkpoint_ns": AnyStr("agent:"),
|
||||
@@ -4660,7 +4660,7 @@ def test_root_graph(
|
||||
content="result for query",
|
||||
name="search_api",
|
||||
tool_call_id="tool_call123",
|
||||
id="00000000-0000-4000-8000-000000000037",
|
||||
id="00000000-0000-4000-8000-000000000040",
|
||||
)
|
||||
]
|
||||
},
|
||||
@@ -4683,7 +4683,7 @@ def test_root_graph(
|
||||
content="result for another",
|
||||
name="search_api",
|
||||
tool_call_id="tool_call456",
|
||||
id="00000000-0000-4000-8000-000000000045",
|
||||
id="00000000-0000-4000-8000-000000000049",
|
||||
)
|
||||
]
|
||||
},
|
||||
@@ -5387,7 +5387,7 @@ def test_root_graph(
|
||||
"__root__": [
|
||||
HumanMessage(
|
||||
content="what is weather in sf",
|
||||
id="00000000-0000-4000-8000-000000000078",
|
||||
id="00000000-0000-4000-8000-000000000083",
|
||||
),
|
||||
AIMessage(
|
||||
content="",
|
||||
@@ -5407,7 +5407,7 @@ def test_root_graph(
|
||||
),
|
||||
AIMessage(content="answer", id="ai2"),
|
||||
AIMessage(
|
||||
content="an extra message", id="00000000-0000-4000-8000-000000000100"
|
||||
content="an extra message", id="00000000-0000-4000-8000-000000000107"
|
||||
),
|
||||
HumanMessage(content="what is weather in la"),
|
||||
],
|
||||
@@ -5501,10 +5501,7 @@ def test_in_one_fan_out_out_one_graph_state() -> None:
|
||||
"id": AnyStr(),
|
||||
"name": "rewrite_query",
|
||||
"input": {"query": "what is weather in sf", "docs": []},
|
||||
"triggers": (
|
||||
"branch:to:rewrite_query",
|
||||
"start:rewrite_query",
|
||||
),
|
||||
"triggers": ("branch:to:rewrite_query",),
|
||||
},
|
||||
},
|
||||
),
|
||||
@@ -5535,10 +5532,7 @@ def test_in_one_fan_out_out_one_graph_state() -> None:
|
||||
"id": AnyStr(),
|
||||
"name": "retriever_one",
|
||||
"input": {"query": "query: what is weather in sf", "docs": []},
|
||||
"triggers": (
|
||||
"branch:to:retriever_one",
|
||||
"rewrite_query",
|
||||
),
|
||||
"triggers": ("branch:to:retriever_one",),
|
||||
},
|
||||
},
|
||||
),
|
||||
@@ -5552,10 +5546,7 @@ def test_in_one_fan_out_out_one_graph_state() -> None:
|
||||
"id": AnyStr(),
|
||||
"name": "retriever_two",
|
||||
"input": {"query": "query: what is weather in sf", "docs": []},
|
||||
"triggers": (
|
||||
"branch:to:retriever_two",
|
||||
"rewrite_query",
|
||||
),
|
||||
"triggers": ("branch:to:retriever_two",),
|
||||
},
|
||||
},
|
||||
),
|
||||
@@ -5617,7 +5608,7 @@ def test_in_one_fan_out_out_one_graph_state() -> None:
|
||||
"query": "query: what is weather in sf",
|
||||
"docs": ["doc1", "doc2", "doc3", "doc4"],
|
||||
},
|
||||
"triggers": ("branch:to:qa", "retriever_one", "retriever_two"),
|
||||
"triggers": ("branch:to:qa",),
|
||||
},
|
||||
},
|
||||
),
|
||||
@@ -6643,7 +6634,7 @@ def test_branch_then(
|
||||
"id": AnyStr(),
|
||||
"name": "prepare",
|
||||
"input": {"my_key": "value", "market": "DE"},
|
||||
"triggers": ("branch:to:prepare", "start:prepare"),
|
||||
"triggers": ("branch:to:prepare",),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -7795,7 +7786,7 @@ def test_nested_graph_state(
|
||||
"langgraph_node": "inner",
|
||||
"langgraph_path": [PULL, "inner"],
|
||||
"langgraph_step": 2,
|
||||
"langgraph_triggers": ["branch:to:inner", "outer_1"],
|
||||
"langgraph_triggers": ["branch:to:inner"],
|
||||
"langgraph_checkpoint_ns": AnyStr("inner:"),
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
@@ -7990,7 +7981,7 @@ def test_nested_graph_state(
|
||||
"langgraph_node": "inner",
|
||||
"langgraph_path": [PULL, "inner"],
|
||||
"langgraph_step": 2,
|
||||
"langgraph_triggers": ["branch:to:inner", "outer_1"],
|
||||
"langgraph_triggers": ["branch:to:inner"],
|
||||
"langgraph_checkpoint_ns": AnyStr("inner:"),
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
@@ -8033,7 +8024,7 @@ def test_nested_graph_state(
|
||||
"langgraph_node": "inner",
|
||||
"langgraph_path": [PULL, "inner"],
|
||||
"langgraph_step": 2,
|
||||
"langgraph_triggers": ["branch:to:inner", "outer_1"],
|
||||
"langgraph_triggers": ["branch:to:inner"],
|
||||
"langgraph_checkpoint_ns": AnyStr("inner:"),
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
@@ -8082,7 +8073,7 @@ def test_nested_graph_state(
|
||||
"langgraph_node": "inner",
|
||||
"langgraph_path": [PULL, "inner"],
|
||||
"langgraph_step": 2,
|
||||
"langgraph_triggers": ["branch:to:inner", "outer_1"],
|
||||
"langgraph_triggers": ["branch:to:inner"],
|
||||
"langgraph_checkpoint_ns": AnyStr("inner:"),
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
@@ -8516,7 +8507,7 @@ def test_doubly_nested_graph_state(
|
||||
"langgraph_node": "child_1",
|
||||
"langgraph_path": [PULL, AnyStr("child_1")],
|
||||
"langgraph_step": 1,
|
||||
"langgraph_triggers": ["branch:to:child_1", AnyStr("start:child_1")],
|
||||
"langgraph_triggers": ["branch:to:child_1"],
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
parent_config=(
|
||||
@@ -8602,7 +8593,6 @@ def test_doubly_nested_graph_state(
|
||||
"langgraph_step": 1,
|
||||
"langgraph_triggers": [
|
||||
"branch:to:child_1",
|
||||
AnyStr("start:child_1"),
|
||||
],
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
@@ -8650,7 +8640,7 @@ def test_doubly_nested_graph_state(
|
||||
"langgraph_node": "child",
|
||||
"langgraph_path": [PULL, AnyStr("child")],
|
||||
"langgraph_step": 2,
|
||||
"langgraph_triggers": ["branch:to:child", AnyStr("parent_1")],
|
||||
"langgraph_triggers": ["branch:to:child"],
|
||||
"langgraph_checkpoint_ns": AnyStr("child:"),
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
@@ -8946,7 +8936,7 @@ def test_doubly_nested_graph_state(
|
||||
"langgraph_node": "child",
|
||||
"langgraph_path": [PULL, AnyStr("child")],
|
||||
"langgraph_step": 2,
|
||||
"langgraph_triggers": ["branch:to:child", AnyStr("parent_1")],
|
||||
"langgraph_triggers": ["branch:to:child"],
|
||||
"langgraph_checkpoint_ns": AnyStr("child:"),
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
@@ -8985,7 +8975,7 @@ def test_doubly_nested_graph_state(
|
||||
"langgraph_node": "child",
|
||||
"langgraph_path": [PULL, AnyStr("child")],
|
||||
"langgraph_step": 2,
|
||||
"langgraph_triggers": ["branch:to:child", AnyStr("parent_1")],
|
||||
"langgraph_triggers": ["branch:to:child"],
|
||||
"langgraph_checkpoint_ns": AnyStr("child:"),
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
@@ -9037,7 +9027,7 @@ def test_doubly_nested_graph_state(
|
||||
"langgraph_node": "child",
|
||||
"langgraph_path": [PULL, AnyStr("child")],
|
||||
"langgraph_step": 2,
|
||||
"langgraph_triggers": ["branch:to:child", AnyStr("parent_1")],
|
||||
"langgraph_triggers": ["branch:to:child"],
|
||||
"langgraph_checkpoint_ns": AnyStr("child:"),
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
@@ -9091,7 +9081,7 @@ def test_doubly_nested_graph_state(
|
||||
AnyStr("child_1"),
|
||||
],
|
||||
"langgraph_step": 1,
|
||||
"langgraph_triggers": ["branch:to:child_1", AnyStr("start:child_1")],
|
||||
"langgraph_triggers": ["branch:to:child_1"],
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
parent_config={
|
||||
@@ -9146,7 +9136,7 @@ def test_doubly_nested_graph_state(
|
||||
AnyStr("child_1"),
|
||||
],
|
||||
"langgraph_step": 1,
|
||||
"langgraph_triggers": ["branch:to:child_1", AnyStr("start:child_1")],
|
||||
"langgraph_triggers": ["branch:to:child_1"],
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
parent_config={
|
||||
@@ -9208,7 +9198,7 @@ def test_doubly_nested_graph_state(
|
||||
AnyStr("child_1"),
|
||||
],
|
||||
"langgraph_step": 1,
|
||||
"langgraph_triggers": ["branch:to:child_1", AnyStr("start:child_1")],
|
||||
"langgraph_triggers": ["branch:to:child_1"],
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
parent_config={
|
||||
@@ -9270,7 +9260,7 @@ def test_doubly_nested_graph_state(
|
||||
AnyStr("child_1"),
|
||||
],
|
||||
"langgraph_step": 1,
|
||||
"langgraph_triggers": ["branch:to:child_1", AnyStr("start:child_1")],
|
||||
"langgraph_triggers": ["branch:to:child_1"],
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
parent_config=None,
|
||||
|
||||
@@ -2300,11 +2300,7 @@ async def test_prebuilt_tool_chat() -> None:
|
||||
{
|
||||
"langgraph_step": 1,
|
||||
"langgraph_node": "agent",
|
||||
"langgraph_triggers": (
|
||||
"branch:to:agent",
|
||||
"start:agent",
|
||||
"tools",
|
||||
),
|
||||
"langgraph_triggers": ("branch:to:agent",),
|
||||
"langgraph_path": ("__pregel_pull", "agent"),
|
||||
"langgraph_checkpoint_ns": AnyStr("agent:"),
|
||||
"checkpoint_ns": AnyStr("agent:"),
|
||||
@@ -2363,11 +2359,7 @@ async def test_prebuilt_tool_chat() -> None:
|
||||
{
|
||||
"langgraph_step": 3,
|
||||
"langgraph_node": "agent",
|
||||
"langgraph_triggers": (
|
||||
"branch:to:agent",
|
||||
"start:agent",
|
||||
"tools",
|
||||
),
|
||||
"langgraph_triggers": ("branch:to:agent",),
|
||||
"langgraph_path": ("__pregel_pull", "agent"),
|
||||
"langgraph_checkpoint_ns": AnyStr("agent:"),
|
||||
"checkpoint_ns": AnyStr("agent:"),
|
||||
@@ -2410,11 +2402,7 @@ async def test_prebuilt_tool_chat() -> None:
|
||||
{
|
||||
"langgraph_step": 5,
|
||||
"langgraph_node": "agent",
|
||||
"langgraph_triggers": (
|
||||
"branch:to:agent",
|
||||
"start:agent",
|
||||
"tools",
|
||||
),
|
||||
"langgraph_triggers": ("branch:to:agent",),
|
||||
"langgraph_path": ("__pregel_pull", "agent"),
|
||||
"langgraph_checkpoint_ns": AnyStr("agent:"),
|
||||
"checkpoint_ns": AnyStr("agent:"),
|
||||
@@ -3895,10 +3883,7 @@ async def test_in_one_fan_out_out_one_graph_state() -> None:
|
||||
"id": AnyStr(),
|
||||
"name": "rewrite_query",
|
||||
"input": {"query": "what is weather in sf", "docs": []},
|
||||
"triggers": (
|
||||
"branch:to:rewrite_query",
|
||||
"start:rewrite_query",
|
||||
),
|
||||
"triggers": ("branch:to:rewrite_query",),
|
||||
},
|
||||
},
|
||||
),
|
||||
@@ -3929,10 +3914,7 @@ async def test_in_one_fan_out_out_one_graph_state() -> None:
|
||||
"id": AnyStr(),
|
||||
"name": "retriever_one",
|
||||
"input": {"query": "query: what is weather in sf", "docs": []},
|
||||
"triggers": (
|
||||
"branch:to:retriever_one",
|
||||
"rewrite_query",
|
||||
),
|
||||
"triggers": ("branch:to:retriever_one",),
|
||||
},
|
||||
},
|
||||
),
|
||||
@@ -3946,10 +3928,7 @@ async def test_in_one_fan_out_out_one_graph_state() -> None:
|
||||
"id": AnyStr(),
|
||||
"name": "retriever_two",
|
||||
"input": {"query": "query: what is weather in sf", "docs": []},
|
||||
"triggers": (
|
||||
"branch:to:retriever_two",
|
||||
"rewrite_query",
|
||||
),
|
||||
"triggers": ("branch:to:retriever_two",),
|
||||
},
|
||||
},
|
||||
),
|
||||
@@ -4011,7 +3990,7 @@ async def test_in_one_fan_out_out_one_graph_state() -> None:
|
||||
"query": "query: what is weather in sf",
|
||||
"docs": ["doc1", "doc2", "doc3", "doc4"],
|
||||
},
|
||||
"triggers": ("branch:to:qa", "retriever_one", "retriever_two"),
|
||||
"triggers": ("branch:to:qa",),
|
||||
},
|
||||
},
|
||||
),
|
||||
@@ -4486,10 +4465,7 @@ async def test_branch_then(checkpointer_name: str) -> None:
|
||||
"id": AnyStr(),
|
||||
"name": "prepare",
|
||||
"input": {"my_key": "value", "market": "DE"},
|
||||
"triggers": (
|
||||
"branch:to:prepare",
|
||||
"start:prepare",
|
||||
),
|
||||
"triggers": ("branch:to:prepare",),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -4805,10 +4781,7 @@ async def test_branch_then(checkpointer_name: str) -> None:
|
||||
"id": AnyStr(),
|
||||
"name": "prepare",
|
||||
"input": {"my_key": "value", "market": "DE"},
|
||||
"triggers": (
|
||||
"branch:to:prepare",
|
||||
"start:prepare",
|
||||
),
|
||||
"triggers": ("branch:to:prepare",),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -5363,7 +5336,7 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"langgraph_node": "inner",
|
||||
"langgraph_path": [PULL, "inner"],
|
||||
"langgraph_step": 2,
|
||||
"langgraph_triggers": ["branch:to:inner", "outer_1"],
|
||||
"langgraph_triggers": ["branch:to:inner"],
|
||||
"langgraph_checkpoint_ns": AnyStr("inner:"),
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
@@ -5560,7 +5533,7 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"langgraph_node": "inner",
|
||||
"langgraph_path": [PULL, "inner"],
|
||||
"langgraph_step": 2,
|
||||
"langgraph_triggers": ["branch:to:inner", "outer_1"],
|
||||
"langgraph_triggers": ["branch:to:inner"],
|
||||
"langgraph_checkpoint_ns": AnyStr("inner:"),
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
@@ -5603,7 +5576,7 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"langgraph_node": "inner",
|
||||
"langgraph_path": [PULL, "inner"],
|
||||
"langgraph_step": 2,
|
||||
"langgraph_triggers": ["branch:to:inner", "outer_1"],
|
||||
"langgraph_triggers": ["branch:to:inner"],
|
||||
"langgraph_checkpoint_ns": AnyStr("inner:"),
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
@@ -5652,7 +5625,7 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"langgraph_node": "inner",
|
||||
"langgraph_path": [PULL, "inner"],
|
||||
"langgraph_step": 2,
|
||||
"langgraph_triggers": ["branch:to:inner", "outer_1"],
|
||||
"langgraph_triggers": ["branch:to:inner"],
|
||||
"langgraph_checkpoint_ns": AnyStr("inner:"),
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
@@ -6090,7 +6063,9 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"langgraph_node": "child_1",
|
||||
"langgraph_path": [PULL, AnyStr("child_1")],
|
||||
"langgraph_step": 1,
|
||||
"langgraph_triggers": ["branch:to:child_1", "start:child_1"],
|
||||
"langgraph_triggers": [
|
||||
"branch:to:child_1",
|
||||
],
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
parent_config=(
|
||||
@@ -6178,7 +6153,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"langgraph_step": 1,
|
||||
"langgraph_triggers": [
|
||||
"branch:to:child_1",
|
||||
"start:child_1",
|
||||
],
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
@@ -6230,7 +6204,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"langgraph_step": 2,
|
||||
"langgraph_triggers": [
|
||||
"branch:to:child",
|
||||
AnyStr("parent_1"),
|
||||
],
|
||||
"langgraph_checkpoint_ns": AnyStr("child:"),
|
||||
},
|
||||
@@ -6529,7 +6502,7 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"langgraph_node": "child",
|
||||
"langgraph_path": [PULL, AnyStr("child")],
|
||||
"langgraph_step": 2,
|
||||
"langgraph_triggers": ["branch:to:child", AnyStr("parent_1")],
|
||||
"langgraph_triggers": ["branch:to:child"],
|
||||
"langgraph_checkpoint_ns": AnyStr("child:"),
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
@@ -6568,7 +6541,7 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"langgraph_node": "child",
|
||||
"langgraph_path": [PULL, AnyStr("child")],
|
||||
"langgraph_step": 2,
|
||||
"langgraph_triggers": ["branch:to:child", AnyStr("parent_1")],
|
||||
"langgraph_triggers": ["branch:to:child"],
|
||||
"langgraph_checkpoint_ns": AnyStr("child:"),
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
@@ -6620,7 +6593,7 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"langgraph_node": "child",
|
||||
"langgraph_path": [PULL, AnyStr("child")],
|
||||
"langgraph_step": 2,
|
||||
"langgraph_triggers": ["branch:to:child", AnyStr("parent_1")],
|
||||
"langgraph_triggers": ["branch:to:child"],
|
||||
"langgraph_checkpoint_ns": AnyStr("child:"),
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
@@ -6680,7 +6653,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"langgraph_step": 1,
|
||||
"langgraph_triggers": [
|
||||
"branch:to:child_1",
|
||||
AnyStr("start:child_1"),
|
||||
],
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
@@ -6738,7 +6710,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"langgraph_step": 1,
|
||||
"langgraph_triggers": [
|
||||
"branch:to:child_1",
|
||||
AnyStr("start:child_1"),
|
||||
],
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
@@ -6803,7 +6774,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"langgraph_step": 1,
|
||||
"langgraph_triggers": [
|
||||
"branch:to:child_1",
|
||||
AnyStr("start:child_1"),
|
||||
],
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
@@ -6868,7 +6838,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"langgraph_step": 1,
|
||||
"langgraph_triggers": [
|
||||
"branch:to:child_1",
|
||||
AnyStr("start:child_1"),
|
||||
],
|
||||
},
|
||||
created_at=AnyStr(),
|
||||
|
||||
@@ -1187,15 +1187,13 @@ def test_pending_writes_resume(
|
||||
assert checkpoint is not None
|
||||
# should contain error from "two"
|
||||
expected_writes = [
|
||||
(AnyStr(), "one", "one"),
|
||||
(AnyStr(), "value", 2),
|
||||
(AnyStr(), ERROR, 'ConnectionError("I\'m not good")'),
|
||||
]
|
||||
assert len(checkpoint.pending_writes) == 3
|
||||
assert len(checkpoint.pending_writes) == 2
|
||||
assert all(w in expected_writes for w in checkpoint.pending_writes)
|
||||
# both non-error pending writes come from same task
|
||||
non_error_writes = [w for w in checkpoint.pending_writes if w[1] != ERROR]
|
||||
assert non_error_writes[0][0] == non_error_writes[1][0]
|
||||
# error write is from the other task
|
||||
error_write = next(w for w in checkpoint.pending_writes if w[1] == ERROR)
|
||||
assert error_write[0] != non_error_writes[0][0]
|
||||
@@ -1242,10 +1240,10 @@ def test_pending_writes_resume(
|
||||
"pending_sends": [],
|
||||
"versions_seen": {
|
||||
"one": {
|
||||
"start:one": AnyVersion(),
|
||||
"branch:to:one": AnyVersion(),
|
||||
},
|
||||
"two": {
|
||||
"start:two": AnyVersion(),
|
||||
"branch:to:two": AnyVersion(),
|
||||
},
|
||||
"__input__": {},
|
||||
"__start__": {
|
||||
@@ -1254,19 +1252,17 @@ def test_pending_writes_resume(
|
||||
"__interrupt__": {
|
||||
"value": AnyVersion(),
|
||||
"__start__": AnyVersion(),
|
||||
"start:one": AnyVersion(),
|
||||
"start:two": AnyVersion(),
|
||||
"branch:to:one": AnyVersion(),
|
||||
"branch:to:two": AnyVersion(),
|
||||
},
|
||||
},
|
||||
"channel_versions": {
|
||||
"one": AnyVersion(),
|
||||
"two": AnyVersion(),
|
||||
"value": AnyVersion(),
|
||||
"__start__": AnyVersion(),
|
||||
"start:one": AnyVersion(),
|
||||
"start:two": AnyVersion(),
|
||||
"branch:to:one": AnyVersion(),
|
||||
"branch:to:two": AnyVersion(),
|
||||
},
|
||||
"channel_values": {"one": "one", "two": "two", "value": 6},
|
||||
"channel_values": {"value": 6},
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
@@ -1309,13 +1305,13 @@ def test_pending_writes_resume(
|
||||
"channel_versions": {
|
||||
"value": AnyVersion(),
|
||||
"__start__": AnyVersion(),
|
||||
"start:one": AnyVersion(),
|
||||
"start:two": AnyVersion(),
|
||||
"branch:to:one": AnyVersion(),
|
||||
"branch:to:two": AnyVersion(),
|
||||
},
|
||||
"channel_values": {
|
||||
"value": 1,
|
||||
"start:one": "__start__",
|
||||
"start:two": "__start__",
|
||||
"branch:to:one": None,
|
||||
"branch:to:two": None,
|
||||
},
|
||||
},
|
||||
metadata={
|
||||
@@ -1333,10 +1329,8 @@ def test_pending_writes_resume(
|
||||
}
|
||||
},
|
||||
pending_writes=UnsortedSequence(
|
||||
(AnyStr(), "one", "one"),
|
||||
(AnyStr(), "value", 2),
|
||||
(AnyStr(), "__error__", 'ConnectionError("I\'m not good")'),
|
||||
(AnyStr(), "two", "two"),
|
||||
(AnyStr(), "value", 3),
|
||||
),
|
||||
)
|
||||
@@ -1369,8 +1363,8 @@ def test_pending_writes_resume(
|
||||
parent_config=None,
|
||||
pending_writes=UnsortedSequence(
|
||||
(AnyStr(), "value", 1),
|
||||
(AnyStr(), "start:one", "__start__"),
|
||||
(AnyStr(), "start:two", "__start__"),
|
||||
(AnyStr(), "branch:to:one", None),
|
||||
(AnyStr(), "branch:to:two", None),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -6876,10 +6870,7 @@ def test_tags_stream_mode_messages() -> None:
|
||||
{
|
||||
"langgraph_step": 1,
|
||||
"langgraph_node": "call_model",
|
||||
"langgraph_triggers": (
|
||||
"branch:to:call_model",
|
||||
"start:call_model",
|
||||
),
|
||||
"langgraph_triggers": ("branch:to:call_model",),
|
||||
"langgraph_path": ("__pregel_pull", "call_model"),
|
||||
"langgraph_checkpoint_ns": AnyStr("call_model:"),
|
||||
"checkpoint_ns": AnyStr("call_model:"),
|
||||
|
||||
@@ -2021,15 +2021,13 @@ async def test_pending_writes_resume(
|
||||
assert checkpoint is not None
|
||||
# should contain error from "two"
|
||||
expected_writes = [
|
||||
(AnyStr(), "one", "one"),
|
||||
(AnyStr(), "value", 2),
|
||||
(AnyStr(), ERROR, 'ConnectionError("I\'m not good")'),
|
||||
]
|
||||
assert len(checkpoint.pending_writes) == 3
|
||||
assert len(checkpoint.pending_writes) == 2
|
||||
assert all(w in expected_writes for w in checkpoint.pending_writes)
|
||||
# both non-error pending writes come from same task
|
||||
non_error_writes = [w for w in checkpoint.pending_writes if w[1] != ERROR]
|
||||
assert non_error_writes[0][0] == non_error_writes[1][0]
|
||||
# error write is from the other task
|
||||
error_write = next(w for w in checkpoint.pending_writes if w[1] == ERROR)
|
||||
assert error_write[0] != non_error_writes[0][0]
|
||||
@@ -2076,10 +2074,10 @@ async def test_pending_writes_resume(
|
||||
"pending_sends": [],
|
||||
"versions_seen": {
|
||||
"one": {
|
||||
"start:one": AnyVersion(),
|
||||
"branch:to:one": AnyVersion(),
|
||||
},
|
||||
"two": {
|
||||
"start:two": AnyVersion(),
|
||||
"branch:to:two": AnyVersion(),
|
||||
},
|
||||
"__input__": {},
|
||||
"__start__": {
|
||||
@@ -2088,19 +2086,17 @@ async def test_pending_writes_resume(
|
||||
"__interrupt__": {
|
||||
"value": AnyVersion(),
|
||||
"__start__": AnyVersion(),
|
||||
"start:one": AnyVersion(),
|
||||
"start:two": AnyVersion(),
|
||||
"branch:to:one": AnyVersion(),
|
||||
"branch:to:two": AnyVersion(),
|
||||
},
|
||||
},
|
||||
"channel_versions": {
|
||||
"one": AnyVersion(),
|
||||
"two": AnyVersion(),
|
||||
"value": AnyVersion(),
|
||||
"__start__": AnyVersion(),
|
||||
"start:one": AnyVersion(),
|
||||
"start:two": AnyVersion(),
|
||||
"branch:to:one": AnyVersion(),
|
||||
"branch:to:two": AnyVersion(),
|
||||
},
|
||||
"channel_values": {"one": "one", "two": "two", "value": 6},
|
||||
"channel_values": {"value": 6},
|
||||
},
|
||||
metadata={
|
||||
"parents": {},
|
||||
@@ -2145,13 +2141,13 @@ async def test_pending_writes_resume(
|
||||
"channel_versions": {
|
||||
"value": AnyVersion(),
|
||||
"__start__": AnyVersion(),
|
||||
"start:one": AnyVersion(),
|
||||
"start:two": AnyVersion(),
|
||||
"branch:to:one": AnyVersion(),
|
||||
"branch:to:two": AnyVersion(),
|
||||
},
|
||||
"channel_values": {
|
||||
"value": 1,
|
||||
"start:one": "__start__",
|
||||
"start:two": "__start__",
|
||||
"branch:to:one": None,
|
||||
"branch:to:two": None,
|
||||
},
|
||||
},
|
||||
metadata={
|
||||
@@ -2171,10 +2167,8 @@ async def test_pending_writes_resume(
|
||||
}
|
||||
},
|
||||
pending_writes=UnsortedSequence(
|
||||
(AnyStr(), "one", "one"),
|
||||
(AnyStr(), "value", 2),
|
||||
(AnyStr(), "__error__", 'ConnectionError("I\'m not good")'),
|
||||
(AnyStr(), "two", "two"),
|
||||
(AnyStr(), "value", 3),
|
||||
),
|
||||
)
|
||||
@@ -2207,8 +2201,8 @@ async def test_pending_writes_resume(
|
||||
parent_config=None,
|
||||
pending_writes=UnsortedSequence(
|
||||
(AnyStr(), "value", 1),
|
||||
(AnyStr(), "start:one", "__start__"),
|
||||
(AnyStr(), "start:two", "__start__"),
|
||||
(AnyStr(), "branch:to:one", None),
|
||||
(AnyStr(), "branch:to:two", None),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -7593,10 +7587,7 @@ async def test_tags_stream_mode_messages() -> None:
|
||||
{
|
||||
"langgraph_step": 1,
|
||||
"langgraph_node": "call_model",
|
||||
"langgraph_triggers": (
|
||||
"branch:to:call_model",
|
||||
"start:call_model",
|
||||
),
|
||||
"langgraph_triggers": ("branch:to:call_model",),
|
||||
"langgraph_path": ("__pregel_pull", "call_model"),
|
||||
"langgraph_checkpoint_ns": AnyStr("call_model:"),
|
||||
"checkpoint_ns": AnyStr("call_model:"),
|
||||
|
||||
@@ -12,6 +12,7 @@ from langgraph_sdk.schema import StreamPart
|
||||
from langgraph.errors import GraphInterrupt
|
||||
from langgraph.pregel.remote import RemoteGraph
|
||||
from langgraph.pregel.types import StateSnapshot
|
||||
from langgraph.types import Interrupt
|
||||
|
||||
|
||||
def test_with_config():
|
||||
@@ -415,7 +416,19 @@ def test_stream():
|
||||
StreamPart(event="values", data={"chunk": "data2"}),
|
||||
StreamPart(event="values", data={"chunk": "data3"}),
|
||||
StreamPart(event="updates", data={"chunk": "data4"}),
|
||||
StreamPart(event="updates", data={"__interrupt__": ()}),
|
||||
StreamPart(
|
||||
event="updates",
|
||||
data={
|
||||
"__interrupt__": [
|
||||
{
|
||||
"value": {"question": "Does this look good?"},
|
||||
"resumable": True,
|
||||
"ns": ["some_ns"],
|
||||
"when": "during",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
# call method / assertions
|
||||
@@ -426,7 +439,7 @@ def test_stream():
|
||||
|
||||
# stream modes doesn't include 'updates'
|
||||
stream_parts = []
|
||||
with pytest.raises(GraphInterrupt):
|
||||
with pytest.raises(GraphInterrupt) as exc:
|
||||
for stream_part in remote_pregel.stream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
@@ -434,6 +447,15 @@ def test_stream():
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
|
||||
assert exc.value.args[0] == [
|
||||
Interrupt(
|
||||
value={"question": "Does this look good?"},
|
||||
resumable=True,
|
||||
ns=["some_ns"],
|
||||
when="during",
|
||||
)
|
||||
]
|
||||
|
||||
assert stream_parts == [
|
||||
{"chunk": "data1"},
|
||||
{"chunk": "data2"},
|
||||
@@ -517,7 +539,19 @@ async def test_astream():
|
||||
StreamPart(event="values", data={"chunk": "data2"}),
|
||||
StreamPart(event="values", data={"chunk": "data3"}),
|
||||
StreamPart(event="updates", data={"chunk": "data4"}),
|
||||
StreamPart(event="updates", data={"__interrupt__": ()}),
|
||||
StreamPart(
|
||||
event="updates",
|
||||
data={
|
||||
"__interrupt__": [
|
||||
{
|
||||
"value": {"question": "Does this look good?"},
|
||||
"resumable": True,
|
||||
"ns": ["some_ns"],
|
||||
"when": "during",
|
||||
}
|
||||
]
|
||||
},
|
||||
),
|
||||
]
|
||||
mock_async_client.runs.stream.return_value = async_iter
|
||||
|
||||
@@ -529,7 +563,7 @@ async def test_astream():
|
||||
|
||||
# stream modes doesn't include 'updates'
|
||||
stream_parts = []
|
||||
with pytest.raises(GraphInterrupt):
|
||||
with pytest.raises(GraphInterrupt) as exc:
|
||||
async for stream_part in remote_pregel.astream(
|
||||
{"input": "data"},
|
||||
config={"configurable": {"thread_id": "thread_1"}},
|
||||
@@ -537,6 +571,15 @@ async def test_astream():
|
||||
):
|
||||
stream_parts.append(stream_part)
|
||||
|
||||
assert exc.value.args[0] == [
|
||||
Interrupt(
|
||||
value={"question": "Does this look good?"},
|
||||
resumable=True,
|
||||
ns=["some_ns"],
|
||||
when="during",
|
||||
)
|
||||
]
|
||||
|
||||
assert stream_parts == [
|
||||
{"chunk": "data1"},
|
||||
{"chunk": "data2"},
|
||||
|
||||
@@ -292,7 +292,7 @@ class ToolNode(RunnableCallable):
|
||||
goto=cast(list[Send], parent_command.goto) + output.goto,
|
||||
)
|
||||
else:
|
||||
parent_command = output
|
||||
parent_command = Command(graph=Command.PARENT, goto=output.goto)
|
||||
else:
|
||||
combined_outputs.append(output)
|
||||
else:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "0.1.6"
|
||||
version = "0.1.7"
|
||||
description = "Library with high-level APIs for creating and executing LangGraph agents and tools."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -97,7 +97,6 @@ def test_no_prompt(
|
||||
_AnyIdHumanMessage(content="hi?"),
|
||||
AIMessage(content="hi?", id="0"),
|
||||
],
|
||||
"agent": "agent",
|
||||
}
|
||||
assert saved.metadata == {
|
||||
"parents": {},
|
||||
@@ -129,7 +128,6 @@ async def test_no_prompt_async(checkpointer_name: str) -> None:
|
||||
_AnyIdHumanMessage(content="hi?"),
|
||||
AIMessage(content="hi?", id="0"),
|
||||
],
|
||||
"agent": "agent",
|
||||
}
|
||||
assert saved.metadata == {
|
||||
"parents": {},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@langchain/langgraph-sdk",
|
||||
"version": "0.0.60",
|
||||
"version": "0.0.62",
|
||||
"description": "Client library for interacting with the LangGraph API",
|
||||
"type": "module",
|
||||
"packageManager": "yarn@1.22.19",
|
||||
|
||||
@@ -109,6 +109,7 @@ class ComponentStore {
|
||||
const COMPONENT_STORE = new ComponentStore();
|
||||
const EXT_STORE_SYMBOL = Symbol.for("LGUI_EXT_STORE");
|
||||
const REQUIRE_SYMBOL = Symbol.for("LGUI_REQUIRE");
|
||||
const REQUIRE_EXTRA_SYMBOL = Symbol.for("LGUI_REQUIRE_EXTRA");
|
||||
|
||||
interface LoadExternalComponentProps
|
||||
extends Pick<React.HTMLAttributes<HTMLDivElement>, "style" | "className"> {
|
||||
@@ -197,9 +198,17 @@ declare global {
|
||||
interface Window {
|
||||
[EXT_STORE_SYMBOL]: ComponentStore;
|
||||
[REQUIRE_SYMBOL]: (name: string) => unknown;
|
||||
[REQUIRE_EXTRA_SYMBOL]: Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
|
||||
export function experimental_loadShare(name: string, module: unknown) {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
window[REQUIRE_EXTRA_SYMBOL] ??= {};
|
||||
window[REQUIRE_EXTRA_SYMBOL][name] = module;
|
||||
}
|
||||
|
||||
export function bootstrapUiContext() {
|
||||
if (typeof window === "undefined") {
|
||||
console.warn(
|
||||
@@ -224,6 +233,14 @@ export function bootstrapUiContext() {
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
window[REQUIRE_EXTRA_SYMBOL] != null &&
|
||||
typeof window[REQUIRE_EXTRA_SYMBOL] === "object" &&
|
||||
name in window[REQUIRE_EXTRA_SYMBOL]
|
||||
) {
|
||||
return window[REQUIRE_EXTRA_SYMBOL][name];
|
||||
}
|
||||
|
||||
throw new Error(`Unknown module...: ${name}`);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { bootstrapUiContext } from "./client.js";
|
||||
bootstrapUiContext();
|
||||
|
||||
export { useStreamContext, LoadExternalComponent } from "./client.js";
|
||||
export {
|
||||
useStreamContext,
|
||||
LoadExternalComponent,
|
||||
experimental_loadShare,
|
||||
} from "./client.js";
|
||||
export {
|
||||
uiMessageReducer,
|
||||
type UIMessage,
|
||||
|
||||
@@ -782,42 +782,6 @@ export function useStream<
|
||||
submittingRef.current = true;
|
||||
abortRef.current = new AbortController();
|
||||
|
||||
let usableThreadId = threadId;
|
||||
if (!usableThreadId) {
|
||||
const thread = await client.threads.create();
|
||||
onThreadId(thread.thread_id);
|
||||
usableThreadId = thread.thread_id;
|
||||
}
|
||||
|
||||
const streamMode = unique([
|
||||
...(submitOptions?.streamMode ?? []),
|
||||
...trackStreamModeRef.current,
|
||||
...callbackStreamMode,
|
||||
]);
|
||||
|
||||
const checkpoint =
|
||||
submitOptions?.checkpoint ?? threadHead?.checkpoint ?? undefined;
|
||||
// @ts-expect-error
|
||||
if (checkpoint != null) delete checkpoint.thread_id;
|
||||
|
||||
const run = (await client.runs.stream(usableThreadId, assistantId, {
|
||||
input: values as Record<string, unknown>,
|
||||
config: submitOptions?.config,
|
||||
command: submitOptions?.command,
|
||||
|
||||
interruptBefore: submitOptions?.interruptBefore,
|
||||
interruptAfter: submitOptions?.interruptAfter,
|
||||
metadata: submitOptions?.metadata,
|
||||
multitaskStrategy: submitOptions?.multitaskStrategy,
|
||||
onCompletion: submitOptions?.onCompletion,
|
||||
onDisconnect: submitOptions?.onDisconnect ?? "cancel",
|
||||
|
||||
signal: abortRef.current.signal,
|
||||
|
||||
checkpoint,
|
||||
streamMode,
|
||||
})) as AsyncGenerator<EventStreamEvent>;
|
||||
|
||||
// Unbranch things
|
||||
const newPath = submitOptions?.checkpoint?.checkpoint_id
|
||||
? branchByCheckpoint[submitOptions?.checkpoint?.checkpoint_id]?.branch
|
||||
@@ -842,6 +806,42 @@ export function useStream<
|
||||
return values;
|
||||
});
|
||||
|
||||
let usableThreadId = threadId;
|
||||
if (!usableThreadId) {
|
||||
const thread = await client.threads.create();
|
||||
onThreadId(thread.thread_id);
|
||||
usableThreadId = thread.thread_id;
|
||||
}
|
||||
|
||||
const streamMode = unique([
|
||||
...(submitOptions?.streamMode ?? []),
|
||||
...trackStreamModeRef.current,
|
||||
...callbackStreamMode,
|
||||
]);
|
||||
|
||||
const checkpoint =
|
||||
submitOptions?.checkpoint ?? threadHead?.checkpoint ?? undefined;
|
||||
// @ts-expect-error
|
||||
if (checkpoint != null) delete checkpoint.thread_id;
|
||||
|
||||
const run = client.runs.stream(usableThreadId, assistantId, {
|
||||
input: values as Record<string, unknown>,
|
||||
config: submitOptions?.config,
|
||||
command: submitOptions?.command,
|
||||
|
||||
interruptBefore: submitOptions?.interruptBefore,
|
||||
interruptAfter: submitOptions?.interruptAfter,
|
||||
metadata: submitOptions?.metadata,
|
||||
multitaskStrategy: submitOptions?.multitaskStrategy,
|
||||
onCompletion: submitOptions?.onCompletion,
|
||||
onDisconnect: submitOptions?.onDisconnect ?? "cancel",
|
||||
|
||||
signal: abortRef.current.signal,
|
||||
|
||||
checkpoint,
|
||||
streamMode,
|
||||
}) as AsyncGenerator<EventStreamEvent>;
|
||||
|
||||
let streamError: StreamError | undefined;
|
||||
for await (const { event, data } of run) {
|
||||
if (event === "error") {
|
||||
|
||||
@@ -57,25 +57,25 @@ export interface GraphSchema {
|
||||
* The schema for the input state.
|
||||
* Missing if unable to generate JSON schema from graph.
|
||||
*/
|
||||
input_schema?: JSONSchema7;
|
||||
input_schema?: JSONSchema7 | null | undefined;
|
||||
|
||||
/**
|
||||
* The schema for the output state.
|
||||
* Missing if unable to generate JSON schema from graph.
|
||||
*/
|
||||
output_schema?: JSONSchema7;
|
||||
output_schema?: JSONSchema7 | null | undefined;
|
||||
|
||||
/**
|
||||
* The schema for the graph state.
|
||||
* Missing if unable to generate JSON schema from graph.
|
||||
*/
|
||||
state_schema?: JSONSchema7;
|
||||
state_schema?: JSONSchema7 | null | undefined;
|
||||
|
||||
/**
|
||||
* The schema for the graph config.
|
||||
* Missing if unable to generate JSON schema from graph.
|
||||
*/
|
||||
config_schema?: JSONSchema7;
|
||||
config_schema?: JSONSchema7 | null | undefined;
|
||||
}
|
||||
|
||||
export type Subgraphs = Record<string, GraphSchema>;
|
||||
|
||||
@@ -192,6 +192,18 @@ class BaseUser(typing.Protocol):
|
||||
"""The permissions associated with the user."""
|
||||
...
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Get a key from your minimal user dict."""
|
||||
...
|
||||
|
||||
def __contains__(self, key):
|
||||
"""Check if a property exists."""
|
||||
...
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over the keys of the user."""
|
||||
...
|
||||
|
||||
|
||||
class StudioUser:
|
||||
"""A user object that's populated from authenticated requests from the LangGraph studio.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.1.59"
|
||||
version = "0.1.60"
|
||||
description = "SDK for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
Reference in New Issue
Block a user