mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-13 13:17:52 +02:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c17ee1bf5a | ||
|
|
88c603b00b | ||
|
|
c12f7cb2b9 | ||
|
|
6d7d689578 | ||
|
|
f1b7eca7fc | ||
|
|
93766a6df1 | ||
|
|
a9d4e0da29 | ||
|
|
9105e60a34 | ||
|
|
b735452153 | ||
|
|
5920d8aa92 | ||
|
|
533f5b3d6f | ||
|
|
be5889a7df | ||
|
|
0bf268feca | ||
|
|
5e7566f4a3 | ||
|
|
494c8ef0d2 | ||
|
|
45e60ff9e1 |
@@ -1,4 +1,4 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: true
|
||||||
version: 2.1
|
version: 2.1
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: 🤔 Question or Problem
|
- name: 🤔 Question or Problem
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Define the directories containing projects
|
||||||
|
LIBS_DIRS := $(wildcard libs/*)
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
.PHONY: all
|
||||||
|
all: lint format lock test
|
||||||
|
|
||||||
|
# Install dependencies for all projects
|
||||||
|
.PHONY: install
|
||||||
|
install:
|
||||||
|
@echo "Creating virtual environment..."
|
||||||
|
@uv venv
|
||||||
|
@for dir in $(LIBS_DIRS); do \
|
||||||
|
if [ -f $$dir/pyproject.toml ]; then \
|
||||||
|
echo "Installing dependencies for $$dir"; \
|
||||||
|
uv pip install -e $$dir; \
|
||||||
|
fi; \
|
||||||
|
done
|
||||||
|
|
||||||
|
# Lint all projects
|
||||||
|
.PHONY: lint
|
||||||
|
lint:
|
||||||
|
@for dir in $(LIBS_DIRS); do \
|
||||||
|
if [ -f $$dir/Makefile ]; then \
|
||||||
|
echo "Running lint in $$dir"; \
|
||||||
|
$(MAKE) -C $$dir lint; \
|
||||||
|
fi; \
|
||||||
|
done
|
||||||
|
|
||||||
|
# Format all projects
|
||||||
|
.PHONY: format
|
||||||
|
format:
|
||||||
|
@for dir in $(LIBS_DIRS); do \
|
||||||
|
if [ -f $$dir/Makefile ]; then \
|
||||||
|
echo "Running format in $$dir"; \
|
||||||
|
$(MAKE) -C $$dir format; \
|
||||||
|
fi; \
|
||||||
|
done
|
||||||
|
|
||||||
|
# Lock all projects
|
||||||
|
.PHONY: lock
|
||||||
|
lock:
|
||||||
|
@for dir in $(LIBS_DIRS); do \
|
||||||
|
if [ -f $$dir/Makefile ]; then \
|
||||||
|
echo "Running lock in $$dir"; \
|
||||||
|
(cd $$dir && uv lock); \
|
||||||
|
fi; \
|
||||||
|
done
|
||||||
|
|
||||||
|
# Test all projects
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
@for dir in $(LIBS_DIRS); do \
|
||||||
|
if [ -f $$dir/Makefile ]; then \
|
||||||
|
echo "Running test in $$dir"; \
|
||||||
|
$(MAKE) -C $$dir test; \
|
||||||
|
fi; \
|
||||||
|
done
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
!!! info "Prerequisites"
|
!!! info "Prerequisites"
|
||||||
|
|
||||||
- [Assistants Overview](../../concepts/assistants.md)
|
- [Assistants Overview](../../../concepts/assistants.md)
|
||||||
|
|
||||||
LangGraph Studio lets you view, edit, and update your assistants, and allows you to run your graph using these assistant configurations.
|
LangGraph Studio lets you view, edit, and update your assistants, and allows you to run your graph using these assistant configurations.
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ The LangGraph CLI requires a JSON configuration file that follows this [schema](
|
|||||||
| <span style="white-space: nowrap;">`graphs`</span> | **Required**. Mapping from graph ID to path where the compiled graph or a function that makes a graph is defined. Example: <ul><li>`./your_package/your_file.py:variable`, where `variable` is an instance of `langgraph.graph.state.CompiledStateGraph`</li><li>`./your_package/your_file.py:make_graph`, where `make_graph` is a function that takes a config dictionary (`langchain_core.runnables.RunnableConfig`) and returns an instance of `langgraph.graph.state.StateGraph` or `langgraph.graph.state.CompiledStateGraph`. See [how to rebuild a graph at runtime](../../cloud/deployment/graph_rebuild.md) for more details.</li></ul> |
|
| <span style="white-space: nowrap;">`graphs`</span> | **Required**. Mapping from graph ID to path where the compiled graph or a function that makes a graph is defined. Example: <ul><li>`./your_package/your_file.py:variable`, where `variable` is an instance of `langgraph.graph.state.CompiledStateGraph`</li><li>`./your_package/your_file.py:make_graph`, where `make_graph` is a function that takes a config dictionary (`langchain_core.runnables.RunnableConfig`) and returns an instance of `langgraph.graph.state.StateGraph` or `langgraph.graph.state.CompiledStateGraph`. See [how to rebuild a graph at runtime](../../cloud/deployment/graph_rebuild.md) for more details.</li></ul> |
|
||||||
| <span style="white-space: nowrap;">`auth`</span> | _(Added in v0.0.11)_ Auth configuration containing the path to your authentication handler. Example: `./your_package/auth.py:auth`, where `auth` is an instance of `langgraph_sdk.Auth`. See [authentication guide](../../concepts/auth.md) for details. |
|
| <span style="white-space: nowrap;">`auth`</span> | _(Added in v0.0.11)_ Auth configuration containing the path to your authentication handler. Example: `./your_package/auth.py:auth`, where `auth` is an instance of `langgraph_sdk.Auth`. See [authentication guide](../../concepts/auth.md) for details. |
|
||||||
| <span style="white-space: nowrap;">`base_image`</span> | Optional. Base image to use for the LangGraph API server. Defaults to `langchain/langgraph-api` or `langchain/langgraphjs-api`. Use this to pin your builds to a particular version of the langgraph API, such as `"langchain/langgraph-server:0.2"`. See https://hub.docker.com/r/langchain/langgraph-server/tags for more details. (added in `langgraph-cli==0.2.8`) |
|
| <span style="white-space: nowrap;">`base_image`</span> | Optional. Base image to use for the LangGraph API server. Defaults to `langchain/langgraph-api` or `langchain/langgraphjs-api`. Use this to pin your builds to a particular version of the langgraph API, such as `"langchain/langgraph-server:0.2"`. See https://hub.docker.com/r/langchain/langgraph-server/tags for more details. (added in `langgraph-cli==0.2.8`) |
|
||||||
|
| <span style="white-space: nowrap;">`image_distro`</span> | Optional. Linux distribution for the base image. Must be either `"debian"` or `"wolfi"`. If omitted, defaults to `"debian"`. Available in `langgraph-cli>=0.2.11`.|
|
||||||
| <span style="white-space: nowrap;">`env`</span> | Path to `.env` file or a mapping from environment variable to its value. |
|
| <span style="white-space: nowrap;">`env`</span> | Path to `.env` file or a mapping from environment variable to its value. |
|
||||||
| <span style="white-space: nowrap;">`store`</span> | Configuration for adding semantic search and/or time-to-live (TTL) to the BaseStore. Contains the following fields: <ul><li>`index` (optional): Configuration for semantic search indexing with fields `embed`, `dims`, and optional `fields`.</li><li>`ttl` (optional): Configuration for item expiration. An object with optional fields: `refresh_on_read` (boolean, defaults to `true`), `default_ttl` (float, lifespan in **minutes**, defaults to no expiration), and `sweep_interval_minutes` (integer, how often to check for expired items, defaults to no sweeping).</li></ul> |
|
| <span style="white-space: nowrap;">`store`</span> | Configuration for adding semantic search and/or time-to-live (TTL) to the BaseStore. Contains the following fields: <ul><li>`index` (optional): Configuration for semantic search indexing with fields `embed`, `dims`, and optional `fields`.</li><li>`ttl` (optional): Configuration for item expiration. An object with optional fields: `refresh_on_read` (boolean, defaults to `true`), `default_ttl` (float, lifespan in **minutes**, defaults to no expiration), and `sweep_interval_minutes` (integer, how often to check for expired items, defaults to no sweeping).</li></ul> |
|
||||||
| <span style="white-space: nowrap;">`ui`</span> | Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file. (added in `langgraph-cli==0.1.84`) |
|
| <span style="white-space: nowrap;">`ui`</span> | Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file. (added in `langgraph-cli==0.1.84`) |
|
||||||
@@ -79,6 +80,20 @@ The LangGraph CLI requires a JSON configuration file that follows this [schema](
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Using Wolfi Base Images
|
||||||
|
|
||||||
|
You can specify the Linux distribution for your base image using the `image_distro` field. Valid options are `debian` or `wolfi`. Wolfi is the recommended option as it provides smaller and more secure images. This is available in `langgraph-cli>=0.2.11`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dependencies": ["."],
|
||||||
|
"graphs": {
|
||||||
|
"chat": "./chat/graph.py:graph"
|
||||||
|
},
|
||||||
|
"image_distro": "wolfi"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
#### Adding semantic search to the store
|
#### Adding semantic search to the store
|
||||||
|
|
||||||
All deployments come with a DB-backed BaseStore. Adding an "index" configuration to your `langgraph.json` will enable [semantic search](../deployment/semantic_search.md) within the BaseStore of your deployment.
|
All deployments come with a DB-backed BaseStore. Adding an "index" configuration to your `langgraph.json` will enable [semantic search](../deployment/semantic_search.md) within the BaseStore of your deployment.
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ async def add_owner(
|
|||||||
You can register handlers for specific resources and actions by chaining the resource and action names together with the [`@auth.on`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.on) decorator.
|
You can register handlers for specific resources and actions by chaining the resource and action names together with the [`@auth.on`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.on) decorator.
|
||||||
When a request is made, the most specific handler that matches that resource and action is called. Below is an example of how to register handlers for specific resources and actions. For the following setup:
|
When a request is made, the most specific handler that matches that resource and action is called. Below is an example of how to register handlers for specific resources and actions. For the following setup:
|
||||||
|
|
||||||
1. Authenticated users are able to create threads, read thread, create runs on threads
|
1. Authenticated users are able to create threads, read threads, and create runs on threads
|
||||||
2. Only users with the "assistants:create" permission are allowed to create new assistants
|
2. Only users with the "assistants:create" permission are allowed to create new assistants
|
||||||
3. All other endpoints (e.g., e.g., delete assistant, crons, store) are disabled for all users.
|
3. All other endpoints (e.g., e.g., delete assistant, crons, store) are disabled for all users.
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ def node_3(state: PrivateState) -> OutputState:
|
|||||||
# Read from PrivateState, write to OutputState
|
# Read from PrivateState, write to OutputState
|
||||||
return {"graph_output": state["bar"] + " Lance"}
|
return {"graph_output": state["bar"] + " Lance"}
|
||||||
|
|
||||||
builder = StateGraph(OverallState,input=InputState,output=OutputState)
|
builder = StateGraph(OverallState,input_schema=InputState,output_schema=OutputState)
|
||||||
builder.add_node("node_1", node_1)
|
builder.add_node("node_1", node_1)
|
||||||
builder.add_node("node_2", node_2)
|
builder.add_node("node_2", node_2)
|
||||||
builder.add_node("node_3", node_3)
|
builder.add_node("node_3", node_3)
|
||||||
@@ -107,7 +107,7 @@ There are two subtle and important points to note here:
|
|||||||
|
|
||||||
1. We pass `state: InputState` as the input schema to `node_1`. But, we write out to `foo`, a channel in `OverallState`. How can we write out to a state channel that is not included in the input schema? This is because a node _can write to any state channel in the graph state._ The graph state is the union of the state channels defined at initialization, which includes `OverallState` and the filters `InputState` and `OutputState`.
|
1. We pass `state: InputState` as the input schema to `node_1`. But, we write out to `foo`, a channel in `OverallState`. How can we write out to a state channel that is not included in the input schema? This is because a node _can write to any state channel in the graph state._ The graph state is the union of the state channels defined at initialization, which includes `OverallState` and the filters `InputState` and `OutputState`.
|
||||||
|
|
||||||
2. We initialize the graph with `StateGraph(OverallState,input=InputState,output=OutputState)`. So, how can we write to `PrivateState` in `node_2`? How does the graph gain access to this schema if it was not passed in the `StateGraph` initialization? We can do this because _nodes can also declare additional state channels_ as long as the state schema definition exists. In this case, the `PrivateState` schema is defined, so we can add `bar` as a new state channel in the graph and write to it.
|
2. We initialize the graph with `StateGraph(OverallState,input_schema=InputState,output_schema=OutputState)`. So, how can we write to `PrivateState` in `node_2`? How does the graph gain access to this schema if it was not passed in the `StateGraph` initialization? We can do this because _nodes can also declare additional state channels_ as long as the state schema definition exists. In this case, the `PrivateState` schema is defined, so we can add `bar` as a new state channel in the graph and write to it.
|
||||||
|
|
||||||
### Reducers
|
### Reducers
|
||||||
|
|
||||||
@@ -197,19 +197,25 @@ In LangGraph, nodes are typically python functions (sync or async) where the **f
|
|||||||
Similar to `NetworkX`, you add these nodes to a graph using the [add_node][langgraph.graph.StateGraph.add_node] method:
|
Similar to `NetworkX`, you add these nodes to a graph using the [add_node][langgraph.graph.StateGraph.add_node] method:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
from langgraph.graph import StateGraph
|
from langgraph.graph import StateGraph
|
||||||
|
|
||||||
builder = StateGraph(dict)
|
class State(TypedDict):
|
||||||
|
input: str
|
||||||
|
results: str
|
||||||
|
|
||||||
|
builder = StateGraph(State)
|
||||||
|
|
||||||
|
|
||||||
def my_node(state: dict, config: RunnableConfig):
|
def my_node(state: State, config: RunnableConfig):
|
||||||
print("In node: ", config["configurable"]["user_id"])
|
print("In node: ", config["configurable"]["user_id"])
|
||||||
return {"results": f"Hello, {state['input']}!"}
|
return {"results": f"Hello, {state['input']}!"}
|
||||||
|
|
||||||
|
|
||||||
# The second argument is optional
|
# The second argument is optional
|
||||||
def my_other_node(state: dict):
|
def my_other_node(state: State):
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ def answer_node(state: InputState):
|
|||||||
return {"answer": "bye", "question": state["question"]}
|
return {"answer": "bye", "question": state["question"]}
|
||||||
|
|
||||||
# Build the graph with explicit schemas
|
# Build the graph with explicit schemas
|
||||||
builder = StateGraph(OverallState, input=InputState, output=OutputState)
|
builder = StateGraph(OverallState, input_schema=InputState, output_schema=OutputState)
|
||||||
builder.add_node(answer_node)
|
builder.add_node(answer_node)
|
||||||
builder.add_edge(START, "answer_node")
|
builder.add_edge(START, "answer_node")
|
||||||
builder.add_edge("answer_node", END)
|
builder.add_edge("answer_node", END)
|
||||||
|
|||||||
@@ -439,7 +439,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 6,
|
"execution_count": null,
|
||||||
"id": "6ec0eb77-874e-443e-8c73-93125b515106",
|
"id": "6ec0eb77-874e-443e-8c73-93125b515106",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -478,7 +478,7 @@
|
|||||||
"\n",
|
"\n",
|
||||||
"\n",
|
"\n",
|
||||||
"# Build the graph with input and output schemas specified\n",
|
"# Build the graph with input and output schemas specified\n",
|
||||||
"builder = StateGraph(OverallState, input=InputState, output=OutputState)\n",
|
"builder = StateGraph(OverallState, input_schema=InputState, output_schema=OutputState)\n",
|
||||||
"builder.add_node(answer_node) # Add the answer node\n",
|
"builder.add_node(answer_node) # Add the answer node\n",
|
||||||
"builder.add_edge(START, \"answer_node\") # Define the starting edge\n",
|
"builder.add_edge(START, \"answer_node\") # Define the starting edge\n",
|
||||||
"builder.add_edge(\"answer_node\", END) # Define the ending edge\n",
|
"builder.add_edge(\"answer_node\", END) # Define the ending edge\n",
|
||||||
@@ -3430,7 +3430,7 @@
|
|||||||
"name": "python",
|
"name": "python",
|
||||||
"nbconvert_exporter": "python",
|
"nbconvert_exporter": "python",
|
||||||
"pygments_lexer": "ipython3",
|
"pygments_lexer": "ipython3",
|
||||||
"version": "3.12.9"
|
"version": "3.9.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nbformat": 4,
|
"nbformat": 4,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Welcome to the LangGraph reference docs! These pages detail the core interfaces
|
|||||||
|
|
||||||
## LangGraph
|
## LangGraph
|
||||||
|
|
||||||
The core APIs for the LangGraph opens source library.
|
The core APIs for the LangGraph open source library.
|
||||||
|
|
||||||
- [Graphs](graphs.md): Main graph abstraction and usage.
|
- [Graphs](graphs.md): Main graph abstraction and usage.
|
||||||
- [Functional API](func.md): Functional programming interface for graphs.
|
- [Functional API](func.md): Functional programming interface for graphs.
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ from typing import Annotated
|
|||||||
|
|
||||||
from typing_extensions import TypedDict
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
from langgraph.graph import StateGraph, START
|
from langgraph.graph import StateGraph, START, END
|
||||||
from langgraph.graph.message import add_messages
|
from langgraph.graph.message import add_messages
|
||||||
|
|
||||||
|
|
||||||
@@ -100,7 +100,16 @@ Add an `entry` point to tell the graph **where to start its work** each time it
|
|||||||
graph_builder.add_edge(START, "chatbot")
|
graph_builder.add_edge(START, "chatbot")
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5. Compile the graph
|
## 5. Add an `exit` point
|
||||||
|
|
||||||
|
Add an `exit` point to indicate **where the graph should finish execution**. This is helpful for more complex flows, but even in a simple graph like this, adding an end node improves clarity.
|
||||||
|
|
||||||
|
```python
|
||||||
|
graph_builder.add_edge("chatbot", END)
|
||||||
|
```
|
||||||
|
This tells the graph to terminate after running the chatbot node.
|
||||||
|
|
||||||
|
## 6. Compile the graph
|
||||||
|
|
||||||
Before running the graph, we'll need to compile it. We can do so by calling `compile()`
|
Before running the graph, we'll need to compile it. We can do so by calling `compile()`
|
||||||
on the graph builder. This creates a `CompiledGraph` we can invoke on our state.
|
on the graph builder. This creates a `CompiledGraph` we can invoke on our state.
|
||||||
@@ -109,7 +118,7 @@ on the graph builder. This creates a `CompiledGraph` we can invoke on our state.
|
|||||||
graph = graph_builder.compile()
|
graph = graph_builder.compile()
|
||||||
```
|
```
|
||||||
|
|
||||||
## 6. Visualize the graph (optional)
|
## 7. Visualize the graph (optional)
|
||||||
|
|
||||||
You can visualize the graph using the `get_graph` method and one of the "draw" methods, like `draw_ascii` or `draw_png`. The `draw` methods each require additional dependencies.
|
You can visualize the graph using the `get_graph` method and one of the "draw" methods, like `draw_ascii` or `draw_png`. The `draw` methods each require additional dependencies.
|
||||||
|
|
||||||
@@ -126,7 +135,7 @@ except Exception:
|
|||||||

|

|
||||||
|
|
||||||
|
|
||||||
## 7. Run the chatbot
|
## 8. Run the chatbot
|
||||||
|
|
||||||
Now run the chatbot!
|
Now run the chatbot!
|
||||||
|
|
||||||
@@ -171,7 +180,7 @@ from typing import Annotated
|
|||||||
from langchain.chat_models import init_chat_model
|
from langchain.chat_models import init_chat_model
|
||||||
from typing_extensions import TypedDict
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
from langgraph.graph import StateGraph, START
|
from langgraph.graph import StateGraph, START, END
|
||||||
from langgraph.graph.message import add_messages
|
from langgraph.graph.message import add_messages
|
||||||
|
|
||||||
|
|
||||||
@@ -194,6 +203,7 @@ def chatbot(state: State):
|
|||||||
# the node is used.
|
# the node is used.
|
||||||
graph_builder.add_node("chatbot", chatbot)
|
graph_builder.add_node("chatbot", chatbot)
|
||||||
graph_builder.add_edge(START, "chatbot")
|
graph_builder.add_edge(START, "chatbot")
|
||||||
|
graph_builder.add_edge("chatbot", END)
|
||||||
graph = graph_builder.compile()
|
graph = graph_builder.compile()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ llm = init_chat_model("anthropic:claude-3-5-sonnet-latest")
|
|||||||
```
|
```
|
||||||
-->
|
-->
|
||||||
|
|
||||||
```python
|
```python hl_lines="36 37"
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from langchain.chat_models import init_chat_model
|
from langchain.chat_models import init_chat_model
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1758,7 +1758,7 @@
|
|||||||
"id": "4eb67198-c84f-458b-8baf-783d7246dddc",
|
"id": "4eb67198-c84f-458b-8baf-783d7246dddc",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"source": [
|
"source": [
|
||||||
"Let's let the agent try again. Call `stream` with `None` to just use the inputs loaded from the memory. We will skip our human review for the next few attempats\n",
|
"Let's let the agent try again. Call `stream` with `None` to just use the inputs loaded from the memory. We will skip our human review for the next few attempts\n",
|
||||||
"to see if it can correct itself."
|
"to see if it can correct itself."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Iterator, Sequence
|
from collections.abc import Iterator, Sequence
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
||||||
@@ -34,8 +36,8 @@ class PostgresSaver(BasePostgresSaver):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
conn: _internal.Conn,
|
conn: _internal.Conn,
|
||||||
pipe: Optional[Pipeline] = None,
|
pipe: Pipeline | None = None,
|
||||||
serde: Optional[SerializerProtocol] = None,
|
serde: SerializerProtocol | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(serde=serde)
|
super().__init__(serde=serde)
|
||||||
if isinstance(conn, ConnectionPool) and pipe is not None:
|
if isinstance(conn, ConnectionPool) and pipe is not None:
|
||||||
@@ -52,7 +54,7 @@ class PostgresSaver(BasePostgresSaver):
|
|||||||
@contextmanager
|
@contextmanager
|
||||||
def from_conn_string(
|
def from_conn_string(
|
||||||
cls, conn_string: str, *, pipeline: bool = False
|
cls, conn_string: str, *, pipeline: bool = False
|
||||||
) -> Iterator["PostgresSaver"]:
|
) -> Iterator[PostgresSaver]:
|
||||||
"""Create a new PostgresSaver instance from a connection string.
|
"""Create a new PostgresSaver instance from a connection string.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -99,11 +101,11 @@ class PostgresSaver(BasePostgresSaver):
|
|||||||
|
|
||||||
def list(
|
def list(
|
||||||
self,
|
self,
|
||||||
config: Optional[RunnableConfig],
|
config: RunnableConfig | None,
|
||||||
*,
|
*,
|
||||||
filter: Optional[dict[str, Any]] = None,
|
filter: dict[str, Any] | None = None,
|
||||||
before: Optional[RunnableConfig] = None,
|
before: RunnableConfig | None = None,
|
||||||
limit: Optional[int] = None,
|
limit: int | None = None,
|
||||||
) -> Iterator[CheckpointTuple]:
|
) -> Iterator[CheckpointTuple]:
|
||||||
"""List checkpoints from the database.
|
"""List checkpoints from the database.
|
||||||
|
|
||||||
@@ -200,7 +202,7 @@ class PostgresSaver(BasePostgresSaver):
|
|||||||
self._load_writes(value["pending_writes"]),
|
self._load_writes(value["pending_writes"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
||||||
"""Get a checkpoint tuple from the database.
|
"""Get a checkpoint tuple from the database.
|
||||||
|
|
||||||
This method retrieves a checkpoint tuple from the Postgres database based on the
|
This method retrieves a checkpoint tuple from the Postgres database based on the
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
||||||
@@ -34,8 +36,8 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
conn: _ainternal.Conn,
|
conn: _ainternal.Conn,
|
||||||
pipe: Optional[AsyncPipeline] = None,
|
pipe: AsyncPipeline | None = None,
|
||||||
serde: Optional[SerializerProtocol] = None,
|
serde: SerializerProtocol | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(serde=serde)
|
super().__init__(serde=serde)
|
||||||
if isinstance(conn, AsyncConnectionPool) and pipe is not None:
|
if isinstance(conn, AsyncConnectionPool) and pipe is not None:
|
||||||
@@ -56,8 +58,8 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|||||||
conn_string: str,
|
conn_string: str,
|
||||||
*,
|
*,
|
||||||
pipeline: bool = False,
|
pipeline: bool = False,
|
||||||
serde: Optional[SerializerProtocol] = None,
|
serde: SerializerProtocol | None = None,
|
||||||
) -> AsyncIterator["AsyncPostgresSaver"]:
|
) -> AsyncIterator[AsyncPostgresSaver]:
|
||||||
"""Create a new AsyncPostgresSaver instance from a connection string.
|
"""Create a new AsyncPostgresSaver instance from a connection string.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -104,11 +106,11 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|||||||
|
|
||||||
async def alist(
|
async def alist(
|
||||||
self,
|
self,
|
||||||
config: Optional[RunnableConfig],
|
config: RunnableConfig | None,
|
||||||
*,
|
*,
|
||||||
filter: Optional[dict[str, Any]] = None,
|
filter: dict[str, Any] | None = None,
|
||||||
before: Optional[RunnableConfig] = None,
|
before: RunnableConfig | None = None,
|
||||||
limit: Optional[int] = None,
|
limit: int | None = None,
|
||||||
) -> AsyncIterator[CheckpointTuple]:
|
) -> AsyncIterator[CheckpointTuple]:
|
||||||
"""List checkpoints from the database asynchronously.
|
"""List checkpoints from the database asynchronously.
|
||||||
|
|
||||||
@@ -187,7 +189,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|||||||
await asyncio.to_thread(self._load_writes, value["pending_writes"]),
|
await asyncio.to_thread(self._load_writes, value["pending_writes"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
||||||
"""Get a checkpoint tuple from the database asynchronously.
|
"""Get a checkpoint tuple from the database asynchronously.
|
||||||
|
|
||||||
This method retrieves a checkpoint tuple from the Postgres database based on the
|
This method retrieves a checkpoint tuple from the Postgres database based on the
|
||||||
@@ -424,11 +426,11 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|||||||
|
|
||||||
def list(
|
def list(
|
||||||
self,
|
self,
|
||||||
config: Optional[RunnableConfig],
|
config: RunnableConfig | None,
|
||||||
*,
|
*,
|
||||||
filter: Optional[dict[str, Any]] = None,
|
filter: dict[str, Any] | None = None,
|
||||||
before: Optional[RunnableConfig] = None,
|
before: RunnableConfig | None = None,
|
||||||
limit: Optional[int] = None,
|
limit: int | None = None,
|
||||||
) -> Iterator[CheckpointTuple]:
|
) -> Iterator[CheckpointTuple]:
|
||||||
"""List checkpoints from the database.
|
"""List checkpoints from the database.
|
||||||
|
|
||||||
@@ -466,7 +468,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|||||||
except StopAsyncIteration:
|
except StopAsyncIteration:
|
||||||
break
|
break
|
||||||
|
|
||||||
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
||||||
"""Get a checkpoint tuple from the database.
|
"""Get a checkpoint tuple from the database.
|
||||||
|
|
||||||
This method retrieves a checkpoint tuple from the Postgres database based on the
|
This method retrieves a checkpoint tuple from the Postgres database based on the
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import random
|
import random
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from typing import Any, Optional, cast
|
from typing import Any, Optional, cast
|
||||||
@@ -186,7 +188,7 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
|
|||||||
checkpoint_ns: str,
|
checkpoint_ns: str,
|
||||||
values: dict[str, Any],
|
values: dict[str, Any],
|
||||||
versions: ChannelVersions,
|
versions: ChannelVersions,
|
||||||
) -> list[tuple[str, str, str, str, str, Optional[bytes]]]:
|
) -> list[tuple[str, str, str, str, str, bytes | None]]:
|
||||||
if not versions:
|
if not versions:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -244,7 +246,7 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
|
|||||||
for idx, (channel, value) in enumerate(writes)
|
for idx, (channel, value) in enumerate(writes)
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_next_version(self, current: Optional[str]) -> str:
|
def get_next_version(self, current: str | None) -> str:
|
||||||
if current is None:
|
if current is None:
|
||||||
current_v = 0
|
current_v = 0
|
||||||
elif isinstance(current, int):
|
elif isinstance(current, int):
|
||||||
@@ -257,9 +259,9 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
|
|||||||
|
|
||||||
def _search_where(
|
def _search_where(
|
||||||
self,
|
self,
|
||||||
config: Optional[RunnableConfig],
|
config: RunnableConfig | None,
|
||||||
filter: MetadataInput,
|
filter: MetadataInput,
|
||||||
before: Optional[RunnableConfig] = None,
|
before: RunnableConfig | None = None,
|
||||||
) -> tuple[str, list[Any]]:
|
) -> tuple[str, list[Any]]:
|
||||||
"""Return WHERE clause predicates for alist() given config, filter, before.
|
"""Return WHERE clause predicates for alist() given config, filter, before.
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from collections.abc import AsyncIterator, Iterable, Sequence
|
from collections.abc import AsyncIterator, Iterable, Sequence
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Any, Callable, Optional, Union, cast
|
from typing import Any, Callable, cast
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
||||||
@@ -132,12 +134,10 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
|||||||
self,
|
self,
|
||||||
conn: _ainternal.Conn,
|
conn: _ainternal.Conn,
|
||||||
*,
|
*,
|
||||||
pipe: Optional[AsyncPipeline] = None,
|
pipe: AsyncPipeline | None = None,
|
||||||
deserializer: Optional[
|
deserializer: Callable[[bytes | orjson.Fragment], dict[str, Any]] | None = None,
|
||||||
Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]
|
index: PostgresIndexConfig | None = None,
|
||||||
] = None,
|
ttl: TTLConfig | None = None,
|
||||||
index: Optional[PostgresIndexConfig] = None,
|
|
||||||
ttl: Optional[TTLConfig] = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
if isinstance(conn, AsyncConnectionPool) and pipe is not None:
|
if isinstance(conn, AsyncConnectionPool) and pipe is not None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -157,7 +157,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
|||||||
self.embeddings = None
|
self.embeddings = None
|
||||||
|
|
||||||
self.ttl_config = ttl
|
self.ttl_config = ttl
|
||||||
self._ttl_sweeper_task: Optional[asyncio.Task[None]] = None
|
self._ttl_sweeper_task: asyncio.Task[None] | None = None
|
||||||
self._ttl_stop_event = asyncio.Event()
|
self._ttl_stop_event = asyncio.Event()
|
||||||
|
|
||||||
async def abatch(self, ops: Iterable[Op]) -> list[Result]:
|
async def abatch(self, ops: Iterable[Op]) -> list[Result]:
|
||||||
@@ -180,10 +180,10 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
|||||||
conn_string: str,
|
conn_string: str,
|
||||||
*,
|
*,
|
||||||
pipeline: bool = False,
|
pipeline: bool = False,
|
||||||
pool_config: Optional[PoolConfig] = None,
|
pool_config: PoolConfig | None = None,
|
||||||
index: Optional[PostgresIndexConfig] = None,
|
index: PostgresIndexConfig | None = None,
|
||||||
ttl: Optional[TTLConfig] = None,
|
ttl: TTLConfig | None = None,
|
||||||
) -> AsyncIterator["AsyncPostgresStore"]:
|
) -> AsyncIterator[AsyncPostgresStore]:
|
||||||
"""Create a new AsyncPostgresStore instance from a connection string.
|
"""Create a new AsyncPostgresStore instance from a connection string.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -289,7 +289,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
|||||||
return deleted_count
|
return deleted_count
|
||||||
|
|
||||||
async def start_ttl_sweeper(
|
async def start_ttl_sweeper(
|
||||||
self, sweep_interval_minutes: Optional[int] = None
|
self, sweep_interval_minutes: int | None = None
|
||||||
) -> asyncio.Task[None]:
|
) -> asyncio.Task[None]:
|
||||||
"""Periodically delete expired store items based on TTL.
|
"""Periodically delete expired store items based on TTL.
|
||||||
|
|
||||||
@@ -334,7 +334,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
|||||||
self._ttl_sweeper_task = task
|
self._ttl_sweeper_task = task
|
||||||
return task
|
return task
|
||||||
|
|
||||||
async def stop_ttl_sweeper(self, timeout: Optional[float] = None) -> bool:
|
async def stop_ttl_sweeper(self, timeout: float | None = None) -> bool:
|
||||||
"""Stop the TTL sweeper task if it's running.
|
"""Stop the TTL sweeper task if it's running.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -369,14 +369,14 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
|||||||
|
|
||||||
return success
|
return success
|
||||||
|
|
||||||
async def __aenter__(self) -> "AsyncPostgresStore":
|
async def __aenter__(self) -> AsyncPostgresStore:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def __aexit__(
|
async def __aexit__(
|
||||||
self,
|
self,
|
||||||
exc_type: Optional[type[BaseException]],
|
exc_type: type[BaseException] | None,
|
||||||
exc_val: Optional[BaseException],
|
exc_val: BaseException | None,
|
||||||
exc_tb: Optional["TracebackType"],
|
exc_tb: TracebackType | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# Ensure the TTL sweeper task is stopped when exiting the context
|
# Ensure the TTL sweeper task is stopped when exiting the context
|
||||||
if hasattr(self, "_ttl_sweeper_task") and self._ttl_sweeper_task is not None:
|
if hasattr(self, "_ttl_sweeper_task") and self._ttl_sweeper_task is not None:
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import json
|
import json
|
||||||
@@ -14,7 +16,6 @@ from typing import (
|
|||||||
Generic,
|
Generic,
|
||||||
Literal,
|
Literal,
|
||||||
NamedTuple,
|
NamedTuple,
|
||||||
Optional,
|
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
cast,
|
cast,
|
||||||
@@ -56,8 +57,8 @@ class Migration(NamedTuple):
|
|||||||
"""A database migration with optional conditions and parameters."""
|
"""A database migration with optional conditions and parameters."""
|
||||||
|
|
||||||
sql: str
|
sql: str
|
||||||
params: Optional[dict[str, Any]] = None
|
params: dict[str, Any] | None = None
|
||||||
condition: Optional[Callable[["BasePostgresStore"], bool]] = None
|
condition: Callable[[BasePostgresStore], bool] | None = None
|
||||||
|
|
||||||
|
|
||||||
MIGRATIONS: Sequence[str] = [
|
MIGRATIONS: Sequence[str] = [
|
||||||
@@ -155,7 +156,7 @@ class PoolConfig(TypedDict, total=False):
|
|||||||
min_size: int
|
min_size: int
|
||||||
"""Minimum number of connections maintained in the pool. Defaults to 1."""
|
"""Minimum number of connections maintained in the pool. Defaults to 1."""
|
||||||
|
|
||||||
max_size: Optional[int]
|
max_size: int | None
|
||||||
"""Maximum number of connections allowed in the pool. None means unlimited."""
|
"""Maximum number of connections allowed in the pool. None means unlimited."""
|
||||||
|
|
||||||
kwargs: dict
|
kwargs: dict
|
||||||
@@ -230,8 +231,8 @@ class BasePostgresStore(Generic[C]):
|
|||||||
MIGRATIONS = MIGRATIONS
|
MIGRATIONS = MIGRATIONS
|
||||||
VECTOR_MIGRATIONS = VECTOR_MIGRATIONS
|
VECTOR_MIGRATIONS = VECTOR_MIGRATIONS
|
||||||
conn: C
|
conn: C
|
||||||
_deserializer: Optional[Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]]
|
_deserializer: Callable[[bytes | orjson.Fragment], dict[str, Any]] | None
|
||||||
index_config: Optional[PostgresIndexConfig]
|
index_config: PostgresIndexConfig | None
|
||||||
|
|
||||||
def _get_batch_GET_ops_queries(
|
def _get_batch_GET_ops_queries(
|
||||||
self,
|
self,
|
||||||
@@ -293,7 +294,7 @@ class BasePostgresStore(Generic[C]):
|
|||||||
put_ops: Sequence[tuple[int, PutOp]],
|
put_ops: Sequence[tuple[int, PutOp]],
|
||||||
) -> tuple[
|
) -> tuple[
|
||||||
list[tuple[str, Sequence]],
|
list[tuple[str, Sequence]],
|
||||||
Optional[tuple[str, Sequence[tuple[str, str, str, str]]]],
|
tuple[str, Sequence[tuple[str, str, str, str]]] | None,
|
||||||
]:
|
]:
|
||||||
dedupped_ops: dict[tuple[tuple[str, ...], str], PutOp] = {}
|
dedupped_ops: dict[tuple[tuple[str, ...], str], PutOp] = {}
|
||||||
for _, op in put_ops:
|
for _, op in put_ops:
|
||||||
@@ -320,9 +321,7 @@ class BasePostgresStore(Generic[C]):
|
|||||||
)
|
)
|
||||||
params = (_namespace_to_text(namespace), *keys)
|
params = (_namespace_to_text(namespace), *keys)
|
||||||
queries.append((query, params))
|
queries.append((query, params))
|
||||||
embedding_request: Optional[tuple[str, Sequence[tuple[str, str, str, str]]]] = (
|
embedding_request: tuple[str, Sequence[tuple[str, str, str, str]]] | None = None
|
||||||
None
|
|
||||||
)
|
|
||||||
if inserts:
|
if inserts:
|
||||||
values = []
|
values = []
|
||||||
insertion_params = []
|
insertion_params = []
|
||||||
@@ -403,7 +402,7 @@ class BasePostgresStore(Generic[C]):
|
|||||||
self,
|
self,
|
||||||
search_ops: Sequence[tuple[int, SearchOp]],
|
search_ops: Sequence[tuple[int, SearchOp]],
|
||||||
) -> tuple[
|
) -> tuple[
|
||||||
list[tuple[str, list[Union[None, str, list[float]]]]], # queries, params
|
list[tuple[str, list[None | str | list[float]]]], # queries, params
|
||||||
list[tuple[int, str]], # idx, query_text pairs to embed
|
list[tuple[int, str]], # idx, query_text pairs to embed
|
||||||
]:
|
]:
|
||||||
"""
|
"""
|
||||||
@@ -432,7 +431,7 @@ class BasePostgresStore(Generic[C]):
|
|||||||
filter_params.extend([key, orjson.dumps(value).decode("utf-8")])
|
filter_params.extend([key, orjson.dumps(value).decode("utf-8")])
|
||||||
|
|
||||||
ns_condition = "TRUE"
|
ns_condition = "TRUE"
|
||||||
ns_param: Optional[Sequence[Union[str]]] = None
|
ns_param: Sequence[str] | None = None
|
||||||
if op.namespace_prefix:
|
if op.namespace_prefix:
|
||||||
ns_condition = "store.prefix LIKE %s"
|
ns_condition = "store.prefix LIKE %s"
|
||||||
ns_param = (f"{_namespace_to_text(op.namespace_prefix)}%",)
|
ns_param = (f"{_namespace_to_text(op.namespace_prefix)}%",)
|
||||||
@@ -719,12 +718,10 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
|||||||
self,
|
self,
|
||||||
conn: _pg_internal.Conn,
|
conn: _pg_internal.Conn,
|
||||||
*,
|
*,
|
||||||
pipe: Optional[Pipeline] = None,
|
pipe: Pipeline | None = None,
|
||||||
deserializer: Optional[
|
deserializer: Callable[[bytes | orjson.Fragment], dict[str, Any]] | None = None,
|
||||||
Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]
|
index: PostgresIndexConfig | None = None,
|
||||||
] = None,
|
ttl: TTLConfig | None = None,
|
||||||
index: Optional[PostgresIndexConfig] = None,
|
|
||||||
ttl: Optional[TTLConfig] = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._deserializer = deserializer
|
self._deserializer = deserializer
|
||||||
@@ -738,7 +735,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
|||||||
else:
|
else:
|
||||||
self.embeddings = None
|
self.embeddings = None
|
||||||
self.ttl_config = ttl
|
self.ttl_config = ttl
|
||||||
self._ttl_sweeper_thread: Optional[threading.Thread] = None
|
self._ttl_sweeper_thread: threading.Thread | None = None
|
||||||
self._ttl_stop_event = threading.Event()
|
self._ttl_stop_event = threading.Event()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -748,10 +745,10 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
|||||||
conn_string: str,
|
conn_string: str,
|
||||||
*,
|
*,
|
||||||
pipeline: bool = False,
|
pipeline: bool = False,
|
||||||
pool_config: Optional[PoolConfig] = None,
|
pool_config: PoolConfig | None = None,
|
||||||
index: Optional[PostgresIndexConfig] = None,
|
index: PostgresIndexConfig | None = None,
|
||||||
ttl: Optional[TTLConfig] = None,
|
ttl: TTLConfig | None = None,
|
||||||
) -> Iterator["PostgresStore"]:
|
) -> Iterator[PostgresStore]:
|
||||||
"""Create a new PostgresStore instance from a connection string.
|
"""Create a new PostgresStore instance from a connection string.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -810,7 +807,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
|||||||
return deleted_count
|
return deleted_count
|
||||||
|
|
||||||
def start_ttl_sweeper(
|
def start_ttl_sweeper(
|
||||||
self, sweep_interval_minutes: Optional[int] = None
|
self, sweep_interval_minutes: int | None = None
|
||||||
) -> concurrent.futures.Future[None]:
|
) -> concurrent.futures.Future[None]:
|
||||||
"""Periodically delete expired store items based on TTL.
|
"""Periodically delete expired store items based on TTL.
|
||||||
|
|
||||||
@@ -867,7 +864,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
|||||||
)
|
)
|
||||||
return future
|
return future
|
||||||
|
|
||||||
def stop_ttl_sweeper(self, timeout: Optional[float] = None) -> bool:
|
def stop_ttl_sweeper(self, timeout: float | None = None) -> bool:
|
||||||
"""Stop the TTL sweeper thread if it's running.
|
"""Stop the TTL sweeper thread if it's running.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -1196,7 +1193,7 @@ def _row_to_item(
|
|||||||
namespace: tuple[str, ...],
|
namespace: tuple[str, ...],
|
||||||
row: Row,
|
row: Row,
|
||||||
*,
|
*,
|
||||||
loader: Optional[Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]] = None,
|
loader: Callable[[bytes | orjson.Fragment], dict[str, Any]] | None = None,
|
||||||
) -> Item:
|
) -> Item:
|
||||||
"""Convert a row from the database into an Item.
|
"""Convert a row from the database into an Item.
|
||||||
|
|
||||||
@@ -1224,7 +1221,7 @@ def _row_to_search_item(
|
|||||||
namespace: tuple[str, ...],
|
namespace: tuple[str, ...],
|
||||||
row: Row,
|
row: Row,
|
||||||
*,
|
*,
|
||||||
loader: Optional[Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]] = None,
|
loader: Callable[[bytes | orjson.Fragment], dict[str, Any]] | None = None,
|
||||||
) -> SearchItem:
|
) -> SearchItem:
|
||||||
"""Convert a row from the database into an Item."""
|
"""Convert a row from the database into an Item."""
|
||||||
loader = loader or _json_loads
|
loader = loader or _json_loads
|
||||||
@@ -1255,7 +1252,7 @@ def _group_ops(ops: Iterable[Op]) -> tuple[dict[type, list[tuple[int, Op]]], int
|
|||||||
return grouped_ops, tot
|
return grouped_ops, tot
|
||||||
|
|
||||||
|
|
||||||
def _json_loads(content: Union[bytes, orjson.Fragment]) -> Any:
|
def _json_loads(content: bytes | orjson.Fragment) -> Any:
|
||||||
if isinstance(content, orjson.Fragment):
|
if isinstance(content, orjson.Fragment):
|
||||||
if hasattr(content, "buf"):
|
if hasattr(content, "buf"):
|
||||||
content = content.buf
|
content = content.buf
|
||||||
@@ -1267,7 +1264,7 @@ def _json_loads(content: Union[bytes, orjson.Fragment]) -> Any:
|
|||||||
return orjson.loads(cast(bytes, content))
|
return orjson.loads(cast(bytes, content))
|
||||||
|
|
||||||
|
|
||||||
def _decode_ns_bytes(namespace: Union[str, bytes, list]) -> tuple[str, ...]:
|
def _decode_ns_bytes(namespace: str | bytes | list) -> tuple[str, ...]:
|
||||||
if isinstance(namespace, list):
|
if isinstance(namespace, list):
|
||||||
return tuple(namespace)
|
return tuple(namespace)
|
||||||
if isinstance(namespace, bytes):
|
if isinstance(namespace, bytes):
|
||||||
@@ -1316,9 +1313,9 @@ def get_distance_operator(store: Any) -> tuple[str, str]:
|
|||||||
|
|
||||||
def _ensure_index_config(
|
def _ensure_index_config(
|
||||||
index_config: PostgresIndexConfig,
|
index_config: PostgresIndexConfig,
|
||||||
) -> tuple[Optional["Embeddings"], PostgresIndexConfig]:
|
) -> tuple[Embeddings | None, PostgresIndexConfig]:
|
||||||
index_config = index_config.copy()
|
index_config = index_config.copy()
|
||||||
tokenized: list[tuple[str, Union[Literal["$"], list[str]]]] = []
|
tokenized: list[tuple[str, Literal["$"] | list[str]]] = []
|
||||||
tot = 0
|
tot = 0
|
||||||
text_fields = index_config.get("fields") or ["$"]
|
text_fields = index_config.get("fields") or ["$"]
|
||||||
if isinstance(text_fields, str):
|
if isinstance(text_fields, str):
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ lint.select = [
|
|||||||
"B", # flake8-bugbear
|
"B", # flake8-bugbear
|
||||||
"I", # isort
|
"I", # isort
|
||||||
]
|
]
|
||||||
lint.ignore = ["E501", "B008", "UP007", "UP006"]
|
lint.ignore = ["E501", "B008"]
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
# https://mypy.readthedocs.io/en/stable/config_file.html
|
# https://mypy.readthedocs.io/en/stable/config_file.html
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Optional, Protocol
|
from typing import Any, Protocol
|
||||||
|
|
||||||
from langgraph.checkpoint.base import Checkpoint, EmptyChannelError
|
from langgraph.checkpoint.base import Checkpoint, EmptyChannelError
|
||||||
from langgraph.checkpoint.base.id import uuid6
|
from langgraph.checkpoint.base.id import uuid6
|
||||||
|
|
||||||
|
|
||||||
class ChannelProtocol(Protocol):
|
class ChannelProtocol(Protocol):
|
||||||
def checkpoint(self) -> Optional[Any]: ...
|
def checkpoint(self) -> Any | None: ...
|
||||||
|
|
||||||
|
|
||||||
def empty_checkpoint() -> Checkpoint:
|
def empty_checkpoint() -> Checkpoint:
|
||||||
@@ -23,10 +25,10 @@ def empty_checkpoint() -> Checkpoint:
|
|||||||
|
|
||||||
def create_checkpoint(
|
def create_checkpoint(
|
||||||
checkpoint: Checkpoint,
|
checkpoint: Checkpoint,
|
||||||
channels: Optional[Mapping[str, ChannelProtocol]],
|
channels: Mapping[str, ChannelProtocol] | None,
|
||||||
step: int,
|
step: int,
|
||||||
*,
|
*,
|
||||||
id: Optional[str] = None,
|
id: str | None = None,
|
||||||
) -> Checkpoint:
|
) -> Checkpoint:
|
||||||
"""Create a checkpoint for the given channels."""
|
"""Create a checkpoint for the given channels."""
|
||||||
ts = datetime.now(timezone.utc).isoformat()
|
ts = datetime.now(timezone.utc).isoformat()
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
# type: ignore
|
# type: ignore
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import itertools
|
import itertools
|
||||||
import sys
|
import sys
|
||||||
@@ -6,7 +8,7 @@ import uuid
|
|||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from langchain_core.embeddings import Embeddings
|
from langchain_core.embeddings import Embeddings
|
||||||
@@ -353,7 +355,7 @@ async def _create_vector_store(
|
|||||||
vector_type: str,
|
vector_type: str,
|
||||||
distance_type: str,
|
distance_type: str,
|
||||||
fake_embeddings: CharacterEmbeddings,
|
fake_embeddings: CharacterEmbeddings,
|
||||||
text_fields: Optional[list[str]] = None,
|
text_fields: list[str] | None = None,
|
||||||
) -> AsyncIterator[AsyncPostgresStore]:
|
) -> AsyncIterator[AsyncPostgresStore]:
|
||||||
"""Create a store with vector search enabled."""
|
"""Create a store with vector search enabled."""
|
||||||
if sys.version_info < (3, 10):
|
if sys.version_info < (3, 10):
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
# type: ignore
|
# type: ignore
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -379,7 +380,7 @@ def _create_vector_store(
|
|||||||
vector_type: str,
|
vector_type: str,
|
||||||
distance_type: str,
|
distance_type: str,
|
||||||
fake_embeddings: Embeddings,
|
fake_embeddings: Embeddings,
|
||||||
text_fields: Optional[list[str]] = None,
|
text_fields: list[str] | None = None,
|
||||||
enable_ttl: bool = True,
|
enable_ttl: bool = True,
|
||||||
) -> PostgresStore:
|
) -> PostgresStore:
|
||||||
"""Create a store with vector search enabled."""
|
"""Create a store with vector search enabled."""
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import random
|
import random
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import threading
|
import threading
|
||||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||||
from contextlib import closing, contextmanager
|
from contextlib import closing, contextmanager
|
||||||
from typing import Any, Optional, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
|
|
||||||
@@ -76,7 +78,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
self,
|
self,
|
||||||
conn: sqlite3.Connection,
|
conn: sqlite3.Connection,
|
||||||
*,
|
*,
|
||||||
serde: Optional[SerializerProtocol] = None,
|
serde: SerializerProtocol | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(serde=serde)
|
super().__init__(serde=serde)
|
||||||
self.jsonplus_serde = JsonPlusSerializer()
|
self.jsonplus_serde = JsonPlusSerializer()
|
||||||
@@ -86,7 +88,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def from_conn_string(cls, conn_string: str) -> Iterator["SqliteSaver"]:
|
def from_conn_string(cls, conn_string: str) -> Iterator[SqliteSaver]:
|
||||||
"""Create a new SqliteSaver instance from a connection string.
|
"""Create a new SqliteSaver instance from a connection string.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -178,7 +180,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
cur.close()
|
cur.close()
|
||||||
|
|
||||||
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
||||||
"""Get a checkpoint tuple from the database.
|
"""Get a checkpoint tuple from the database.
|
||||||
|
|
||||||
This method retrieves a checkpoint tuple from the SQLite database based on the
|
This method retrieves a checkpoint tuple from the SQLite database based on the
|
||||||
@@ -286,11 +288,11 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
|
|
||||||
def list(
|
def list(
|
||||||
self,
|
self,
|
||||||
config: Optional[RunnableConfig],
|
config: RunnableConfig | None,
|
||||||
*,
|
*,
|
||||||
filter: Optional[dict[str, Any]] = None,
|
filter: dict[str, Any] | None = None,
|
||||||
before: Optional[RunnableConfig] = None,
|
before: RunnableConfig | None = None,
|
||||||
limit: Optional[int] = None,
|
limit: int | None = None,
|
||||||
) -> Iterator[CheckpointTuple]:
|
) -> Iterator[CheckpointTuple]:
|
||||||
"""List checkpoints from the database.
|
"""List checkpoints from the database.
|
||||||
|
|
||||||
@@ -493,7 +495,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
(str(thread_id),),
|
(str(thread_id),),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
||||||
"""Get a checkpoint tuple from the database asynchronously.
|
"""Get a checkpoint tuple from the database asynchronously.
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
@@ -504,11 +506,11 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
|
|
||||||
async def alist(
|
async def alist(
|
||||||
self,
|
self,
|
||||||
config: Optional[RunnableConfig],
|
config: RunnableConfig | None,
|
||||||
*,
|
*,
|
||||||
filter: Optional[dict[str, Any]] = None,
|
filter: dict[str, Any] | None = None,
|
||||||
before: Optional[RunnableConfig] = None,
|
before: RunnableConfig | None = None,
|
||||||
limit: Optional[int] = None,
|
limit: int | None = None,
|
||||||
) -> AsyncIterator[CheckpointTuple]:
|
) -> AsyncIterator[CheckpointTuple]:
|
||||||
"""List checkpoints from the database asynchronously.
|
"""List checkpoints from the database asynchronously.
|
||||||
|
|
||||||
@@ -534,7 +536,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError(_AIO_ERROR_MSG)
|
raise NotImplementedError(_AIO_ERROR_MSG)
|
||||||
|
|
||||||
def get_next_version(self, current: Optional[str]) -> str:
|
def get_next_version(self, current: str | None) -> str:
|
||||||
"""Generate the next version ID for a channel.
|
"""Generate the next version ID for a channel.
|
||||||
|
|
||||||
This method creates a new version identifier for a channel based on its current version.
|
This method creates a new version identifier for a channel based on its current version.
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import random
|
import random
|
||||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from typing import Any, Callable, Optional, TypeVar, cast
|
from typing import Any, Callable, TypeVar, cast
|
||||||
|
|
||||||
import aiosqlite
|
import aiosqlite
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
@@ -108,7 +110,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
self,
|
self,
|
||||||
conn: aiosqlite.Connection,
|
conn: aiosqlite.Connection,
|
||||||
*,
|
*,
|
||||||
serde: Optional[SerializerProtocol] = None,
|
serde: SerializerProtocol | None = None,
|
||||||
):
|
):
|
||||||
super().__init__(serde=serde)
|
super().__init__(serde=serde)
|
||||||
self.jsonplus_serde = JsonPlusSerializer()
|
self.jsonplus_serde = JsonPlusSerializer()
|
||||||
@@ -121,7 +123,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def from_conn_string(
|
async def from_conn_string(
|
||||||
cls, conn_string: str
|
cls, conn_string: str
|
||||||
) -> AsyncIterator["AsyncSqliteSaver"]:
|
) -> AsyncIterator[AsyncSqliteSaver]:
|
||||||
"""Create a new AsyncSqliteSaver instance from a connection string.
|
"""Create a new AsyncSqliteSaver instance from a connection string.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -133,7 +135,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
async with aiosqlite.connect(conn_string) as conn:
|
async with aiosqlite.connect(conn_string) as conn:
|
||||||
yield cls(conn)
|
yield cls(conn)
|
||||||
|
|
||||||
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
||||||
"""Get a checkpoint tuple from the database.
|
"""Get a checkpoint tuple from the database.
|
||||||
|
|
||||||
This method retrieves a checkpoint tuple from the SQLite database based on the
|
This method retrieves a checkpoint tuple from the SQLite database based on the
|
||||||
@@ -165,11 +167,11 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
|
|
||||||
def list(
|
def list(
|
||||||
self,
|
self,
|
||||||
config: Optional[RunnableConfig],
|
config: RunnableConfig | None,
|
||||||
*,
|
*,
|
||||||
filter: Optional[dict[str, Any]] = None,
|
filter: dict[str, Any] | None = None,
|
||||||
before: Optional[RunnableConfig] = None,
|
before: RunnableConfig | None = None,
|
||||||
limit: Optional[int] = None,
|
limit: int | None = None,
|
||||||
) -> Iterator[CheckpointTuple]:
|
) -> Iterator[CheckpointTuple]:
|
||||||
"""List checkpoints from the database asynchronously.
|
"""List checkpoints from the database asynchronously.
|
||||||
|
|
||||||
@@ -310,7 +312,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
|
|
||||||
self.is_setup = True
|
self.is_setup = True
|
||||||
|
|
||||||
async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
||||||
"""Get a checkpoint tuple from the database asynchronously.
|
"""Get a checkpoint tuple from the database asynchronously.
|
||||||
|
|
||||||
This method retrieves a checkpoint tuple from the SQLite database based on the
|
This method retrieves a checkpoint tuple from the SQLite database based on the
|
||||||
@@ -398,11 +400,11 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
|
|
||||||
async def alist(
|
async def alist(
|
||||||
self,
|
self,
|
||||||
config: Optional[RunnableConfig],
|
config: RunnableConfig | None,
|
||||||
*,
|
*,
|
||||||
filter: Optional[dict[str, Any]] = None,
|
filter: dict[str, Any] | None = None,
|
||||||
before: Optional[RunnableConfig] = None,
|
before: RunnableConfig | None = None,
|
||||||
limit: Optional[int] = None,
|
limit: int | None = None,
|
||||||
) -> AsyncIterator[CheckpointTuple]:
|
) -> AsyncIterator[CheckpointTuple]:
|
||||||
"""List checkpoints from the database asynchronously.
|
"""List checkpoints from the database asynchronously.
|
||||||
|
|
||||||
@@ -589,7 +591,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
|||||||
)
|
)
|
||||||
await self.conn.commit()
|
await self.conn.commit()
|
||||||
|
|
||||||
def get_next_version(self, current: Optional[str]) -> str:
|
def get_next_version(self, current: str | None) -> str:
|
||||||
"""Generate the next version ID for a channel.
|
"""Generate the next version ID for a channel.
|
||||||
|
|
||||||
This method creates a new version identifier for a channel based on its current version.
|
This method creates a new version identifier for a channel based on its current version.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
|
|
||||||
@@ -52,9 +54,9 @@ def _metadata_predicate(
|
|||||||
|
|
||||||
|
|
||||||
def search_where(
|
def search_where(
|
||||||
config: Optional[RunnableConfig],
|
config: RunnableConfig | None,
|
||||||
filter: Optional[dict[str, Any]],
|
filter: dict[str, Any] | None,
|
||||||
before: Optional[RunnableConfig] = None,
|
before: RunnableConfig | None = None,
|
||||||
) -> tuple[str, Sequence[Any]]:
|
) -> tuple[str, Sequence[Any]]:
|
||||||
"""Return WHERE clause predicates for (a)search() given metadata filter
|
"""Return WHERE clause predicates for (a)search() given metadata filter
|
||||||
and `before` config.
|
and `before` config.
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import AsyncIterator, Iterable, Sequence
|
from collections.abc import AsyncIterator, Iterable, Sequence
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Any, Callable, Optional, Union, cast
|
from typing import Any, Callable, cast
|
||||||
|
|
||||||
import aiosqlite
|
import aiosqlite
|
||||||
import orjson
|
import orjson
|
||||||
@@ -88,11 +90,10 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|||||||
self,
|
self,
|
||||||
conn: aiosqlite.Connection,
|
conn: aiosqlite.Connection,
|
||||||
*,
|
*,
|
||||||
deserializer: Optional[
|
deserializer: Callable[[bytes | str | orjson.Fragment], dict[str, Any]]
|
||||||
Callable[[Union[bytes, str, orjson.Fragment]], dict[str, Any]]
|
| None = None,
|
||||||
] = None,
|
index: SqliteIndexConfig | None = None,
|
||||||
index: Optional[SqliteIndexConfig] = None,
|
ttl: TTLConfig | None = None,
|
||||||
ttl: Optional[TTLConfig] = None,
|
|
||||||
):
|
):
|
||||||
"""Initialize the async SQLite store.
|
"""Initialize the async SQLite store.
|
||||||
|
|
||||||
@@ -114,7 +115,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|||||||
else:
|
else:
|
||||||
self.embeddings = None
|
self.embeddings = None
|
||||||
self.ttl_config = ttl
|
self.ttl_config = ttl
|
||||||
self._ttl_sweeper_task: Optional[asyncio.Task[None]] = None
|
self._ttl_sweeper_task: asyncio.Task[None] | None = None
|
||||||
self._ttl_stop_event = asyncio.Event()
|
self._ttl_stop_event = asyncio.Event()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -123,9 +124,9 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|||||||
cls,
|
cls,
|
||||||
conn_string: str,
|
conn_string: str,
|
||||||
*,
|
*,
|
||||||
index: Optional[SqliteIndexConfig] = None,
|
index: SqliteIndexConfig | None = None,
|
||||||
ttl: Optional[TTLConfig] = None,
|
ttl: TTLConfig | None = None,
|
||||||
) -> AsyncIterator["AsyncSqliteStore"]:
|
) -> AsyncIterator[AsyncSqliteStore]:
|
||||||
"""Create a new AsyncSqliteStore instance from a connection string.
|
"""Create a new AsyncSqliteStore instance from a connection string.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -253,7 +254,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|||||||
return deleted_count
|
return deleted_count
|
||||||
|
|
||||||
async def start_ttl_sweeper(
|
async def start_ttl_sweeper(
|
||||||
self, sweep_interval_minutes: Optional[int] = None
|
self, sweep_interval_minutes: int | None = None
|
||||||
) -> asyncio.Task[None]:
|
) -> asyncio.Task[None]:
|
||||||
"""Periodically delete expired store items based on TTL.
|
"""Periodically delete expired store items based on TTL.
|
||||||
|
|
||||||
@@ -298,7 +299,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|||||||
self._ttl_sweeper_task = task
|
self._ttl_sweeper_task = task
|
||||||
return task
|
return task
|
||||||
|
|
||||||
async def stop_ttl_sweeper(self, timeout: Optional[float] = None) -> bool:
|
async def stop_ttl_sweeper(self, timeout: float | None = None) -> bool:
|
||||||
"""Stop the TTL sweeper task if it's running.
|
"""Stop the TTL sweeper task if it's running.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -333,14 +334,14 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
|
|||||||
|
|
||||||
return success
|
return success
|
||||||
|
|
||||||
async def __aenter__(self) -> "AsyncSqliteStore":
|
async def __aenter__(self) -> AsyncSqliteStore:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def __aexit__(
|
async def __aexit__(
|
||||||
self,
|
self,
|
||||||
exc_type: Optional[type[BaseException]],
|
exc_type: type[BaseException] | None,
|
||||||
exc_val: Optional[BaseException],
|
exc_val: BaseException | None,
|
||||||
exc_tb: Optional["TracebackType"],
|
exc_tb: TracebackType | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# Ensure the TTL sweeper task is stopped when exiting the context
|
# Ensure the TTL sweeper task is stopped when exiting the context
|
||||||
if hasattr(self, "_ttl_sweeper_task") and self._ttl_sweeper_task is not None:
|
if hasattr(self, "_ttl_sweeper_task") and self._ttl_sweeper_task is not None:
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
@@ -6,7 +8,7 @@ import threading
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Iterable, Iterator, Sequence
|
from collections.abc import Iterable, Iterator, Sequence
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import Any, Callable, Literal, NamedTuple, Optional, Union, cast
|
from typing import Any, Callable, Literal, NamedTuple, cast
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
import sqlite_vec # type: ignore[import-untyped]
|
import sqlite_vec # type: ignore[import-untyped]
|
||||||
@@ -105,7 +107,7 @@ def _decode_ns_text(namespace: str) -> tuple[str, ...]:
|
|||||||
return tuple(namespace.split("."))
|
return tuple(namespace.split("."))
|
||||||
|
|
||||||
|
|
||||||
def _json_loads(content: Union[bytes, str, orjson.Fragment]) -> Any:
|
def _json_loads(content: bytes | str | orjson.Fragment) -> Any:
|
||||||
if isinstance(content, orjson.Fragment):
|
if isinstance(content, orjson.Fragment):
|
||||||
if hasattr(content, "buf"):
|
if hasattr(content, "buf"):
|
||||||
content = content.buf
|
content = content.buf
|
||||||
@@ -125,9 +127,7 @@ def _row_to_item(
|
|||||||
namespace: tuple[str, ...],
|
namespace: tuple[str, ...],
|
||||||
row: dict[str, Any],
|
row: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
loader: Optional[
|
loader: Callable[[bytes | str | orjson.Fragment], dict[str, Any]] | None = None,
|
||||||
Callable[[Union[bytes, str, orjson.Fragment]], dict[str, Any]]
|
|
||||||
] = None,
|
|
||||||
) -> Item:
|
) -> Item:
|
||||||
"""Convert a row from the database into an Item."""
|
"""Convert a row from the database into an Item."""
|
||||||
val = row["value"]
|
val = row["value"]
|
||||||
@@ -149,9 +149,7 @@ def _row_to_search_item(
|
|||||||
namespace: tuple[str, ...],
|
namespace: tuple[str, ...],
|
||||||
row: dict[str, Any],
|
row: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
loader: Optional[
|
loader: Callable[[bytes | str | orjson.Fragment], dict[str, Any]] | None = None,
|
||||||
Callable[[Union[bytes, str, orjson.Fragment]], dict[str, Any]]
|
|
||||||
] = None,
|
|
||||||
) -> SearchItem:
|
) -> SearchItem:
|
||||||
"""Convert a row from the database into a SearchItem."""
|
"""Convert a row from the database into a SearchItem."""
|
||||||
loader = loader or _json_loads
|
loader = loader or _json_loads
|
||||||
@@ -196,8 +194,8 @@ class BaseSqliteStore:
|
|||||||
MIGRATIONS = MIGRATIONS
|
MIGRATIONS = MIGRATIONS
|
||||||
VECTOR_MIGRATIONS = VECTOR_MIGRATIONS
|
VECTOR_MIGRATIONS = VECTOR_MIGRATIONS
|
||||||
supports_ttl = True
|
supports_ttl = True
|
||||||
index_config: Optional[SqliteIndexConfig] = None
|
index_config: SqliteIndexConfig | None = None
|
||||||
ttl_config: Optional[TTLConfig] = None
|
ttl_config: TTLConfig | None = None
|
||||||
|
|
||||||
def _get_batch_GET_ops_queries(
|
def _get_batch_GET_ops_queries(
|
||||||
self, get_ops: Sequence[tuple[int, GetOp]]
|
self, get_ops: Sequence[tuple[int, GetOp]]
|
||||||
@@ -259,7 +257,7 @@ class BaseSqliteStore:
|
|||||||
self, put_ops: Sequence[tuple[int, PutOp]]
|
self, put_ops: Sequence[tuple[int, PutOp]]
|
||||||
) -> tuple[
|
) -> tuple[
|
||||||
list[tuple[str, Sequence]],
|
list[tuple[str, Sequence]],
|
||||||
Optional[tuple[str, Sequence[tuple[str, str, str, str]]]],
|
tuple[str, Sequence[tuple[str, str, str, str]]] | None,
|
||||||
]:
|
]:
|
||||||
# Last-write wins
|
# Last-write wins
|
||||||
dedupped_ops: dict[tuple[tuple[str, ...], str], PutOp] = {}
|
dedupped_ops: dict[tuple[tuple[str, ...], str], PutOp] = {}
|
||||||
@@ -288,9 +286,7 @@ class BaseSqliteStore:
|
|||||||
params = (_namespace_to_text(namespace), *keys)
|
params = (_namespace_to_text(namespace), *keys)
|
||||||
queries.append((query, params))
|
queries.append((query, params))
|
||||||
|
|
||||||
embedding_request: Optional[tuple[str, Sequence[tuple[str, str, str, str]]]] = (
|
embedding_request: tuple[str, Sequence[tuple[str, str, str, str]]] | None = None
|
||||||
None
|
|
||||||
)
|
|
||||||
if inserts:
|
if inserts:
|
||||||
values = []
|
values = []
|
||||||
insertion_params = []
|
insertion_params = []
|
||||||
@@ -358,7 +354,7 @@ class BaseSqliteStore:
|
|||||||
def _prepare_batch_search_queries(
|
def _prepare_batch_search_queries(
|
||||||
self, search_ops: Sequence[tuple[int, SearchOp]]
|
self, search_ops: Sequence[tuple[int, SearchOp]]
|
||||||
) -> tuple[
|
) -> tuple[
|
||||||
list[tuple[str, list[Union[None, str, list[float]]]]], # queries, params
|
list[tuple[str, list[None | str | list[float]]]], # queries, params
|
||||||
list[tuple[int, str]], # idx, query_text pairs to embed
|
list[tuple[int, str]], # idx, query_text pairs to embed
|
||||||
]:
|
]:
|
||||||
"""
|
"""
|
||||||
@@ -785,11 +781,10 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
|||||||
self,
|
self,
|
||||||
conn: sqlite3.Connection,
|
conn: sqlite3.Connection,
|
||||||
*,
|
*,
|
||||||
deserializer: Optional[
|
deserializer: Callable[[bytes | str | orjson.Fragment], dict[str, Any]]
|
||||||
Callable[[Union[bytes, str, orjson.Fragment]], dict[str, Any]]
|
| None = None,
|
||||||
] = None,
|
index: SqliteIndexConfig | None = None,
|
||||||
index: Optional[SqliteIndexConfig] = None,
|
ttl: TTLConfig | None = None,
|
||||||
ttl: Optional[TTLConfig] = None,
|
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._deserializer = deserializer
|
self._deserializer = deserializer
|
||||||
@@ -802,7 +797,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
|||||||
else:
|
else:
|
||||||
self.embeddings = None
|
self.embeddings = None
|
||||||
self.ttl_config = ttl
|
self.ttl_config = ttl
|
||||||
self._ttl_sweeper_thread: Optional[threading.Thread] = None
|
self._ttl_sweeper_thread: threading.Thread | None = None
|
||||||
self._ttl_stop_event = threading.Event()
|
self._ttl_stop_event = threading.Event()
|
||||||
|
|
||||||
def _get_batch_GET_ops_queries(
|
def _get_batch_GET_ops_queries(
|
||||||
@@ -956,9 +951,9 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
|||||||
cls,
|
cls,
|
||||||
conn_string: str,
|
conn_string: str,
|
||||||
*,
|
*,
|
||||||
index: Optional[SqliteIndexConfig] = None,
|
index: SqliteIndexConfig | None = None,
|
||||||
ttl: Optional[TTLConfig] = None,
|
ttl: TTLConfig | None = None,
|
||||||
) -> Iterator["SqliteStore"]:
|
) -> Iterator[SqliteStore]:
|
||||||
"""Create a new SqliteStore instance from a connection string.
|
"""Create a new SqliteStore instance from a connection string.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -1087,7 +1082,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
|||||||
return deleted_count
|
return deleted_count
|
||||||
|
|
||||||
def start_ttl_sweeper(
|
def start_ttl_sweeper(
|
||||||
self, sweep_interval_minutes: Optional[int] = None
|
self, sweep_interval_minutes: int | None = None
|
||||||
) -> concurrent.futures.Future[None]:
|
) -> concurrent.futures.Future[None]:
|
||||||
"""Periodically delete expired store items based on TTL.
|
"""Periodically delete expired store items based on TTL.
|
||||||
|
|
||||||
@@ -1144,7 +1139,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
|
|||||||
)
|
)
|
||||||
return future
|
return future
|
||||||
|
|
||||||
def stop_ttl_sweeper(self, timeout: Optional[float] = None) -> bool:
|
def stop_ttl_sweeper(self, timeout: float | None = None) -> bool:
|
||||||
"""Stop the TTL sweeper thread if it's running.
|
"""Stop the TTL sweeper thread if it's running.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -1396,7 +1391,7 @@ def _ensure_index_config(
|
|||||||
) -> tuple[Any, SqliteIndexConfig]:
|
) -> tuple[Any, SqliteIndexConfig]:
|
||||||
"""Process and validate index configuration."""
|
"""Process and validate index configuration."""
|
||||||
index_config = index_config.copy()
|
index_config = index_config.copy()
|
||||||
tokenized: list[tuple[str, Union[Literal["$"], list[str]]]] = []
|
tokenized: list[tuple[str, Literal["$"] | list[str]]] = []
|
||||||
tot = 0
|
tot = 0
|
||||||
text_fields = index_config.get("text_fields") or ["$"]
|
text_fields = index_config.get("text_fields") or ["$"]
|
||||||
if isinstance(text_fields, str):
|
if isinstance(text_fields, str):
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ lint.select = [
|
|||||||
"B", # flake8-bugbear
|
"B", # flake8-bugbear
|
||||||
"I", # isort
|
"I", # isort
|
||||||
]
|
]
|
||||||
lint.ignore = ["E501", "B008", "UP007", "UP006"]
|
lint.ignore = ["E501", "B008"]
|
||||||
|
|
||||||
[tool.pytest-watcher]
|
[tool.pytest-watcher]
|
||||||
now = true
|
now = true
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Optional, Protocol
|
from typing import Any, Protocol
|
||||||
|
|
||||||
from langgraph.checkpoint.base import Checkpoint, EmptyChannelError
|
from langgraph.checkpoint.base import Checkpoint, EmptyChannelError
|
||||||
from langgraph.checkpoint.base.id import uuid6
|
from langgraph.checkpoint.base.id import uuid6
|
||||||
|
|
||||||
|
|
||||||
class ChannelProtocol(Protocol):
|
class ChannelProtocol(Protocol):
|
||||||
def checkpoint(self) -> Optional[Any]: ...
|
def checkpoint(self) -> Any | None: ...
|
||||||
|
|
||||||
|
|
||||||
def empty_checkpoint() -> Checkpoint:
|
def empty_checkpoint() -> Checkpoint:
|
||||||
@@ -23,10 +25,10 @@ def empty_checkpoint() -> Checkpoint:
|
|||||||
|
|
||||||
def create_checkpoint(
|
def create_checkpoint(
|
||||||
checkpoint: Checkpoint,
|
checkpoint: Checkpoint,
|
||||||
channels: Optional[Mapping[str, ChannelProtocol]],
|
channels: Mapping[str, ChannelProtocol] | None,
|
||||||
step: int,
|
step: int,
|
||||||
*,
|
*,
|
||||||
id: Optional[str] = None,
|
id: str | None = None,
|
||||||
) -> Checkpoint:
|
) -> Checkpoint:
|
||||||
"""Create a checkpoint for the given channels."""
|
"""Create a checkpoint for the given channels."""
|
||||||
ts = datetime.now(timezone.utc).isoformat()
|
ts = datetime.now(timezone.utc).isoformat()
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||||
from typing import ( # noqa: UP035
|
from typing import ( # noqa: UP035
|
||||||
Any,
|
Any,
|
||||||
Generic,
|
Generic,
|
||||||
List,
|
|
||||||
Literal,
|
Literal,
|
||||||
NamedTuple,
|
NamedTuple,
|
||||||
Optional,
|
|
||||||
TypedDict,
|
TypedDict,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
@@ -98,8 +98,8 @@ class CheckpointTuple(NamedTuple):
|
|||||||
config: RunnableConfig
|
config: RunnableConfig
|
||||||
checkpoint: Checkpoint
|
checkpoint: Checkpoint
|
||||||
metadata: CheckpointMetadata
|
metadata: CheckpointMetadata
|
||||||
parent_config: Optional[RunnableConfig] = None
|
parent_config: RunnableConfig | None = None
|
||||||
pending_writes: Optional[List[PendingWrite]] = None
|
pending_writes: list[PendingWrite] | None = None
|
||||||
|
|
||||||
|
|
||||||
class BaseCheckpointSaver(Generic[V]):
|
class BaseCheckpointSaver(Generic[V]):
|
||||||
@@ -121,11 +121,11 @@ class BaseCheckpointSaver(Generic[V]):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
serde: Optional[SerializerProtocol] = None,
|
serde: SerializerProtocol | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.serde = maybe_add_typed_methods(serde or self.serde)
|
self.serde = maybe_add_typed_methods(serde or self.serde)
|
||||||
|
|
||||||
def get(self, config: RunnableConfig) -> Optional[Checkpoint]:
|
def get(self, config: RunnableConfig) -> Checkpoint | None:
|
||||||
"""Fetch a checkpoint using the given configuration.
|
"""Fetch a checkpoint using the given configuration.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -137,7 +137,7 @@ class BaseCheckpointSaver(Generic[V]):
|
|||||||
if value := self.get_tuple(config):
|
if value := self.get_tuple(config):
|
||||||
return value.checkpoint
|
return value.checkpoint
|
||||||
|
|
||||||
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
||||||
"""Fetch a checkpoint tuple using the given configuration.
|
"""Fetch a checkpoint tuple using the given configuration.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -153,11 +153,11 @@ class BaseCheckpointSaver(Generic[V]):
|
|||||||
|
|
||||||
def list(
|
def list(
|
||||||
self,
|
self,
|
||||||
config: Optional[RunnableConfig],
|
config: RunnableConfig | None,
|
||||||
*,
|
*,
|
||||||
filter: Optional[dict[str, Any]] = None,
|
filter: dict[str, Any] | None = None,
|
||||||
before: Optional[RunnableConfig] = None,
|
before: RunnableConfig | None = None,
|
||||||
limit: Optional[int] = None,
|
limit: int | None = None,
|
||||||
) -> Iterator[CheckpointTuple]:
|
) -> Iterator[CheckpointTuple]:
|
||||||
"""List checkpoints that match the given criteria.
|
"""List checkpoints that match the given criteria.
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ class BaseCheckpointSaver(Generic[V]):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def aget(self, config: RunnableConfig) -> Optional[Checkpoint]:
|
async def aget(self, config: RunnableConfig) -> Checkpoint | None:
|
||||||
"""Asynchronously fetch a checkpoint using the given configuration.
|
"""Asynchronously fetch a checkpoint using the given configuration.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -241,7 +241,7 @@ class BaseCheckpointSaver(Generic[V]):
|
|||||||
if value := await self.aget_tuple(config):
|
if value := await self.aget_tuple(config):
|
||||||
return value.checkpoint
|
return value.checkpoint
|
||||||
|
|
||||||
async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
||||||
"""Asynchronously fetch a checkpoint tuple using the given configuration.
|
"""Asynchronously fetch a checkpoint tuple using the given configuration.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -257,11 +257,11 @@ class BaseCheckpointSaver(Generic[V]):
|
|||||||
|
|
||||||
async def alist(
|
async def alist(
|
||||||
self,
|
self,
|
||||||
config: Optional[RunnableConfig],
|
config: RunnableConfig | None,
|
||||||
*,
|
*,
|
||||||
filter: Optional[dict[str, Any]] = None,
|
filter: dict[str, Any] | None = None,
|
||||||
before: Optional[RunnableConfig] = None,
|
before: RunnableConfig | None = None,
|
||||||
limit: Optional[int] = None,
|
limit: int | None = None,
|
||||||
) -> AsyncIterator[CheckpointTuple]:
|
) -> AsyncIterator[CheckpointTuple]:
|
||||||
"""Asynchronously list checkpoints that match the given criteria.
|
"""Asynchronously list checkpoints that match the given criteria.
|
||||||
|
|
||||||
@@ -334,7 +334,7 @@ class BaseCheckpointSaver(Generic[V]):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def get_next_version(self, current: Optional[V]) -> V:
|
def get_next_version(self, current: V | None) -> V:
|
||||||
"""Generate the next version ID for a channel.
|
"""Generate the next version ID for a channel.
|
||||||
|
|
||||||
Default is to use integer versions, incrementing by 1. If you override, you can use str/int/float versions,
|
Default is to use integer versions, incrementing by 1. If you override, you can use str/int/float versions,
|
||||||
@@ -361,7 +361,7 @@ class EmptyChannelError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_checkpoint_id(config: RunnableConfig) -> Optional[str]:
|
def get_checkpoint_id(config: RunnableConfig) -> str | None:
|
||||||
"""Get checkpoint ID in a backwards-compatible manner (fallback on thread_ts)."""
|
"""Get checkpoint ID in a backwards-compatible manner (fallback on thread_ts)."""
|
||||||
return config["configurable"].get(
|
return config["configurable"].get(
|
||||||
"checkpoint_id", config["configurable"].get("thread_ts")
|
"checkpoint_id", config["configurable"].get("thread_ts")
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ https://github.com/oittaa/uuid6-python/blob/main/src/uuid6/__init__.py#L95
|
|||||||
Bundled in to avoid install issues with uuid6 package
|
Bundled in to avoid install issues with uuid6 package
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
_last_v6_timestamp = None
|
_last_v6_timestamp = None
|
||||||
|
|
||||||
@@ -18,12 +19,12 @@ class UUID(uuid.UUID):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hex: Optional[str] = None,
|
hex: str | None = None,
|
||||||
bytes: Optional[bytes] = None,
|
bytes: bytes | None = None,
|
||||||
bytes_le: Optional[bytes] = None,
|
bytes_le: bytes | None = None,
|
||||||
fields: Optional[tuple[int, int, int, int, int, int]] = None,
|
fields: tuple[int, int, int, int, int, int] | None = None,
|
||||||
int: Optional[int] = None,
|
int: int | None = None,
|
||||||
version: Optional[int] = None,
|
version: int | None = None,
|
||||||
*,
|
*,
|
||||||
is_safe: uuid.SafeUUID = uuid.SafeUUID.unknown,
|
is_safe: uuid.SafeUUID = uuid.SafeUUID.unknown,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -75,7 +76,7 @@ def _subsec_decode(value: int) -> int:
|
|||||||
return -(-value * 10**6 // 2**20)
|
return -(-value * 10**6 // 2**20)
|
||||||
|
|
||||||
|
|
||||||
def uuid6(node: Optional[int] = None, clock_seq: Optional[int] = None) -> UUID:
|
def uuid6(node: int | None = None, clock_seq: int | None = None) -> UUID:
|
||||||
r"""UUID version 6 is a field-compatible version of UUIDv1, reordered for
|
r"""UUID version 6 is a field-compatible version of UUIDv1, reordered for
|
||||||
improved DB locality. It is expected that UUIDv6 will primarily be
|
improved DB locality. It is expected that UUIDv6 will primarily be
|
||||||
used in contexts where there are existing v1 UUIDs. Systems that do
|
used in contexts where there are existing v1 UUIDs. Systems that do
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
@@ -7,7 +9,7 @@ from collections import defaultdict
|
|||||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||||
from contextlib import AbstractAsyncContextManager, AbstractContextManager, ExitStack
|
from contextlib import AbstractAsyncContextManager, AbstractContextManager, ExitStack
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Any, Optional, Union
|
from typing import Any
|
||||||
|
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
|
|
||||||
@@ -63,9 +65,7 @@ class InMemorySaver(
|
|||||||
# thread ID -> checkpoint NS -> checkpoint ID -> checkpoint mapping
|
# thread ID -> checkpoint NS -> checkpoint ID -> checkpoint mapping
|
||||||
storage: defaultdict[
|
storage: defaultdict[
|
||||||
str,
|
str,
|
||||||
dict[
|
dict[str, dict[str, tuple[tuple[str, bytes], tuple[str, bytes], str | None]]],
|
||||||
str, dict[str, tuple[tuple[str, bytes], tuple[str, bytes], Optional[str]]]
|
|
||||||
],
|
|
||||||
]
|
]
|
||||||
# (thread ID, checkpoint NS, checkpoint ID) -> (task ID, write idx)
|
# (thread ID, checkpoint NS, checkpoint ID) -> (task ID, write idx)
|
||||||
writes: defaultdict[
|
writes: defaultdict[
|
||||||
@@ -74,7 +74,7 @@ class InMemorySaver(
|
|||||||
]
|
]
|
||||||
blobs: dict[
|
blobs: dict[
|
||||||
tuple[
|
tuple[
|
||||||
str, str, str, Union[str, int, float]
|
str, str, str, str | int | float
|
||||||
], # thread id, checkpoint ns, channel, version
|
], # thread id, checkpoint ns, channel, version
|
||||||
tuple[str, bytes],
|
tuple[str, bytes],
|
||||||
]
|
]
|
||||||
@@ -82,7 +82,7 @@ class InMemorySaver(
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
serde: Optional[SerializerProtocol] = None,
|
serde: SerializerProtocol | None = None,
|
||||||
factory: type[defaultdict] = defaultdict,
|
factory: type[defaultdict] = defaultdict,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(serde=serde)
|
super().__init__(serde=serde)
|
||||||
@@ -95,26 +95,26 @@ class InMemorySaver(
|
|||||||
self.stack.enter_context(self.writes) # type: ignore[arg-type]
|
self.stack.enter_context(self.writes) # type: ignore[arg-type]
|
||||||
self.stack.enter_context(self.blobs) # type: ignore[arg-type]
|
self.stack.enter_context(self.blobs) # type: ignore[arg-type]
|
||||||
|
|
||||||
def __enter__(self) -> "InMemorySaver":
|
def __enter__(self) -> InMemorySaver:
|
||||||
return self.stack.__enter__()
|
return self.stack.__enter__()
|
||||||
|
|
||||||
def __exit__(
|
def __exit__(
|
||||||
self,
|
self,
|
||||||
exc_type: Optional[type[BaseException]],
|
exc_type: type[BaseException] | None,
|
||||||
exc_value: Optional[BaseException],
|
exc_value: BaseException | None,
|
||||||
traceback: Optional[TracebackType],
|
traceback: TracebackType | None,
|
||||||
) -> Optional[bool]:
|
) -> bool | None:
|
||||||
return self.stack.__exit__(exc_type, exc_value, traceback)
|
return self.stack.__exit__(exc_type, exc_value, traceback)
|
||||||
|
|
||||||
async def __aenter__(self) -> "InMemorySaver":
|
async def __aenter__(self) -> InMemorySaver:
|
||||||
return self.stack.__enter__()
|
return self.stack.__enter__()
|
||||||
|
|
||||||
async def __aexit__(
|
async def __aexit__(
|
||||||
self,
|
self,
|
||||||
__exc_type: Optional[type[BaseException]],
|
__exc_type: type[BaseException] | None,
|
||||||
__exc_value: Optional[BaseException],
|
__exc_value: BaseException | None,
|
||||||
__traceback: Optional[TracebackType],
|
__traceback: TracebackType | None,
|
||||||
) -> Optional[bool]:
|
) -> bool | None:
|
||||||
return self.stack.__exit__(__exc_type, __exc_value, __traceback)
|
return self.stack.__exit__(__exc_type, __exc_value, __traceback)
|
||||||
|
|
||||||
def _load_blobs(
|
def _load_blobs(
|
||||||
@@ -129,7 +129,7 @@ class InMemorySaver(
|
|||||||
channel_values[k] = self.serde.loads_typed(vv)
|
channel_values[k] = self.serde.loads_typed(vv)
|
||||||
return channel_values
|
return channel_values
|
||||||
|
|
||||||
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
||||||
"""Get a checkpoint tuple from the in-memory storage.
|
"""Get a checkpoint tuple from the in-memory storage.
|
||||||
|
|
||||||
This method retrieves a checkpoint tuple from the in-memory storage based on the
|
This method retrieves a checkpoint tuple from the in-memory storage based on the
|
||||||
@@ -213,11 +213,11 @@ class InMemorySaver(
|
|||||||
|
|
||||||
def list(
|
def list(
|
||||||
self,
|
self,
|
||||||
config: Optional[RunnableConfig],
|
config: RunnableConfig | None,
|
||||||
*,
|
*,
|
||||||
filter: Optional[dict[str, Any]] = None,
|
filter: dict[str, Any] | None = None,
|
||||||
before: Optional[RunnableConfig] = None,
|
before: RunnableConfig | None = None,
|
||||||
limit: Optional[int] = None,
|
limit: int | None = None,
|
||||||
) -> Iterator[CheckpointTuple]:
|
) -> Iterator[CheckpointTuple]:
|
||||||
"""List checkpoints from the in-memory storage.
|
"""List checkpoints from the in-memory storage.
|
||||||
|
|
||||||
@@ -422,7 +422,7 @@ class InMemorySaver(
|
|||||||
if k[0] == thread_id:
|
if k[0] == thread_id:
|
||||||
del self.blobs[k]
|
del self.blobs[k]
|
||||||
|
|
||||||
async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
|
||||||
"""Asynchronous version of get_tuple.
|
"""Asynchronous version of get_tuple.
|
||||||
|
|
||||||
This method is an asynchronous wrapper around get_tuple that runs the synchronous
|
This method is an asynchronous wrapper around get_tuple that runs the synchronous
|
||||||
@@ -438,11 +438,11 @@ class InMemorySaver(
|
|||||||
|
|
||||||
async def alist(
|
async def alist(
|
||||||
self,
|
self,
|
||||||
config: Optional[RunnableConfig],
|
config: RunnableConfig | None,
|
||||||
*,
|
*,
|
||||||
filter: Optional[dict[str, Any]] = None,
|
filter: dict[str, Any] | None = None,
|
||||||
before: Optional[RunnableConfig] = None,
|
before: RunnableConfig | None = None,
|
||||||
limit: Optional[int] = None,
|
limit: int | None = None,
|
||||||
) -> AsyncIterator[CheckpointTuple]:
|
) -> AsyncIterator[CheckpointTuple]:
|
||||||
"""Asynchronous version of list.
|
"""Asynchronous version of list.
|
||||||
|
|
||||||
@@ -512,7 +512,7 @@ class InMemorySaver(
|
|||||||
"""
|
"""
|
||||||
return self.delete_thread(thread_id)
|
return self.delete_thread(thread_id)
|
||||||
|
|
||||||
def get_next_version(self, current: Optional[str]) -> str:
|
def get_next_version(self, current: str | None) -> str:
|
||||||
if current is None:
|
if current is None:
|
||||||
current_v = 0
|
current_v = 0
|
||||||
elif isinstance(current, int):
|
elif isinstance(current, int):
|
||||||
@@ -571,7 +571,7 @@ class PersistentDict(defaultdict):
|
|||||||
self.sync()
|
self.sync()
|
||||||
self.clear()
|
self.clear()
|
||||||
|
|
||||||
def __enter__(self) -> "PersistentDict":
|
def __enter__(self) -> PersistentDict:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, *exc_info: Any) -> None:
|
def __exit__(self, *exc_info: Any) -> None:
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import decimal
|
import decimal
|
||||||
import importlib
|
import importlib
|
||||||
@@ -18,7 +20,7 @@ from ipaddress import (
|
|||||||
IPv6Interface,
|
IPv6Interface,
|
||||||
IPv6Network,
|
IPv6Network,
|
||||||
)
|
)
|
||||||
from typing import Any, Callable, Optional, Union, cast
|
from typing import Any, Callable, cast
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@ class JsonPlusSerializer(SerializerProtocol):
|
|||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
pickle_fallback: bool = False,
|
pickle_fallback: bool = False,
|
||||||
__unpack_ext_hook__: Optional[Callable[[int, bytes], Any]] = None,
|
__unpack_ext_hook__: Callable[[int, bytes], Any] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.pickle_fallback = pickle_fallback
|
self.pickle_fallback = pickle_fallback
|
||||||
self._unpack_ext_hook = (
|
self._unpack_ext_hook = (
|
||||||
@@ -52,11 +54,11 @@ class JsonPlusSerializer(SerializerProtocol):
|
|||||||
|
|
||||||
def _encode_constructor_args(
|
def _encode_constructor_args(
|
||||||
self,
|
self,
|
||||||
constructor: Union[Callable, type[Any]],
|
constructor: Callable | type[Any],
|
||||||
*,
|
*,
|
||||||
method: Union[None, str, Sequence[Union[None, str]]] = None,
|
method: None | str | Sequence[None | str] = None,
|
||||||
args: Optional[Sequence[Any]] = None,
|
args: Sequence[Any] | None = None,
|
||||||
kwargs: Optional[dict[str, Any]] = None,
|
kwargs: dict[str, Any] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
out = {
|
out = {
|
||||||
"lc": 2,
|
"lc": 2,
|
||||||
@@ -71,7 +73,7 @@ class JsonPlusSerializer(SerializerProtocol):
|
|||||||
out["kwargs"] = kwargs
|
out["kwargs"] = kwargs
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def _default(self, obj: Any) -> Union[str, dict[str, Any]]:
|
def _default(self, obj: Any) -> str | dict[str, Any]:
|
||||||
if isinstance(obj, Serializable):
|
if isinstance(obj, Serializable):
|
||||||
return cast(dict[str, Any], obj.to_json())
|
return cast(dict[str, Any], obj.to_json())
|
||||||
elif hasattr(obj, "model_dump") and callable(obj.model_dump):
|
elif hasattr(obj, "model_dump") and callable(obj.model_dump):
|
||||||
@@ -251,7 +253,7 @@ EXT_PYDANTIC_V1 = 4
|
|||||||
EXT_PYDANTIC_V2 = 5
|
EXT_PYDANTIC_V2 = 5
|
||||||
|
|
||||||
|
|
||||||
def _msgpack_default(obj: Any) -> Union[str, ormsgpack.Ext]:
|
def _msgpack_default(obj: Any) -> str | ormsgpack.Ext:
|
||||||
if hasattr(obj, "model_dump") and callable(obj.model_dump): # pydantic v2
|
if hasattr(obj, "model_dump") and callable(obj.model_dump): # pydantic v2
|
||||||
return ormsgpack.Ext(
|
return ormsgpack.Ext(
|
||||||
EXT_PYDANTIC_V2,
|
EXT_PYDANTIC_V2,
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ Core types:
|
|||||||
- Op: Get/Put/Search/List operations
|
- Op: Get/Put/Search/List operations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -16,7 +18,6 @@ from typing import (
|
|||||||
Any,
|
Any,
|
||||||
Literal,
|
Literal,
|
||||||
NamedTuple,
|
NamedTuple,
|
||||||
Optional,
|
|
||||||
TypedDict,
|
TypedDict,
|
||||||
Union,
|
Union,
|
||||||
cast,
|
cast,
|
||||||
@@ -127,7 +128,7 @@ class SearchItem(Item):
|
|||||||
value: dict[str, Any],
|
value: dict[str, Any],
|
||||||
created_at: datetime,
|
created_at: datetime,
|
||||||
updated_at: datetime,
|
updated_at: datetime,
|
||||||
score: Optional[float] = None,
|
score: float | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a result item.
|
"""Initialize a result item.
|
||||||
|
|
||||||
@@ -242,7 +243,7 @@ class SearchOp(NamedTuple):
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
filter: Optional[dict[str, Any]] = None
|
filter: dict[str, Any] | None = None
|
||||||
"""Key-value pairs for filtering results based on exact matches or comparison operators.
|
"""Key-value pairs for filtering results based on exact matches or comparison operators.
|
||||||
|
|
||||||
The filter supports both exact matches and operator-based comparisons.
|
The filter supports both exact matches and operator-based comparisons.
|
||||||
@@ -284,7 +285,7 @@ class SearchOp(NamedTuple):
|
|||||||
offset: int = 0
|
offset: int = 0
|
||||||
"""Number of matching items to skip for pagination."""
|
"""Number of matching items to skip for pagination."""
|
||||||
|
|
||||||
query: Optional[str] = None
|
query: str | None = None
|
||||||
"""Natural language search query for semantic search capabilities.
|
"""Natural language search query for semantic search capabilities.
|
||||||
|
|
||||||
???+ example "Examples"
|
???+ example "Examples"
|
||||||
@@ -379,7 +380,7 @@ class ListNamespacesOp(NamedTuple):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
match_conditions: Optional[tuple[MatchCondition, ...]] = None
|
match_conditions: tuple[MatchCondition, ...] | None = None
|
||||||
"""Optional conditions for filtering namespaces.
|
"""Optional conditions for filtering namespaces.
|
||||||
|
|
||||||
???+ example "Examples"
|
???+ example "Examples"
|
||||||
@@ -397,7 +398,7 @@ class ListNamespacesOp(NamedTuple):
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
max_depth: Optional[int] = None
|
max_depth: int | None = None
|
||||||
"""Maximum depth of namespace hierarchy to return.
|
"""Maximum depth of namespace hierarchy to return.
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
@@ -452,7 +453,7 @@ class PutOp(NamedTuple):
|
|||||||
the full path would effectively be "documents/user123/report1"
|
the full path would effectively be "documents/user123/report1"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
value: Optional[dict[str, Any]]
|
value: dict[str, Any] | None
|
||||||
"""The data to store, or None to mark the item for deletion.
|
"""The data to store, or None to mark the item for deletion.
|
||||||
|
|
||||||
The value must be a dictionary with string keys and JSON-serializable values.
|
The value must be a dictionary with string keys and JSON-serializable values.
|
||||||
@@ -466,7 +467,7 @@ class PutOp(NamedTuple):
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
index: Optional[Union[Literal[False], list[str]]] = None # type: ignore[assignment]
|
index: Literal[False] | list[str] | None = None # type: ignore[assignment]
|
||||||
"""Controls how the item's fields are indexed for search operations.
|
"""Controls how the item's fields are indexed for search operations.
|
||||||
|
|
||||||
Indexing configuration determines how the item can be found through search:
|
Indexing configuration determines how the item can be found through search:
|
||||||
@@ -501,7 +502,7 @@ class PutOp(NamedTuple):
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
ttl: Optional[float] = None
|
ttl: float | None = None
|
||||||
"""Controls the TTL (time-to-live) for the item in minutes.
|
"""Controls the TTL (time-to-live) for the item in minutes.
|
||||||
|
|
||||||
If provided, and if the store you are using supports this feature, the item
|
If provided, and if the store you are using supports this feature, the item
|
||||||
@@ -530,14 +531,14 @@ class TTLConfig(TypedDict, total=False):
|
|||||||
This can be overridden per-operation by explicitly setting refresh_ttl.
|
This can be overridden per-operation by explicitly setting refresh_ttl.
|
||||||
Defaults to True if not configured.
|
Defaults to True if not configured.
|
||||||
"""
|
"""
|
||||||
default_ttl: Optional[float]
|
default_ttl: float | None
|
||||||
"""Default TTL (time-to-live) in minutes for new items.
|
"""Default TTL (time-to-live) in minutes for new items.
|
||||||
|
|
||||||
If provided, new items will expire after this many minutes after their last access.
|
If provided, new items will expire after this many minutes after their last access.
|
||||||
The expiration timer refreshes on both read and write operations.
|
The expiration timer refreshes on both read and write operations.
|
||||||
Defaults to None (no expiration).
|
Defaults to None (no expiration).
|
||||||
"""
|
"""
|
||||||
sweep_interval_minutes: Optional[int]
|
sweep_interval_minutes: int | None
|
||||||
"""Interval in minutes between TTL sweep operations.
|
"""Interval in minutes between TTL sweep operations.
|
||||||
|
|
||||||
If provided, the store will periodically delete expired items based on TTL.
|
If provided, the store will periodically delete expired items based on TTL.
|
||||||
@@ -565,7 +566,7 @@ class IndexConfig(TypedDict, total=False):
|
|||||||
- cohere:embed-multilingual-light-v3.0: 384
|
- cohere:embed-multilingual-light-v3.0: 384
|
||||||
"""
|
"""
|
||||||
|
|
||||||
embed: Union[Embeddings, EmbeddingsFunc, AEmbeddingsFunc, str]
|
embed: Embeddings | EmbeddingsFunc | AEmbeddingsFunc | str
|
||||||
"""Optional function to generate embeddings from text.
|
"""Optional function to generate embeddings from text.
|
||||||
|
|
||||||
Can be specified in three ways:
|
Can be specified in three ways:
|
||||||
@@ -633,7 +634,7 @@ class IndexConfig(TypedDict, total=False):
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fields: Optional[list[str]]
|
fields: list[str] | None
|
||||||
"""Fields to extract text from for embedding generation.
|
"""Fields to extract text from for embedding generation.
|
||||||
|
|
||||||
Controls which parts of stored items are embedded for semantic search. Follows JSON path syntax:
|
Controls which parts of stored items are embedded for semantic search. Follows JSON path syntax:
|
||||||
@@ -690,7 +691,7 @@ class BaseStore(ABC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
supports_ttl: bool = False
|
supports_ttl: bool = False
|
||||||
ttl_config: Optional[TTLConfig] = None
|
ttl_config: TTLConfig | None = None
|
||||||
|
|
||||||
__slots__ = ("__weakref__",)
|
__slots__ = ("__weakref__",)
|
||||||
|
|
||||||
@@ -723,8 +724,8 @@ class BaseStore(ABC):
|
|||||||
namespace: tuple[str, ...],
|
namespace: tuple[str, ...],
|
||||||
key: str,
|
key: str,
|
||||||
*,
|
*,
|
||||||
refresh_ttl: Optional[bool] = None,
|
refresh_ttl: bool | None = None,
|
||||||
) -> Optional[Item]:
|
) -> Item | None:
|
||||||
"""Retrieve a single item.
|
"""Retrieve a single item.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -746,11 +747,11 @@ class BaseStore(ABC):
|
|||||||
namespace_prefix: tuple[str, ...],
|
namespace_prefix: tuple[str, ...],
|
||||||
/,
|
/,
|
||||||
*,
|
*,
|
||||||
query: Optional[str] = None,
|
query: str | None = None,
|
||||||
filter: Optional[dict[str, Any]] = None,
|
filter: dict[str, Any] | None = None,
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
refresh_ttl: Optional[bool] = None,
|
refresh_ttl: bool | None = None,
|
||||||
) -> list[SearchItem]:
|
) -> list[SearchItem]:
|
||||||
"""Search for items within a namespace prefix.
|
"""Search for items within a namespace prefix.
|
||||||
|
|
||||||
@@ -817,9 +818,9 @@ class BaseStore(ABC):
|
|||||||
namespace: tuple[str, ...],
|
namespace: tuple[str, ...],
|
||||||
key: str,
|
key: str,
|
||||||
value: dict[str, Any],
|
value: dict[str, Any],
|
||||||
index: Optional[Union[Literal[False], list[str]]] = None,
|
index: Literal[False] | list[str] | None = None,
|
||||||
*,
|
*,
|
||||||
ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED,
|
ttl: float | None | NotProvided = NOT_PROVIDED,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Store or update an item in the store.
|
"""Store or update an item in the store.
|
||||||
|
|
||||||
@@ -901,9 +902,9 @@ class BaseStore(ABC):
|
|||||||
def list_namespaces(
|
def list_namespaces(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
prefix: Optional[NamespacePath] = None,
|
prefix: NamespacePath | None = None,
|
||||||
suffix: Optional[NamespacePath] = None,
|
suffix: NamespacePath | None = None,
|
||||||
max_depth: Optional[int] = None,
|
max_depth: int | None = None,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
) -> list[tuple[str, ...]]:
|
) -> list[tuple[str, ...]]:
|
||||||
@@ -956,8 +957,8 @@ class BaseStore(ABC):
|
|||||||
namespace: tuple[str, ...],
|
namespace: tuple[str, ...],
|
||||||
key: str,
|
key: str,
|
||||||
*,
|
*,
|
||||||
refresh_ttl: Optional[bool] = None,
|
refresh_ttl: bool | None = None,
|
||||||
) -> Optional[Item]:
|
) -> Item | None:
|
||||||
"""Asynchronously retrieve a single item.
|
"""Asynchronously retrieve a single item.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -984,11 +985,11 @@ class BaseStore(ABC):
|
|||||||
namespace_prefix: tuple[str, ...],
|
namespace_prefix: tuple[str, ...],
|
||||||
/,
|
/,
|
||||||
*,
|
*,
|
||||||
query: Optional[str] = None,
|
query: str | None = None,
|
||||||
filter: Optional[dict[str, Any]] = None,
|
filter: dict[str, Any] | None = None,
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
refresh_ttl: Optional[bool] = None,
|
refresh_ttl: bool | None = None,
|
||||||
) -> list[SearchItem]:
|
) -> list[SearchItem]:
|
||||||
"""Asynchronously search for items within a namespace prefix.
|
"""Asynchronously search for items within a namespace prefix.
|
||||||
|
|
||||||
@@ -1058,9 +1059,9 @@ class BaseStore(ABC):
|
|||||||
namespace: tuple[str, ...],
|
namespace: tuple[str, ...],
|
||||||
key: str,
|
key: str,
|
||||||
value: dict[str, Any],
|
value: dict[str, Any],
|
||||||
index: Optional[Union[Literal[False], list[str]]] = None,
|
index: Literal[False] | list[str] | None = None,
|
||||||
*,
|
*,
|
||||||
ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED,
|
ttl: float | None | NotProvided = NOT_PROVIDED,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Asynchronously store or update an item in the store.
|
"""Asynchronously store or update an item in the store.
|
||||||
|
|
||||||
@@ -1150,9 +1151,9 @@ class BaseStore(ABC):
|
|||||||
async def alist_namespaces(
|
async def alist_namespaces(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
prefix: Optional[NamespacePath] = None,
|
prefix: NamespacePath | None = None,
|
||||||
suffix: Optional[NamespacePath] = None,
|
suffix: NamespacePath | None = None,
|
||||||
max_depth: Optional[int] = None,
|
max_depth: int | None = None,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
) -> list[tuple[str, ...]]:
|
) -> list[tuple[str, ...]]:
|
||||||
@@ -1226,7 +1227,7 @@ def _validate_namespace(namespace: tuple[str, ...]) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _ensure_refresh(
|
def _ensure_refresh(
|
||||||
ttl_config: Optional[TTLConfig], refresh_ttl: Optional[bool] = None
|
ttl_config: TTLConfig | None, refresh_ttl: bool | None = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if refresh_ttl is not None:
|
if refresh_ttl is not None:
|
||||||
return refresh_ttl
|
return refresh_ttl
|
||||||
@@ -1236,9 +1237,9 @@ def _ensure_refresh(
|
|||||||
|
|
||||||
|
|
||||||
def _ensure_ttl(
|
def _ensure_ttl(
|
||||||
ttl_config: Optional[TTLConfig],
|
ttl_config: TTLConfig | None,
|
||||||
ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED,
|
ttl: float | None | NotProvided = NOT_PROVIDED,
|
||||||
) -> Optional[float]:
|
) -> float | None:
|
||||||
if ttl is NOT_PROVIDED:
|
if ttl is NOT_PROVIDED:
|
||||||
if ttl_config:
|
if ttl_config:
|
||||||
return ttl_config.get("default_ttl")
|
return ttl_config.get("default_ttl")
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
"""Utilities for batching operations in a background task."""
|
"""Utilities for batching operations in a background task."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
import weakref
|
import weakref
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from typing import Any, Callable, Literal, Optional, TypeVar, Union
|
from typing import Any, Callable, Literal, TypeVar
|
||||||
|
|
||||||
from langgraph.store.base import (
|
from langgraph.store.base import (
|
||||||
NOT_PROVIDED,
|
NOT_PROVIDED,
|
||||||
@@ -30,7 +32,7 @@ F = TypeVar("F", bound=Callable)
|
|||||||
|
|
||||||
def _check_loop(func: F) -> F:
|
def _check_loop(func: F) -> F:
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def wrapper(store: "AsyncBatchedBaseStore", *args: Any, **kwargs: Any) -> Any:
|
def wrapper(store: AsyncBatchedBaseStore, *args: Any, **kwargs: Any) -> Any:
|
||||||
method_name: str = func.__name__
|
method_name: str = func.__name__
|
||||||
try:
|
try:
|
||||||
current_loop = asyncio.get_running_loop()
|
current_loop = asyncio.get_running_loop()
|
||||||
@@ -75,8 +77,8 @@ class AsyncBatchedBaseStore(BaseStore):
|
|||||||
namespace: tuple[str, ...],
|
namespace: tuple[str, ...],
|
||||||
key: str,
|
key: str,
|
||||||
*,
|
*,
|
||||||
refresh_ttl: Optional[bool] = None,
|
refresh_ttl: bool | None = None,
|
||||||
) -> Optional[Item]:
|
) -> Item | None:
|
||||||
assert not self._task.done()
|
assert not self._task.done()
|
||||||
fut = self._loop.create_future()
|
fut = self._loop.create_future()
|
||||||
self._aqueue.put_nowait(
|
self._aqueue.put_nowait(
|
||||||
@@ -96,11 +98,11 @@ class AsyncBatchedBaseStore(BaseStore):
|
|||||||
namespace_prefix: tuple[str, ...],
|
namespace_prefix: tuple[str, ...],
|
||||||
/,
|
/,
|
||||||
*,
|
*,
|
||||||
query: Optional[str] = None,
|
query: str | None = None,
|
||||||
filter: Optional[dict[str, Any]] = None,
|
filter: dict[str, Any] | None = None,
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
refresh_ttl: Optional[bool] = None,
|
refresh_ttl: bool | None = None,
|
||||||
) -> list[SearchItem]:
|
) -> list[SearchItem]:
|
||||||
assert not self._task.done()
|
assert not self._task.done()
|
||||||
fut = self._loop.create_future()
|
fut = self._loop.create_future()
|
||||||
@@ -124,9 +126,9 @@ class AsyncBatchedBaseStore(BaseStore):
|
|||||||
namespace: tuple[str, ...],
|
namespace: tuple[str, ...],
|
||||||
key: str,
|
key: str,
|
||||||
value: dict[str, Any],
|
value: dict[str, Any],
|
||||||
index: Optional[Union[Literal[False], list[str]]] = None,
|
index: Literal[False] | list[str] | None = None,
|
||||||
*,
|
*,
|
||||||
ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED,
|
ttl: float | None | NotProvided = NOT_PROVIDED,
|
||||||
) -> None:
|
) -> None:
|
||||||
assert not self._task.done()
|
assert not self._task.done()
|
||||||
_validate_namespace(namespace)
|
_validate_namespace(namespace)
|
||||||
@@ -154,9 +156,9 @@ class AsyncBatchedBaseStore(BaseStore):
|
|||||||
async def alist_namespaces(
|
async def alist_namespaces(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
prefix: Optional[NamespacePath] = None,
|
prefix: NamespacePath | None = None,
|
||||||
suffix: Optional[NamespacePath] = None,
|
suffix: NamespacePath | None = None,
|
||||||
max_depth: Optional[int] = None,
|
max_depth: int | None = None,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
) -> list[tuple[str, ...]]:
|
) -> list[tuple[str, ...]]:
|
||||||
@@ -187,8 +189,8 @@ class AsyncBatchedBaseStore(BaseStore):
|
|||||||
namespace: tuple[str, ...],
|
namespace: tuple[str, ...],
|
||||||
key: str,
|
key: str,
|
||||||
*,
|
*,
|
||||||
refresh_ttl: Optional[bool] = None,
|
refresh_ttl: bool | None = None,
|
||||||
) -> Optional[Item]:
|
) -> Item | None:
|
||||||
return asyncio.run_coroutine_threadsafe(
|
return asyncio.run_coroutine_threadsafe(
|
||||||
self.aget(namespace, key=key, refresh_ttl=refresh_ttl), self._loop
|
self.aget(namespace, key=key, refresh_ttl=refresh_ttl), self._loop
|
||||||
).result()
|
).result()
|
||||||
@@ -199,11 +201,11 @@ class AsyncBatchedBaseStore(BaseStore):
|
|||||||
namespace_prefix: tuple[str, ...],
|
namespace_prefix: tuple[str, ...],
|
||||||
/,
|
/,
|
||||||
*,
|
*,
|
||||||
query: Optional[str] = None,
|
query: str | None = None,
|
||||||
filter: Optional[dict[str, Any]] = None,
|
filter: dict[str, Any] | None = None,
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
refresh_ttl: Optional[bool] = None,
|
refresh_ttl: bool | None = None,
|
||||||
) -> list[SearchItem]:
|
) -> list[SearchItem]:
|
||||||
return asyncio.run_coroutine_threadsafe(
|
return asyncio.run_coroutine_threadsafe(
|
||||||
self.asearch(
|
self.asearch(
|
||||||
@@ -223,9 +225,9 @@ class AsyncBatchedBaseStore(BaseStore):
|
|||||||
namespace: tuple[str, ...],
|
namespace: tuple[str, ...],
|
||||||
key: str,
|
key: str,
|
||||||
value: dict[str, Any],
|
value: dict[str, Any],
|
||||||
index: Optional[Union[Literal[False], list[str]]] = None,
|
index: Literal[False] | list[str] | None = None,
|
||||||
*,
|
*,
|
||||||
ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED,
|
ttl: float | None | NotProvided = NOT_PROVIDED,
|
||||||
) -> None:
|
) -> None:
|
||||||
_validate_namespace(namespace)
|
_validate_namespace(namespace)
|
||||||
asyncio.run_coroutine_threadsafe(
|
asyncio.run_coroutine_threadsafe(
|
||||||
@@ -253,9 +255,9 @@ class AsyncBatchedBaseStore(BaseStore):
|
|||||||
def list_namespaces(
|
def list_namespaces(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
prefix: Optional[NamespacePath] = None,
|
prefix: NamespacePath | None = None,
|
||||||
suffix: Optional[NamespacePath] = None,
|
suffix: NamespacePath | None = None,
|
||||||
max_depth: Optional[int] = None,
|
max_depth: int | None = None,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
) -> list[tuple[str, ...]]:
|
) -> list[tuple[str, ...]]:
|
||||||
@@ -271,7 +273,7 @@ class AsyncBatchedBaseStore(BaseStore):
|
|||||||
).result()
|
).result()
|
||||||
|
|
||||||
|
|
||||||
def _dedupe_ops(values: list[Op]) -> tuple[Optional[list[int]], list[Op]]:
|
def _dedupe_ops(values: list[Op]) -> tuple[list[int] | None, list[Op]]:
|
||||||
"""Dedupe operations while preserving order for results.
|
"""Dedupe operations while preserving order for results.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ with LangChain-compatible tools while maintaining support for both synchronous a
|
|||||||
asynchronous operations.
|
asynchronous operations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
import json
|
import json
|
||||||
from collections.abc import Awaitable, Sequence
|
from collections.abc import Awaitable, Sequence
|
||||||
from typing import Any, Callable, Optional, Union
|
from typing import Any, Callable
|
||||||
|
|
||||||
from langchain_core.embeddings import Embeddings
|
from langchain_core.embeddings import Embeddings
|
||||||
|
|
||||||
@@ -30,7 +32,7 @@ Similar to EmbeddingsFunc, but returns an awaitable that resolves to the embeddi
|
|||||||
|
|
||||||
|
|
||||||
def ensure_embeddings(
|
def ensure_embeddings(
|
||||||
embed: Union[Embeddings, EmbeddingsFunc, AEmbeddingsFunc, str, None],
|
embed: Embeddings | EmbeddingsFunc | AEmbeddingsFunc | str | None,
|
||||||
) -> Embeddings:
|
) -> Embeddings:
|
||||||
"""Ensure that an embedding function conforms to LangChain's Embeddings interface.
|
"""Ensure that an embedding function conforms to LangChain's Embeddings interface.
|
||||||
|
|
||||||
@@ -141,7 +143,7 @@ class EmbeddingsLambda(Embeddings):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
func: Union[EmbeddingsFunc, AEmbeddingsFunc],
|
func: EmbeddingsFunc | AEmbeddingsFunc,
|
||||||
) -> None:
|
) -> None:
|
||||||
if func is None:
|
if func is None:
|
||||||
raise ValueError("func must be provided")
|
raise ValueError("func must be provided")
|
||||||
@@ -221,7 +223,7 @@ class EmbeddingsLambda(Embeddings):
|
|||||||
return (await afunc([text]))[0]
|
return (await afunc([text]))[0]
|
||||||
|
|
||||||
|
|
||||||
def get_text_at_path(obj: Any, path: Union[str, list[str]]) -> list[str]:
|
def get_text_at_path(obj: Any, path: str | list[str]) -> list[str]:
|
||||||
"""Extract text from an object using a path expression or pre-tokenized path.
|
"""Extract text from an object using a path expression or pre-tokenized path.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -279,7 +281,7 @@ def get_text_at_path(obj: Any, path: Union[str, list[str]]) -> list[str]:
|
|||||||
for field in fields:
|
for field in fields:
|
||||||
nested_tokens = tokenize_path(field)
|
nested_tokens = tokenize_path(field)
|
||||||
if nested_tokens:
|
if nested_tokens:
|
||||||
current_obj: Optional[dict] = obj
|
current_obj: dict | None = obj
|
||||||
for nested_token in nested_tokens:
|
for nested_token in nested_tokens:
|
||||||
if (
|
if (
|
||||||
isinstance(current_obj, dict)
|
isinstance(current_obj, dict)
|
||||||
@@ -404,7 +406,7 @@ def _is_async_callable(
|
|||||||
|
|
||||||
|
|
||||||
@functools.lru_cache
|
@functools.lru_cache
|
||||||
def _get_init_embeddings() -> Optional[Callable[[str], Embeddings]]:
|
def _get_init_embeddings() -> Callable[[str], Embeddings] | None:
|
||||||
try:
|
try:
|
||||||
from langchain.embeddings import init_embeddings # type: ignore
|
from langchain.embeddings import init_embeddings # type: ignore
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ Tip:
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import concurrent.futures as cf
|
import concurrent.futures as cf
|
||||||
import functools
|
import functools
|
||||||
@@ -107,7 +109,7 @@ from collections import defaultdict
|
|||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from importlib import util
|
from importlib import util
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
from langchain_core.embeddings import Embeddings
|
from langchain_core.embeddings import Embeddings
|
||||||
|
|
||||||
@@ -178,7 +180,7 @@ class InMemoryStore(BaseStore):
|
|||||||
"embeddings",
|
"embeddings",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *, index: Optional[IndexConfig] = None) -> None:
|
def __init__(self, *, index: IndexConfig | None = None) -> None:
|
||||||
# Both _data and _vectors are wrapped in the In-memory API
|
# Both _data and _vectors are wrapped in the In-memory API
|
||||||
# Do not change their names
|
# Do not change their names
|
||||||
self._data: dict[tuple[str, ...], dict[str, Item]] = defaultdict(dict)
|
self._data: dict[tuple[str, ...], dict[str, Item]] = defaultdict(dict)
|
||||||
@@ -189,7 +191,7 @@ class InMemoryStore(BaseStore):
|
|||||||
self.index_config = index
|
self.index_config = index
|
||||||
if self.index_config:
|
if self.index_config:
|
||||||
self.index_config = self.index_config.copy()
|
self.index_config = self.index_config.copy()
|
||||||
self.embeddings: Optional[Embeddings] = ensure_embeddings(
|
self.embeddings: Embeddings | None = ensure_embeddings(
|
||||||
self.index_config.get("embed"),
|
self.index_config.get("embed"),
|
||||||
)
|
)
|
||||||
self.index_config["__tokenized_fields"] = [
|
self.index_config["__tokenized_fields"] = [
|
||||||
@@ -325,7 +327,7 @@ class InMemoryStore(BaseStore):
|
|||||||
)
|
)
|
||||||
# max pooling
|
# max pooling
|
||||||
seen: set[tuple[tuple[str, ...], str]] = set()
|
seen: set[tuple[tuple[str, ...], str]] = set()
|
||||||
kept: list[tuple[Optional[float], Item]] = []
|
kept: list[tuple[float | None, Item]] = []
|
||||||
for score, item in sorted_results:
|
for score, item in sorted_results:
|
||||||
key = (item.namespace, item.key)
|
key = (item.namespace, item.key)
|
||||||
if key in seen:
|
if key in seen:
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ lint.select = [
|
|||||||
"B", # flake8-bugbear
|
"B", # flake8-bugbear
|
||||||
"I", # isort
|
"I", # isort
|
||||||
]
|
]
|
||||||
lint.ignore = ["E501", "B008", "UP007", "UP006"]
|
lint.ignore = ["E501", "B008"]
|
||||||
|
|
||||||
[tool.pytest-watcher]
|
[tool.pytest-watcher]
|
||||||
now = true
|
now = true
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Optional, Protocol
|
from typing import Any, Protocol
|
||||||
|
|
||||||
from langgraph.checkpoint.base import Checkpoint, EmptyChannelError
|
from langgraph.checkpoint.base import Checkpoint, EmptyChannelError
|
||||||
from langgraph.checkpoint.base.id import uuid6
|
from langgraph.checkpoint.base.id import uuid6
|
||||||
|
|
||||||
|
|
||||||
class ChannelProtocol(Protocol):
|
class ChannelProtocol(Protocol):
|
||||||
def checkpoint(self) -> Optional[Any]: ...
|
def checkpoint(self) -> Any | None: ...
|
||||||
|
|
||||||
|
|
||||||
def empty_checkpoint() -> Checkpoint:
|
def empty_checkpoint() -> Checkpoint:
|
||||||
@@ -23,10 +25,10 @@ def empty_checkpoint() -> Checkpoint:
|
|||||||
|
|
||||||
def create_checkpoint(
|
def create_checkpoint(
|
||||||
checkpoint: Checkpoint,
|
checkpoint: Checkpoint,
|
||||||
channels: Optional[Mapping[str, ChannelProtocol]],
|
channels: Mapping[str, ChannelProtocol] | None,
|
||||||
step: int,
|
step: int,
|
||||||
*,
|
*,
|
||||||
id: Optional[str] = None,
|
id: str | None = None,
|
||||||
) -> Checkpoint:
|
) -> Checkpoint:
|
||||||
"""Create a checkpoint for the given channels."""
|
"""Create a checkpoint for the given channels."""
|
||||||
ts = datetime.now(timezone.utc).isoformat()
|
ts = datetime.now(timezone.utc).isoformat()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import re
|
||||||
import textwrap
|
import textwrap
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from typing import Any, Literal, NamedTuple, Optional, TypedDict, Union
|
from typing import Any, Literal, NamedTuple, Optional, TypedDict, Union
|
||||||
@@ -461,7 +462,7 @@ class Config(TypedDict, total=False):
|
|||||||
PIP_CLEANUP_LINES = """# -- Ensure user deps didn't inadvertently overwrite langgraph-api
|
PIP_CLEANUP_LINES = """# -- Ensure user deps didn't inadvertently overwrite langgraph-api
|
||||||
RUN mkdir -p /api/langgraph_api /api/langgraph_runtime /api/langgraph_license && \
|
RUN mkdir -p /api/langgraph_api /api/langgraph_runtime /api/langgraph_license && \
|
||||||
touch /api/langgraph_api/__init__.py /api/langgraph_runtime/__init__.py /api/langgraph_license/__init__.py
|
touch /api/langgraph_api/__init__.py /api/langgraph_runtime/__init__.py /api/langgraph_license/__init__.py
|
||||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir --no-deps -e /api
|
RUN PYTHONDONTWRITEBYTECODE=1 {install_cmd} --no-cache-dir --no-deps -e /api
|
||||||
# -- End of ensuring user deps didn't inadvertently overwrite langgraph-api --
|
# -- End of ensuring user deps didn't inadvertently overwrite langgraph-api --
|
||||||
# -- Removing pip from the final image ~<:===~~~ --
|
# -- Removing pip from the final image ~<:===~~~ --
|
||||||
RUN pip uninstall -y pip setuptools wheel && \
|
RUN pip uninstall -y pip setuptools wheel && \
|
||||||
@@ -470,6 +471,7 @@ RUN pip uninstall -y pip setuptools wheel && \
|
|||||||
# pip removal for wolfi
|
# pip removal for wolfi
|
||||||
RUN rm -rf /usr/lib/python*/site-packages/pip* /usr/lib/python*/site-packages/setuptools* /usr/lib/python*/site-packages/wheel* && \
|
RUN rm -rf /usr/lib/python*/site-packages/pip* /usr/lib/python*/site-packages/setuptools* /usr/lib/python*/site-packages/wheel* && \
|
||||||
find /usr/bin -name "pip*" -delete || true
|
find /usr/bin -name "pip*" -delete || true
|
||||||
|
{uv_removal}
|
||||||
# -- End of pip removal --"""
|
# -- End of pip removal --"""
|
||||||
|
|
||||||
|
|
||||||
@@ -1089,16 +1091,38 @@ def _get_node_pm_install_cmd(config_path: pathlib.Path, config: Config) -> str:
|
|||||||
return install_cmd
|
return install_cmd
|
||||||
|
|
||||||
|
|
||||||
|
semver_pattern = re.compile(r":(\d+(?:\.\d+)?(?:\.\d+)?)(?:-|$)")
|
||||||
|
|
||||||
|
|
||||||
|
def _image_supports_uv(base_image: str) -> bool:
|
||||||
|
if base_image == "langchain/langgraph-trial":
|
||||||
|
return False
|
||||||
|
match = semver_pattern.search(base_image)
|
||||||
|
if not match:
|
||||||
|
# Default image (langchain/langgraph-api) supports it.
|
||||||
|
return True
|
||||||
|
|
||||||
|
version_str = match.group(1)
|
||||||
|
version = tuple(map(int, version_str.split(".")))
|
||||||
|
min_uv = (0, 2, 47)
|
||||||
|
return version >= min_uv
|
||||||
|
|
||||||
|
|
||||||
def python_config_to_docker(
|
def python_config_to_docker(
|
||||||
config_path: pathlib.Path,
|
config_path: pathlib.Path,
|
||||||
config: Config,
|
config: Config,
|
||||||
base_image: str,
|
base_image: str,
|
||||||
) -> tuple[str, dict[str, str]]:
|
) -> tuple[str, dict[str, str]]:
|
||||||
"""Generate a Dockerfile from the configuration."""
|
"""Generate a Dockerfile from the configuration."""
|
||||||
|
if _image_supports_uv(base_image):
|
||||||
|
install_cmd = "uv pip install --system"
|
||||||
|
uv_removal = "RUN uv pip uninstall --system pip setuptools wheel && rm /usr/bin/uv /usr/bin/uvx"
|
||||||
|
else:
|
||||||
|
install_cmd = "pip install"
|
||||||
|
uv_removal = ""
|
||||||
|
|
||||||
# configure pip
|
# configure pip
|
||||||
pip_install = (
|
pip_install = f"PYTHONDONTWRITEBYTECODE=1 {install_cmd} --no-cache-dir -c /api/constraints.txt"
|
||||||
"PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt"
|
|
||||||
)
|
|
||||||
if config.get("pip_config_file"):
|
if config.get("pip_config_file"):
|
||||||
pip_install = f"PIP_CONFIG_FILE=/pipconfig.txt {pip_install}"
|
pip_install = f"PIP_CONFIG_FILE=/pipconfig.txt {pip_install}"
|
||||||
pip_config_file_str = (
|
pip_config_file_str = (
|
||||||
@@ -1151,7 +1175,10 @@ RUN set -ex && \\
|
|||||||
'name = "{fullpath.name}"' \\
|
'name = "{fullpath.name}"' \\
|
||||||
'version = "0.1"' \\
|
'version = "0.1"' \\
|
||||||
'[tool.setuptools.package-data]' \\
|
'[tool.setuptools.package-data]' \\
|
||||||
'"*" = ["**/*"]'; do \\
|
'"*" = ["**/*"]' \\
|
||||||
|
'[build-system]' \\
|
||||||
|
'requires = ["setuptools>=61"]' \\
|
||||||
|
'build-backend = "setuptools.build_meta"'; do \\
|
||||||
echo "$line" >> /deps/__outer_{fullpath.name}/pyproject.toml; \\
|
echo "$line" >> /deps/__outer_{fullpath.name}/pyproject.toml; \\
|
||||||
done
|
done
|
||||||
# -- End of non-package dependency {fullpath.name} --"""
|
# -- End of non-package dependency {fullpath.name} --"""
|
||||||
@@ -1240,7 +1267,8 @@ ADD {relpath} /deps/{name}
|
|||||||
"",
|
"",
|
||||||
js_inst_str,
|
js_inst_str,
|
||||||
"",
|
"",
|
||||||
PIP_CLEANUP_LINES, # Add pip cleanup after all installations are complete
|
# Add pip cleanup after all installations are complete
|
||||||
|
PIP_CLEANUP_LINES.format(install_cmd=install_cmd, uv_removal=uv_removal),
|
||||||
"",
|
"",
|
||||||
f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else "",
|
f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else "",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "langgraph-cli"
|
name = "langgraph-cli"
|
||||||
version = "0.2.12"
|
version = "0.3.1"
|
||||||
description = "CLI for interacting with LangGraph API"
|
description = "CLI for interacting with LangGraph API"
|
||||||
authors = []
|
authors = []
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ from langgraph_cli.config import PIP_CLEANUP_LINES, Config, validate_config
|
|||||||
from langgraph_cli.docker import DEFAULT_POSTGRES_URI, DockerCapabilities, Version
|
from langgraph_cli.docker import DEFAULT_POSTGRES_URI, DockerCapabilities, Version
|
||||||
from langgraph_cli.util import clean_empty_lines
|
from langgraph_cli.util import clean_empty_lines
|
||||||
|
|
||||||
|
FORMATTED_CLEANUP_LINES = PIP_CLEANUP_LINES.format(
|
||||||
|
install_cmd="uv pip install --system",
|
||||||
|
uv_removal="RUN uv pip uninstall --system pip setuptools wheel && rm /usr/bin/uv /usr/bin/uvx",
|
||||||
|
)
|
||||||
DEFAULT_DOCKER_CAPABILITIES = DockerCapabilities(
|
DEFAULT_DOCKER_CAPABILITIES = DockerCapabilities(
|
||||||
version_docker=Version(26, 1, 1),
|
version_docker=Version(26, 1, 1),
|
||||||
version_compose=Version(2, 27, 0),
|
version_compose=Version(2, 27, 0),
|
||||||
@@ -144,10 +148,10 @@ services:
|
|||||||
COPY --from=cli_1 . /deps/cli_1
|
COPY --from=cli_1 . /deps/cli_1
|
||||||
# -- End of local package ../../.. --
|
# -- End of local package ../../.. --
|
||||||
# -- Installing all local dependencies --
|
# -- Installing all local dependencies --
|
||||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||||
# -- End of local dependencies install --
|
# -- End of local dependencies install --
|
||||||
ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}'
|
ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}'
|
||||||
{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")}
|
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
|
||||||
WORKDIR /deps/cli
|
WORKDIR /deps/cli
|
||||||
|
|
||||||
develop:
|
develop:
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ from langgraph_cli.config import (
|
|||||||
)
|
)
|
||||||
from langgraph_cli.util import clean_empty_lines
|
from langgraph_cli.util import clean_empty_lines
|
||||||
|
|
||||||
|
FORMATTED_CLEANUP_LINES = PIP_CLEANUP_LINES.format(
|
||||||
|
install_cmd="uv pip install --system",
|
||||||
|
uv_removal="RUN uv pip uninstall --system pip setuptools wheel && rm /usr/bin/uv /usr/bin/uvx",
|
||||||
|
)
|
||||||
|
|
||||||
PATH_TO_CONFIG = pathlib.Path(__file__).parent / "test_config.json"
|
PATH_TO_CONFIG = pathlib.Path(__file__).parent / "test_config.json"
|
||||||
|
|
||||||
|
|
||||||
@@ -345,7 +350,7 @@ def test_config_to_docker_simple():
|
|||||||
FROM langchain/langgraph-api:3.11
|
FROM langchain/langgraph-api:3.11
|
||||||
# -- Installing local requirements --
|
# -- Installing local requirements --
|
||||||
COPY --from=__outer_requirements.txt requirements.txt /deps/__outer_graphs_reqs_a/graphs_reqs_a/requirements.txt
|
COPY --from=__outer_requirements.txt requirements.txt /deps/__outer_graphs_reqs_a/graphs_reqs_a/requirements.txt
|
||||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -r /deps/__outer_graphs_reqs_a/graphs_reqs_a/requirements.txt
|
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -r /deps/__outer_graphs_reqs_a/graphs_reqs_a/requirements.txt
|
||||||
# -- End of local requirements install --
|
# -- End of local requirements install --
|
||||||
# -- Adding local package ../../examples --
|
# -- Adding local package ../../examples --
|
||||||
COPY --from=examples . /deps/examples
|
COPY --from=examples . /deps/examples
|
||||||
@@ -357,7 +362,10 @@ RUN set -ex && \\
|
|||||||
'name = "unit_tests"' \\
|
'name = "unit_tests"' \\
|
||||||
'version = "0.1"' \\
|
'version = "0.1"' \\
|
||||||
'[tool.setuptools.package-data]' \\
|
'[tool.setuptools.package-data]' \\
|
||||||
'"*" = ["**/*"]'; do \\
|
'"*" = ["**/*"]' \\
|
||||||
|
'[build-system]' \\
|
||||||
|
'requires = ["setuptools>=61"]' \\
|
||||||
|
'build-backend = "setuptools.build_meta"'; do \\
|
||||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||||
done
|
done
|
||||||
# -- End of non-package dependency unit_tests --
|
# -- End of non-package dependency unit_tests --
|
||||||
@@ -368,16 +376,19 @@ RUN set -ex && \\
|
|||||||
'name = "graphs_reqs_a"' \\
|
'name = "graphs_reqs_a"' \\
|
||||||
'version = "0.1"' \\
|
'version = "0.1"' \\
|
||||||
'[tool.setuptools.package-data]' \\
|
'[tool.setuptools.package-data]' \\
|
||||||
'"*" = ["**/*"]'; do \\
|
'"*" = ["**/*"]' \\
|
||||||
|
'[build-system]' \\
|
||||||
|
'requires = ["setuptools>=61"]' \\
|
||||||
|
'build-backend = "setuptools.build_meta"'; do \\
|
||||||
echo "$line" >> /deps/__outer_graphs_reqs_a/pyproject.toml; \\
|
echo "$line" >> /deps/__outer_graphs_reqs_a/pyproject.toml; \\
|
||||||
done
|
done
|
||||||
# -- End of non-package dependency graphs_reqs_a --
|
# -- End of non-package dependency graphs_reqs_a --
|
||||||
# -- Installing all local dependencies --
|
# -- Installing all local dependencies --
|
||||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||||
# -- End of local dependencies install --
|
# -- End of local dependencies install --
|
||||||
ENV LANGGRAPH_HTTP='{{"app": "/deps/examples/my_app.py:app"}}'
|
ENV LANGGRAPH_HTTP='{{"app": "/deps/examples/my_app.py:app"}}'
|
||||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}'
|
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}'
|
||||||
{PIP_CLEANUP_LINES}
|
{FORMATTED_CLEANUP_LINES}
|
||||||
WORKDIR /deps/__outer_unit_tests/unit_tests\
|
WORKDIR /deps/__outer_unit_tests/unit_tests\
|
||||||
"""
|
"""
|
||||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||||
@@ -407,7 +418,10 @@ RUN set -ex && \\
|
|||||||
'name = "unit_tests"' \\
|
'name = "unit_tests"' \\
|
||||||
'version = "0.1"' \\
|
'version = "0.1"' \\
|
||||||
'[tool.setuptools.package-data]' \\
|
'[tool.setuptools.package-data]' \\
|
||||||
'"*" = ["**/*"]'; do \\
|
'"*" = ["**/*"]' \\
|
||||||
|
'[build-system]' \\
|
||||||
|
'requires = ["setuptools>=61"]' \\
|
||||||
|
'build-backend = "setuptools.build_meta"'; do \\
|
||||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||||
done
|
done
|
||||||
# -- End of non-package dependency unit_tests --
|
# -- End of non-package dependency unit_tests --
|
||||||
@@ -418,16 +432,19 @@ RUN set -ex && \\
|
|||||||
'name = "tests"' \\
|
'name = "tests"' \\
|
||||||
'version = "0.1"' \\
|
'version = "0.1"' \\
|
||||||
'[tool.setuptools.package-data]' \\
|
'[tool.setuptools.package-data]' \\
|
||||||
'"*" = ["**/*"]'; do \\
|
'"*" = ["**/*"]' \\
|
||||||
|
'[build-system]' \\
|
||||||
|
'requires = ["setuptools>=61"]' \\
|
||||||
|
'build-backend = "setuptools.build_meta"'; do \\
|
||||||
echo "$line" >> /deps/__outer_tests/pyproject.toml; \\
|
echo "$line" >> /deps/__outer_tests/pyproject.toml; \\
|
||||||
done
|
done
|
||||||
# -- End of non-package dependency tests --
|
# -- End of non-package dependency tests --
|
||||||
# -- Installing all local dependencies --
|
# -- Installing all local dependencies --
|
||||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||||
# -- End of local dependencies install --
|
# -- End of local dependencies install --
|
||||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||||
"""
|
"""
|
||||||
+ PIP_CLEANUP_LINES
|
+ FORMATTED_CLEANUP_LINES
|
||||||
+ """
|
+ """
|
||||||
WORKDIR /deps/__outer_unit_tests/unit_tests\
|
WORKDIR /deps/__outer_unit_tests/unit_tests\
|
||||||
"""
|
"""
|
||||||
@@ -462,16 +479,19 @@ RUN set -ex && \\
|
|||||||
'name = "unit_tests"' \\
|
'name = "unit_tests"' \\
|
||||||
'version = "0.1"' \\
|
'version = "0.1"' \\
|
||||||
'[tool.setuptools.package-data]' \\
|
'[tool.setuptools.package-data]' \\
|
||||||
'"*" = ["**/*"]'; do \\
|
'"*" = ["**/*"]' \\
|
||||||
|
'[build-system]' \\
|
||||||
|
'requires = ["setuptools>=61"]' \\
|
||||||
|
'build-backend = "setuptools.build_meta"'; do \\
|
||||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||||
done
|
done
|
||||||
# -- End of non-package dependency unit_tests --
|
# -- End of non-package dependency unit_tests --
|
||||||
# -- Installing all local dependencies --
|
# -- Installing all local dependencies --
|
||||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||||
# -- End of local dependencies install --
|
# -- End of local dependencies install --
|
||||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}'
|
||||||
"""
|
"""
|
||||||
+ PIP_CLEANUP_LINES
|
+ FORMATTED_CLEANUP_LINES
|
||||||
+ """
|
+ """
|
||||||
WORKDIR /deps/__outer_unit_tests/unit_tests\
|
WORKDIR /deps/__outer_unit_tests/unit_tests\
|
||||||
"""
|
"""
|
||||||
@@ -521,15 +541,18 @@ RUN set -ex && \\
|
|||||||
'name = "graphs"' \\
|
'name = "graphs"' \\
|
||||||
'version = "0.1"' \\
|
'version = "0.1"' \\
|
||||||
'[tool.setuptools.package-data]' \\
|
'[tool.setuptools.package-data]' \\
|
||||||
'"*" = ["**/*"]'; do \\
|
'"*" = ["**/*"]' \\
|
||||||
|
'[build-system]' \\
|
||||||
|
'requires = ["setuptools>=61"]' \\
|
||||||
|
'build-backend = "setuptools.build_meta"'; do \\
|
||||||
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \\
|
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \\
|
||||||
done
|
done
|
||||||
# -- End of non-package dependency graphs --
|
# -- End of non-package dependency graphs --
|
||||||
# -- Installing all local dependencies --
|
# -- Installing all local dependencies --
|
||||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||||
# -- End of local dependencies install --
|
# -- End of local dependencies install --
|
||||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_graphs/src/agent.py:graph"}}'
|
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_graphs/src/agent.py:graph"}}'
|
||||||
{PIP_CLEANUP_LINES}\
|
{FORMATTED_CLEANUP_LINES}\
|
||||||
"""
|
"""
|
||||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||||
assert additional_contexts == {}
|
assert additional_contexts == {}
|
||||||
@@ -562,11 +585,11 @@ dependencies = ["langchain"]"""
|
|||||||
ADD . /deps/unit_tests
|
ADD . /deps/unit_tests
|
||||||
# -- End of local package . --
|
# -- End of local package . --
|
||||||
# -- Installing all local dependencies --
|
# -- Installing all local dependencies --
|
||||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||||
# -- End of local dependencies install --
|
# -- End of local dependencies install --
|
||||||
ENV LANGSERVE_GRAPHS='{"agent": "/deps/unit_tests/graphs/agent.py:graph"}'
|
ENV LANGSERVE_GRAPHS='{"agent": "/deps/unit_tests/graphs/agent.py:graph"}'
|
||||||
"""
|
"""
|
||||||
+ PIP_CLEANUP_LINES
|
+ FORMATTED_CLEANUP_LINES
|
||||||
+ "\n"
|
+ "\n"
|
||||||
+ "WORKDIR /deps/unit_tests"
|
+ "WORKDIR /deps/unit_tests"
|
||||||
""
|
""
|
||||||
@@ -594,7 +617,7 @@ def test_config_to_docker_end_to_end():
|
|||||||
ARG meow
|
ARG meow
|
||||||
ARG foo
|
ARG foo
|
||||||
ADD pipconfig.txt /pipconfig.txt
|
ADD pipconfig.txt /pipconfig.txt
|
||||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt langchain langchain_openai
|
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt langchain langchain_openai
|
||||||
# -- Adding non-package dependency graphs --
|
# -- Adding non-package dependency graphs --
|
||||||
ADD ./graphs/ /deps/__outer_graphs/src
|
ADD ./graphs/ /deps/__outer_graphs/src
|
||||||
RUN set -ex && \\
|
RUN set -ex && \\
|
||||||
@@ -602,15 +625,18 @@ RUN set -ex && \\
|
|||||||
'name = "graphs"' \\
|
'name = "graphs"' \\
|
||||||
'version = "0.1"' \\
|
'version = "0.1"' \\
|
||||||
'[tool.setuptools.package-data]' \\
|
'[tool.setuptools.package-data]' \\
|
||||||
'"*" = ["**/*"]'; do \\
|
'"*" = ["**/*"]' \\
|
||||||
|
'[build-system]' \\
|
||||||
|
'requires = ["setuptools>=61"]' \\
|
||||||
|
'build-backend = "setuptools.build_meta"'; do \\
|
||||||
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \\
|
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \\
|
||||||
done
|
done
|
||||||
# -- End of non-package dependency graphs --
|
# -- End of non-package dependency graphs --
|
||||||
# -- Installing all local dependencies --
|
# -- Installing all local dependencies --
|
||||||
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||||
# -- End of local dependencies install --
|
# -- End of local dependencies install --
|
||||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_graphs/src/agent.py:graph"}}'
|
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_graphs/src/agent.py:graph"}}'
|
||||||
{PIP_CLEANUP_LINES}"""
|
{FORMATTED_CLEANUP_LINES}"""
|
||||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||||
assert additional_contexts == {}
|
assert additional_contexts == {}
|
||||||
|
|
||||||
@@ -705,12 +731,15 @@ RUN set -ex && \\
|
|||||||
'name = "unit_tests"' \\
|
'name = "unit_tests"' \\
|
||||||
'version = "0.1"' \\
|
'version = "0.1"' \\
|
||||||
'[tool.setuptools.package-data]' \\
|
'[tool.setuptools.package-data]' \\
|
||||||
'"*" = ["**/*"]'; do \\
|
'"*" = ["**/*"]' \\
|
||||||
|
'[build-system]' \\
|
||||||
|
'requires = ["setuptools>=61"]' \\
|
||||||
|
'build-backend = "setuptools.build_meta"'; do \\
|
||||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||||
done
|
done
|
||||||
# -- End of non-package dependency unit_tests --
|
# -- End of non-package dependency unit_tests --
|
||||||
# -- Installing all local dependencies --
|
# -- Installing all local dependencies --
|
||||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||||
# -- End of local dependencies install --
|
# -- End of local dependencies install --
|
||||||
ENV LANGGRAPH_UI='{{"agent": "./graphs/agent.ui.jsx"}}'
|
ENV LANGGRAPH_UI='{{"agent": "./graphs/agent.ui.jsx"}}'
|
||||||
ENV LANGGRAPH_UI_CONFIG='{{"shared": ["nuqs"]}}'
|
ENV LANGGRAPH_UI_CONFIG='{{"shared": ["nuqs"]}}'
|
||||||
@@ -719,7 +748,7 @@ ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:g
|
|||||||
ENV NODE_VERSION=20
|
ENV NODE_VERSION=20
|
||||||
RUN cd /deps/__outer_unit_tests/unit_tests && npm i && tsx /api/langgraph_api/js/build.mts
|
RUN cd /deps/__outer_unit_tests/unit_tests && npm i && tsx /api/langgraph_api/js/build.mts
|
||||||
# -- End of JS dependencies install --
|
# -- End of JS dependencies install --
|
||||||
{PIP_CLEANUP_LINES}
|
{FORMATTED_CLEANUP_LINES}
|
||||||
WORKDIR /deps/__outer_unit_tests/unit_tests"""
|
WORKDIR /deps/__outer_unit_tests/unit_tests"""
|
||||||
|
|
||||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||||
@@ -748,19 +777,22 @@ RUN set -ex && \\
|
|||||||
'name = "unit_tests"' \\
|
'name = "unit_tests"' \\
|
||||||
'version = "0.1"' \\
|
'version = "0.1"' \\
|
||||||
'[tool.setuptools.package-data]' \\
|
'[tool.setuptools.package-data]' \\
|
||||||
'"*" = ["**/*"]'; do \\
|
'"*" = ["**/*"]' \\
|
||||||
|
'[build-system]' \\
|
||||||
|
'requires = ["setuptools>=61"]' \\
|
||||||
|
'build-backend = "setuptools.build_meta"'; do \\
|
||||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||||
done
|
done
|
||||||
# -- End of non-package dependency unit_tests --
|
# -- End of non-package dependency unit_tests --
|
||||||
# -- Installing all local dependencies --
|
# -- Installing all local dependencies --
|
||||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||||
# -- End of local dependencies install --
|
# -- End of local dependencies install --
|
||||||
ENV LANGSERVE_GRAPHS='{{"python": "/deps/__outer_unit_tests/unit_tests/multiplatform/python.py:graph", "js": "/deps/__outer_unit_tests/unit_tests/multiplatform/js.mts:graph"}}'
|
ENV LANGSERVE_GRAPHS='{{"python": "/deps/__outer_unit_tests/unit_tests/multiplatform/python.py:graph", "js": "/deps/__outer_unit_tests/unit_tests/multiplatform/js.mts:graph"}}'
|
||||||
# -- Installing JS dependencies --
|
# -- Installing JS dependencies --
|
||||||
ENV NODE_VERSION=22
|
ENV NODE_VERSION=22
|
||||||
RUN cd /deps/__outer_unit_tests/unit_tests && npm i && tsx /api/langgraph_api/js/build.mts
|
RUN cd /deps/__outer_unit_tests/unit_tests && npm i && tsx /api/langgraph_api/js/build.mts
|
||||||
# -- End of JS dependencies install --
|
# -- End of JS dependencies install --
|
||||||
{PIP_CLEANUP_LINES}
|
{FORMATTED_CLEANUP_LINES}
|
||||||
WORKDIR /deps/__outer_unit_tests/unit_tests"""
|
WORKDIR /deps/__outer_unit_tests/unit_tests"""
|
||||||
|
|
||||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||||
@@ -770,7 +802,7 @@ WORKDIR /deps/__outer_unit_tests/unit_tests"""
|
|||||||
# config_to_compose
|
# config_to_compose
|
||||||
def test_config_to_compose_simple_config():
|
def test_config_to_compose_simple_config():
|
||||||
graphs = {"agent": "./agent.py:graph"}
|
graphs = {"agent": "./agent.py:graph"}
|
||||||
# Create a properly indented version of PIP_CLEANUP_LINES for compose files
|
# Create a properly indented version of FORMATTED_CLEANUP_LINES for compose files
|
||||||
expected_compose_stdin = f"""
|
expected_compose_stdin = f"""
|
||||||
pull_policy: build
|
pull_policy: build
|
||||||
build:
|
build:
|
||||||
@@ -784,15 +816,18 @@ def test_config_to_compose_simple_config():
|
|||||||
'name = "unit_tests"' \\
|
'name = "unit_tests"' \\
|
||||||
'version = "0.1"' \\
|
'version = "0.1"' \\
|
||||||
'[tool.setuptools.package-data]' \\
|
'[tool.setuptools.package-data]' \\
|
||||||
'"*" = ["**/*"]'; do \\
|
'"*" = ["**/*"]' \\
|
||||||
|
'[build-system]' \\
|
||||||
|
'requires = ["setuptools>=61"]' \\
|
||||||
|
'build-backend = "setuptools.build_meta"'; do \\
|
||||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||||
done
|
done
|
||||||
# -- End of non-package dependency unit_tests --
|
# -- End of non-package dependency unit_tests --
|
||||||
# -- Installing all local dependencies --
|
# -- Installing all local dependencies --
|
||||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||||
# -- End of local dependencies install --
|
# -- End of local dependencies install --
|
||||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}'
|
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}'
|
||||||
{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")}
|
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
|
||||||
WORKDIR /deps/__outer_unit_tests/unit_tests
|
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||||
"""
|
"""
|
||||||
actual_compose_stdin = config_to_compose(
|
actual_compose_stdin = config_to_compose(
|
||||||
@@ -822,15 +857,18 @@ def test_config_to_compose_env_vars():
|
|||||||
'name = "unit_tests"' \\
|
'name = "unit_tests"' \\
|
||||||
'version = "0.1"' \\
|
'version = "0.1"' \\
|
||||||
'[tool.setuptools.package-data]' \\
|
'[tool.setuptools.package-data]' \\
|
||||||
'"*" = ["**/*"]'; do \\
|
'"*" = ["**/*"]' \\
|
||||||
|
'[build-system]' \\
|
||||||
|
'requires = ["setuptools>=61"]' \\
|
||||||
|
'build-backend = "setuptools.build_meta"'; do \\
|
||||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||||
done
|
done
|
||||||
# -- End of non-package dependency unit_tests --
|
# -- End of non-package dependency unit_tests --
|
||||||
# -- Installing all local dependencies --
|
# -- Installing all local dependencies --
|
||||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||||
# -- End of local dependencies install --
|
# -- End of local dependencies install --
|
||||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}'
|
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}'
|
||||||
{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")}
|
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
|
||||||
WORKDIR /deps/__outer_unit_tests/unit_tests
|
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||||
"""
|
"""
|
||||||
openai_api_key = "key"
|
openai_api_key = "key"
|
||||||
@@ -864,15 +902,18 @@ def test_config_to_compose_env_file():
|
|||||||
'name = "unit_tests"' \\
|
'name = "unit_tests"' \\
|
||||||
'version = "0.1"' \\
|
'version = "0.1"' \\
|
||||||
'[tool.setuptools.package-data]' \\
|
'[tool.setuptools.package-data]' \\
|
||||||
'"*" = ["**/*"]'; do \\
|
'"*" = ["**/*"]' \\
|
||||||
|
'[build-system]' \\
|
||||||
|
'requires = ["setuptools>=61"]' \\
|
||||||
|
'build-backend = "setuptools.build_meta"'; do \\
|
||||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||||
done
|
done
|
||||||
# -- End of non-package dependency unit_tests --
|
# -- End of non-package dependency unit_tests --
|
||||||
# -- Installing all local dependencies --
|
# -- Installing all local dependencies --
|
||||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||||
# -- End of local dependencies install --
|
# -- End of local dependencies install --
|
||||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}'
|
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}'
|
||||||
{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")}
|
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
|
||||||
WORKDIR /deps/__outer_unit_tests/unit_tests
|
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||||
"""
|
"""
|
||||||
actual_compose_stdin = config_to_compose(
|
actual_compose_stdin = config_to_compose(
|
||||||
@@ -899,15 +940,18 @@ def test_config_to_compose_watch():
|
|||||||
'name = "unit_tests"' \\
|
'name = "unit_tests"' \\
|
||||||
'version = "0.1"' \\
|
'version = "0.1"' \\
|
||||||
'[tool.setuptools.package-data]' \\
|
'[tool.setuptools.package-data]' \\
|
||||||
'"*" = ["**/*"]'; do \\
|
'"*" = ["**/*"]' \\
|
||||||
|
'[build-system]' \\
|
||||||
|
'requires = ["setuptools>=61"]' \\
|
||||||
|
'build-backend = "setuptools.build_meta"'; do \\
|
||||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||||
done
|
done
|
||||||
# -- End of non-package dependency unit_tests --
|
# -- End of non-package dependency unit_tests --
|
||||||
# -- Installing all local dependencies --
|
# -- Installing all local dependencies --
|
||||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||||
# -- End of local dependencies install --
|
# -- End of local dependencies install --
|
||||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}'
|
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}'
|
||||||
{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")}
|
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
|
||||||
WORKDIR /deps/__outer_unit_tests/unit_tests
|
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||||
|
|
||||||
develop:
|
develop:
|
||||||
@@ -943,15 +987,18 @@ def test_config_to_compose_end_to_end():
|
|||||||
'name = "unit_tests"' \\
|
'name = "unit_tests"' \\
|
||||||
'version = "0.1"' \\
|
'version = "0.1"' \\
|
||||||
'[tool.setuptools.package-data]' \\
|
'[tool.setuptools.package-data]' \\
|
||||||
'"*" = ["**/*"]'; do \\
|
'"*" = ["**/*"]' \\
|
||||||
|
'[build-system]' \\
|
||||||
|
'requires = ["setuptools>=61"]' \\
|
||||||
|
'build-backend = "setuptools.build_meta"'; do \\
|
||||||
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\
|
||||||
done
|
done
|
||||||
# -- End of non-package dependency unit_tests --
|
# -- End of non-package dependency unit_tests --
|
||||||
# -- Installing all local dependencies --
|
# -- Installing all local dependencies --
|
||||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
RUN PYTHONDONTWRITEBYTECODE=1 uv pip install --system --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||||
# -- End of local dependencies install --
|
# -- End of local dependencies install --
|
||||||
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}'
|
ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}'
|
||||||
{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")}
|
{textwrap.indent(textwrap.dedent(FORMATTED_CLEANUP_LINES), " ")}
|
||||||
WORKDIR /deps/__outer_unit_tests/unit_tests
|
WORKDIR /deps/__outer_unit_tests/unit_tests
|
||||||
|
|
||||||
develop:
|
develop:
|
||||||
|
|||||||
Generated
+1
-1
@@ -501,7 +501,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "langgraph-cli"
|
name = "langgraph-cli"
|
||||||
version = "0.2.12"
|
version = "0.3.1"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def fanout_to_subgraph() -> StateGraph:
|
|||||||
return END if state["jokes"][0].endswith(" a" * 10) else "bump"
|
return END if state["jokes"][0].endswith(" a" * 10) else "bump"
|
||||||
|
|
||||||
# subgraph
|
# subgraph
|
||||||
subgraph = StateGraph(JokeState, input=JokeInput, output=JokeOutput)
|
subgraph = StateGraph(JokeState, input_schema=JokeInput, output_schema=JokeOutput)
|
||||||
subgraph.add_node("edit", edit)
|
subgraph.add_node("edit", edit)
|
||||||
subgraph.add_node("generate", generate)
|
subgraph.add_node("generate", generate)
|
||||||
subgraph.add_node("bump", bump)
|
subgraph.add_node("bump", bump)
|
||||||
@@ -87,7 +87,7 @@ def fanout_to_subgraph_sync() -> StateGraph:
|
|||||||
return END if state["jokes"][0].endswith(" a" * 10) else "bump"
|
return END if state["jokes"][0].endswith(" a" * 10) else "bump"
|
||||||
|
|
||||||
# subgraph
|
# subgraph
|
||||||
subgraph = StateGraph(JokeState, input=JokeInput, output=JokeOutput)
|
subgraph = StateGraph(JokeState, input_schema=JokeInput, output_schema=JokeOutput)
|
||||||
subgraph.add_node("edit", edit)
|
subgraph.add_node("edit", edit)
|
||||||
subgraph.add_node("generate", generate)
|
subgraph.add_node("generate", generate)
|
||||||
subgraph.add_node("bump", bump)
|
subgraph.add_node("bump", bump)
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""Private typing utilities for LangGraph."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import Field
|
||||||
|
from typing import Any, ClassVar, Protocol, Union
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing_extensions import TypeAlias, TypedDict
|
||||||
|
|
||||||
|
|
||||||
|
class TypedDictLikeV1(Protocol):
|
||||||
|
"""Protocol to represent types that behave like TypedDicts
|
||||||
|
|
||||||
|
Version 1: using `ClassVar` for keys."""
|
||||||
|
|
||||||
|
__required_keys__: ClassVar[frozenset[str]]
|
||||||
|
__optional_keys__: ClassVar[frozenset[str]]
|
||||||
|
|
||||||
|
|
||||||
|
class TypedDictLikeV2(Protocol):
|
||||||
|
"""Protocol to represent types that behave like TypedDicts
|
||||||
|
|
||||||
|
Version 2: not using `ClassVar` for keys."""
|
||||||
|
|
||||||
|
__required_keys__: frozenset[str]
|
||||||
|
__optional_keys__: frozenset[str]
|
||||||
|
|
||||||
|
|
||||||
|
class DataclassLike(Protocol):
|
||||||
|
"""Protocol to represent types that behave like dataclasses.
|
||||||
|
|
||||||
|
Inspired by the private _DataclassT from dataclasses that uses a similar protocol as a bound."""
|
||||||
|
|
||||||
|
__dataclass_fields__: ClassVar[dict[str, Field[Any]]]
|
||||||
|
|
||||||
|
|
||||||
|
StateLike: TypeAlias = Union[TypedDictLikeV1, TypedDictLikeV2, DataclassLike, BaseModel]
|
||||||
|
"""Type alias for state-like types.
|
||||||
|
|
||||||
|
It can either be a `TypedDict`, `dataclass`, or Pydantic `BaseModel`.
|
||||||
|
Note: we cannot use either `TypedDict` or `dataclass` directly due to limitations in type checking.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Unset:
|
||||||
|
"""A sentinel value to represent an unset type."""
|
||||||
|
|
||||||
|
|
||||||
|
UNSET: Unset = Unset()
|
||||||
|
|
||||||
|
|
||||||
|
class DeprecatedKwargs(TypedDict):
|
||||||
|
"""TypedDict to use for extra keyword arguments, enabling type checking warnings for deprecated arguments."""
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Iterator, Sequence
|
from collections.abc import Iterator, Sequence
|
||||||
from typing import Any, Generic, Union
|
from typing import Any, Generic, Union
|
||||||
|
|
||||||
@@ -8,7 +10,7 @@ from langgraph.constants import MISSING
|
|||||||
from langgraph.errors import EmptyChannelError
|
from langgraph.errors import EmptyChannelError
|
||||||
|
|
||||||
|
|
||||||
def flatten(values: Sequence[Union[Value, list[Value]]]) -> Iterator[Value]:
|
def flatten(values: Sequence[Value | list[Value]]) -> Iterator[Value]:
|
||||||
for value in values:
|
for value in values:
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
yield from value
|
yield from value
|
||||||
@@ -70,7 +72,7 @@ class Topic(
|
|||||||
empty.values = checkpoint
|
empty.values = checkpoint
|
||||||
return empty
|
return empty
|
||||||
|
|
||||||
def update(self, values: Sequence[Union[Value, list[Value]]]) -> bool:
|
def update(self, values: Sequence[Value | list[Value]]) -> bool:
|
||||||
updated = False
|
updated = False
|
||||||
if not self.accumulate:
|
if not self.accumulate:
|
||||||
updated = bool(self.values)
|
updated = bool(self.values)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import functools
|
import functools
|
||||||
@@ -9,9 +11,7 @@ from typing import (
|
|||||||
Any,
|
Any,
|
||||||
Callable,
|
Callable,
|
||||||
Generic,
|
Generic,
|
||||||
Optional,
|
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
|
||||||
get_args,
|
get_args,
|
||||||
get_origin,
|
get_origin,
|
||||||
overload,
|
overload,
|
||||||
@@ -19,6 +19,7 @@ from typing import (
|
|||||||
|
|
||||||
from typing_extensions import Unpack
|
from typing_extensions import Unpack
|
||||||
|
|
||||||
|
from langgraph._typing import UNSET, DeprecatedKwargs
|
||||||
from langgraph.cache.base import BaseCache
|
from langgraph.cache.base import BaseCache
|
||||||
from langgraph.channels.ephemeral_value import EphemeralValue
|
from langgraph.channels.ephemeral_value import EphemeralValue
|
||||||
from langgraph.channels.last_value import LastValue
|
from langgraph.channels.last_value import LastValue
|
||||||
@@ -37,7 +38,6 @@ from langgraph.pregel.read import PregelNode
|
|||||||
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
|
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
|
||||||
from langgraph.store.base import BaseStore
|
from langgraph.store.base import BaseStore
|
||||||
from langgraph.types import _DC_KWARGS, CachePolicy, RetryPolicy, StreamMode
|
from langgraph.types import _DC_KWARGS, CachePolicy, RetryPolicy, StreamMode
|
||||||
from langgraph.typing import DeprecatedKwargs
|
|
||||||
from langgraph.warnings import LangGraphDeprecatedSinceV10
|
from langgraph.warnings import LangGraphDeprecatedSinceV10
|
||||||
|
|
||||||
|
|
||||||
@@ -47,8 +47,8 @@ class TaskFunction(Generic[P, T]):
|
|||||||
func: Callable[P, T],
|
func: Callable[P, T],
|
||||||
*,
|
*,
|
||||||
retry_policy: Sequence[RetryPolicy],
|
retry_policy: Sequence[RetryPolicy],
|
||||||
cache_policy: Optional[CachePolicy[Callable[P, Union[str, bytes]]]] = None,
|
cache_policy: CachePolicy[Callable[P, str | bytes]] | None = None,
|
||||||
name: Optional[str] = None,
|
name: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if name is not None:
|
if name is not None:
|
||||||
if hasattr(func, "__func__"):
|
if hasattr(func, "__func__"):
|
||||||
@@ -91,36 +91,33 @@ class TaskFunction(Generic[P, T]):
|
|||||||
@overload
|
@overload
|
||||||
def task(
|
def task(
|
||||||
*,
|
*,
|
||||||
name: Optional[str] = None,
|
name: str | None = None,
|
||||||
retry_policy: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
|
retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
|
||||||
cache_policy: Optional[CachePolicy[Callable[P, Union[str, bytes]]]] = None,
|
cache_policy: CachePolicy[Callable[P, str | bytes]] | None = None,
|
||||||
**kwargs: Unpack[DeprecatedKwargs],
|
**kwargs: Unpack[DeprecatedKwargs],
|
||||||
) -> Callable[
|
) -> Callable[
|
||||||
[Union[Callable[P, Awaitable[T]], Callable[P, T]]],
|
[Callable[P, Awaitable[T]] | Callable[P, T]],
|
||||||
TaskFunction[P, T],
|
TaskFunction[P, T],
|
||||||
]: ...
|
]: ...
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def task(
|
def task(
|
||||||
__func_or_none__: Union[Callable[P, Awaitable[T]], Callable[P, T]],
|
__func_or_none__: Callable[P, Awaitable[T]] | Callable[P, T],
|
||||||
) -> TaskFunction[P, T]: ...
|
) -> TaskFunction[P, T]: ...
|
||||||
|
|
||||||
|
|
||||||
def task(
|
def task(
|
||||||
__func_or_none__: Optional[Union[Callable[P, Awaitable[T]], Callable[P, T]]] = None,
|
__func_or_none__: Callable[P, Awaitable[T]] | Callable[P, T] | None = None,
|
||||||
*,
|
*,
|
||||||
name: Optional[str] = None,
|
name: str | None = None,
|
||||||
retry_policy: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
|
retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
|
||||||
cache_policy: Optional[CachePolicy[Callable[P, Union[str, bytes]]]] = None,
|
cache_policy: CachePolicy[Callable[P, str | bytes]] | None = None,
|
||||||
**kwargs: Unpack[DeprecatedKwargs],
|
**kwargs: Unpack[DeprecatedKwargs],
|
||||||
) -> Union[
|
) -> (
|
||||||
Callable[
|
Callable[[Callable[P, Awaitable[T]] | Callable[P, T]], TaskFunction[P, T]]
|
||||||
[Union[Callable[P, Awaitable[T]], Callable[P, T]]],
|
| TaskFunction[P, T]
|
||||||
TaskFunction[P, T],
|
):
|
||||||
],
|
|
||||||
TaskFunction[P, T],
|
|
||||||
]:
|
|
||||||
"""Define a LangGraph task using the `task` decorator.
|
"""Define a LangGraph task using the `task` decorator.
|
||||||
|
|
||||||
!!! important "Requires python 3.11 or higher for async functions"
|
!!! important "Requires python 3.11 or higher for async functions"
|
||||||
@@ -179,7 +176,7 @@ def task(
|
|||||||
await add_one.ainvoke([1, 2, 3]) # Returns [2, 3, 4]
|
await add_one.ainvoke([1, 2, 3]) # Returns [2, 3, 4]
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
if (retry := kwargs.get("retry")) is not None:
|
if (retry := kwargs.get("retry", UNSET)) is not UNSET:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"`retry` is deprecated and will be removed. Please use `retry_policy` instead.",
|
"`retry` is deprecated and will be removed. Please use `retry_policy` instead.",
|
||||||
category=LangGraphDeprecatedSinceV10,
|
category=LangGraphDeprecatedSinceV10,
|
||||||
@@ -196,10 +193,8 @@ def task(
|
|||||||
)
|
)
|
||||||
|
|
||||||
def decorator(
|
def decorator(
|
||||||
func: Union[Callable[P, Awaitable[T]], Callable[P, T]],
|
func: Callable[P, Awaitable[T]] | Callable[P, T],
|
||||||
) -> Union[
|
) -> Callable[P, concurrent.futures.Future[T]] | Callable[P, asyncio.Future[T]]:
|
||||||
Callable[P, concurrent.futures.Future[T]], Callable[P, asyncio.Future[T]]
|
|
||||||
]:
|
|
||||||
return TaskFunction(
|
return TaskFunction(
|
||||||
func, retry_policy=retry_policies, cache_policy=cache_policy, name=name
|
func, retry_policy=retry_policies, cache_policy=cache_policy, name=name
|
||||||
)
|
)
|
||||||
@@ -376,16 +371,16 @@ class entrypoint:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
checkpointer: Optional[BaseCheckpointSaver] = None,
|
checkpointer: BaseCheckpointSaver | None = None,
|
||||||
store: Optional[BaseStore] = None,
|
store: BaseStore | None = None,
|
||||||
cache: Optional[BaseCache] = None,
|
cache: BaseCache | None = None,
|
||||||
config_schema: Optional[type[Any]] = None,
|
config_schema: type[Any] | None = None,
|
||||||
cache_policy: Optional[CachePolicy] = None,
|
cache_policy: CachePolicy | None = None,
|
||||||
retry_policy: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
|
retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
|
||||||
**kwargs: Unpack[DeprecatedKwargs],
|
**kwargs: Unpack[DeprecatedKwargs],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the entrypoint decorator."""
|
"""Initialize the entrypoint decorator."""
|
||||||
if (retry := kwargs.get("retry")) is not None:
|
if (retry := kwargs.get("retry", UNSET)) is not UNSET:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"`retry` is deprecated and will be removed. Please use `retry_policy` instead.",
|
"`retry` is deprecated and will be removed. Please use `retry_policy` instead.",
|
||||||
category=LangGraphDeprecatedSinceV10,
|
category=LangGraphDeprecatedSinceV10,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Awaitable, Hashable, Sequence
|
from collections.abc import Awaitable, Hashable, Sequence
|
||||||
from inspect import (
|
from inspect import (
|
||||||
isfunction,
|
isfunction,
|
||||||
@@ -11,7 +13,6 @@ from typing import (
|
|||||||
Callable,
|
Callable,
|
||||||
Literal,
|
Literal,
|
||||||
NamedTuple,
|
NamedTuple,
|
||||||
Optional,
|
|
||||||
Union,
|
Union,
|
||||||
cast,
|
cast,
|
||||||
get_args,
|
get_args,
|
||||||
@@ -40,21 +41,18 @@ Writer = Callable[
|
|||||||
|
|
||||||
|
|
||||||
def _get_branch_path_input_schema(
|
def _get_branch_path_input_schema(
|
||||||
path: Union[
|
path: Callable[..., Hashable | list[Hashable]]
|
||||||
Callable[..., Union[Hashable, list[Hashable]]],
|
| Callable[..., Awaitable[Hashable | list[Hashable]]]
|
||||||
Callable[..., Awaitable[Union[Hashable, list[Hashable]]]],
|
| Runnable[Any, Hashable | list[Hashable]],
|
||||||
Runnable[Any, Union[Hashable, list[Hashable]]],
|
) -> type[Any] | None:
|
||||||
],
|
|
||||||
) -> Optional[type[Any]]:
|
|
||||||
input = None
|
input = None
|
||||||
# detect input schema annotation in the branch callable
|
# detect input schema annotation in the branch callable
|
||||||
try:
|
try:
|
||||||
callable_: Optional[
|
callable_: (
|
||||||
Union[
|
Callable[..., Hashable | list[Hashable]]
|
||||||
Callable[..., Union[Hashable, list[Hashable]]],
|
| Callable[..., Awaitable[Hashable | list[Hashable]]]
|
||||||
Callable[..., Awaitable[Union[Hashable, list[Hashable]]]],
|
| None
|
||||||
]
|
) = None
|
||||||
] = None
|
|
||||||
if isinstance(path, (RunnableCallable, RunnableLambda)):
|
if isinstance(path, (RunnableCallable, RunnableLambda)):
|
||||||
if isfunction(path.func) or ismethod(path.func):
|
if isfunction(path.func) or ismethod(path.func):
|
||||||
callable_ = path.func
|
callable_ = path.func
|
||||||
@@ -85,19 +83,19 @@ def _get_branch_path_input_schema(
|
|||||||
|
|
||||||
|
|
||||||
class Branch(NamedTuple):
|
class Branch(NamedTuple):
|
||||||
path: Runnable[Any, Union[Hashable, list[Hashable]]]
|
path: Runnable[Any, Hashable | list[Hashable]]
|
||||||
ends: Optional[dict[Hashable, str]]
|
ends: dict[Hashable, str] | None
|
||||||
input_schema: Optional[type[Any]] = None
|
input_schema: type[Any] | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_path(
|
def from_path(
|
||||||
cls,
|
cls,
|
||||||
path: Runnable[Any, Union[Hashable, list[Hashable]]],
|
path: Runnable[Any, Hashable | list[Hashable]],
|
||||||
path_map: Optional[Union[dict[Hashable, str], list[str]]],
|
path_map: dict[Hashable, str] | list[str] | None,
|
||||||
infer_schema: bool = False,
|
infer_schema: bool = False,
|
||||||
) -> "Branch":
|
) -> Branch:
|
||||||
# coerce path_map to a dictionary
|
# coerce path_map to a dictionary
|
||||||
path_map_: Optional[dict[Hashable, str]] = None
|
path_map_: dict[Hashable, str] | None = None
|
||||||
try:
|
try:
|
||||||
if isinstance(path_map, dict):
|
if isinstance(path_map, dict):
|
||||||
path_map_ = path_map.copy()
|
path_map_ = path_map.copy()
|
||||||
@@ -105,7 +103,7 @@ class Branch(NamedTuple):
|
|||||||
path_map_ = {name: name for name in path_map}
|
path_map_ = {name: name for name in path_map}
|
||||||
else:
|
else:
|
||||||
# find func
|
# find func
|
||||||
func: Optional[Callable] = None
|
func: Callable | None = None
|
||||||
if isinstance(path, (RunnableCallable, RunnableLambda)):
|
if isinstance(path, (RunnableCallable, RunnableLambda)):
|
||||||
func = path.func or path.afunc
|
func = path.func or path.afunc
|
||||||
if func is not None:
|
if func is not None:
|
||||||
@@ -126,7 +124,7 @@ class Branch(NamedTuple):
|
|||||||
def run(
|
def run(
|
||||||
self,
|
self,
|
||||||
writer: Writer,
|
writer: Writer,
|
||||||
reader: Optional[Callable[[RunnableConfig], Any]] = None,
|
reader: Callable[[RunnableConfig], Any] | None = None,
|
||||||
) -> RunnableCallable:
|
) -> RunnableCallable:
|
||||||
return ChannelWrite.register_writer(
|
return ChannelWrite.register_writer(
|
||||||
RunnableCallable(
|
RunnableCallable(
|
||||||
@@ -153,7 +151,7 @@ class Branch(NamedTuple):
|
|||||||
input: Any,
|
input: Any,
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
*,
|
*,
|
||||||
reader: Optional[Callable[[RunnableConfig], Any]],
|
reader: Callable[[RunnableConfig], Any] | None,
|
||||||
writer: Writer,
|
writer: Writer,
|
||||||
) -> Runnable:
|
) -> Runnable:
|
||||||
if reader:
|
if reader:
|
||||||
@@ -176,7 +174,7 @@ class Branch(NamedTuple):
|
|||||||
input: Any,
|
input: Any,
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
*,
|
*,
|
||||||
reader: Optional[Callable[[RunnableConfig], Any]],
|
reader: Callable[[RunnableConfig], Any] | None,
|
||||||
writer: Writer,
|
writer: Writer,
|
||||||
) -> Runnable:
|
) -> Runnable:
|
||||||
if reader:
|
if reader:
|
||||||
@@ -200,11 +198,11 @@ class Branch(NamedTuple):
|
|||||||
input: Any,
|
input: Any,
|
||||||
result: Any,
|
result: Any,
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
) -> Union[Runnable, Any]:
|
) -> Runnable | Any:
|
||||||
if not isinstance(result, (list, tuple)):
|
if not isinstance(result, (list, tuple)):
|
||||||
result = [result]
|
result = [result]
|
||||||
if self.ends:
|
if self.ends:
|
||||||
destinations: Sequence[Union[Send, str]] = [
|
destinations: Sequence[Send | str] = [
|
||||||
r if isinstance(r, Send) else self.ends[r] for r in result
|
r if isinstance(r, Send) else self.ends[r] for r in result
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
import warnings
|
import warnings
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
@@ -7,7 +9,6 @@ from typing import (
|
|||||||
Any,
|
Any,
|
||||||
Callable,
|
Callable,
|
||||||
Literal,
|
Literal,
|
||||||
Optional,
|
|
||||||
Union,
|
Union,
|
||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
@@ -32,8 +33,8 @@ REMOVE_ALL_MESSAGES = "__remove_all__"
|
|||||||
|
|
||||||
def _add_messages_wrapper(func: Callable) -> Callable[[Messages, Messages], Messages]:
|
def _add_messages_wrapper(func: Callable) -> Callable[[Messages, Messages], Messages]:
|
||||||
def _add_messages(
|
def _add_messages(
|
||||||
left: Optional[Messages] = None, right: Optional[Messages] = None, **kwargs: Any
|
left: Messages | None = None, right: Messages | None = None, **kwargs: Any
|
||||||
) -> Union[Messages, Callable[[Messages, Messages], Messages]]:
|
) -> Messages | Callable[[Messages, Messages], Messages]:
|
||||||
if left is not None and right is not None:
|
if left is not None and right is not None:
|
||||||
return func(left, right, **kwargs)
|
return func(left, right, **kwargs)
|
||||||
elif left is not None or right is not None:
|
elif left is not None or right is not None:
|
||||||
@@ -54,7 +55,7 @@ def add_messages(
|
|||||||
left: Messages,
|
left: Messages,
|
||||||
right: Messages,
|
right: Messages,
|
||||||
*,
|
*,
|
||||||
format: Optional[Literal["langchain-openai"]] = None,
|
format: Literal["langchain-openai"] | None = None,
|
||||||
) -> Messages:
|
) -> Messages:
|
||||||
"""Merges two lists of messages, updating existing messages by ID.
|
"""Merges two lists of messages, updating existing messages by ID.
|
||||||
|
|
||||||
@@ -246,9 +247,9 @@ def _format_messages(messages: Sequence[BaseMessage]) -> list[BaseMessage]:
|
|||||||
|
|
||||||
|
|
||||||
def push_message(
|
def push_message(
|
||||||
message: Union[MessageLikeRepresentation, BaseMessageChunk],
|
message: MessageLikeRepresentation | BaseMessageChunk,
|
||||||
*,
|
*,
|
||||||
state_key: Optional[str] = "messages",
|
state_key: str | None = "messages",
|
||||||
) -> AnyMessage:
|
) -> AnyMessage:
|
||||||
"""Write a message manually to the `messages` / `messages-tuple` stream mode.
|
"""Write a message manually to the `messages` / `messages-tuple` stream mode.
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from typing import (
|
|||||||
Generic,
|
Generic,
|
||||||
Literal,
|
Literal,
|
||||||
NamedTuple,
|
NamedTuple,
|
||||||
Optional,
|
|
||||||
Protocol,
|
Protocol,
|
||||||
Union,
|
Union,
|
||||||
cast,
|
cast,
|
||||||
@@ -29,6 +28,7 @@ from langchain_core.runnables import Runnable, RunnableConfig
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing_extensions import Self, TypeAlias, Unpack
|
from typing_extensions import Self, TypeAlias, Unpack
|
||||||
|
|
||||||
|
from langgraph._typing import UNSET, DeprecatedKwargs
|
||||||
from langgraph.cache.base import BaseCache
|
from langgraph.cache.base import BaseCache
|
||||||
from langgraph.channels.base import BaseChannel
|
from langgraph.channels.base import BaseChannel
|
||||||
from langgraph.channels.binop import BinaryOperatorAggregate
|
from langgraph.channels.binop import BinaryOperatorAggregate
|
||||||
@@ -78,7 +78,7 @@ from langgraph.types import (
|
|||||||
Send,
|
Send,
|
||||||
StreamWriter,
|
StreamWriter,
|
||||||
)
|
)
|
||||||
from langgraph.typing import DeprecatedKwargs, InputT, StateT, StateT_contra, Unset
|
from langgraph.typing import InputT, OutputT, StateT, StateT_contra
|
||||||
from langgraph.utils.fields import (
|
from langgraph.utils.fields import (
|
||||||
get_cached_annotated_keys,
|
get_cached_annotated_keys,
|
||||||
get_field_default,
|
get_field_default,
|
||||||
@@ -91,7 +91,7 @@ from langgraph.warnings import LangGraphDeprecatedSinceV10
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _warn_invalid_state_schema(schema: Union[type[Any], Any]) -> None:
|
def _warn_invalid_state_schema(schema: type[Any] | Any) -> None:
|
||||||
if isinstance(schema, type):
|
if isinstance(schema, type):
|
||||||
return
|
return
|
||||||
if typing.get_args(schema):
|
if typing.get_args(schema):
|
||||||
@@ -174,15 +174,17 @@ class StateNodeSpec(NamedTuple):
|
|||||||
# TODO: rename this callable, also move away from NamedTuple so that we can use
|
# TODO: rename this callable, also move away from NamedTuple so that we can use
|
||||||
# a generic StateNode, so maybe a dataclass
|
# a generic StateNode, so maybe a dataclass
|
||||||
runnable: StateNode
|
runnable: StateNode
|
||||||
metadata: Optional[dict[str, Any]]
|
metadata: dict[str, Any] | None
|
||||||
|
# TODO: rename to input_schema, though we really just want to modify this structure to
|
||||||
|
# be a dataclass
|
||||||
input: type[Any]
|
input: type[Any]
|
||||||
retry_policy: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]]
|
retry_policy: RetryPolicy | Sequence[RetryPolicy] | None
|
||||||
cache_policy: Optional[CachePolicy]
|
cache_policy: CachePolicy | None
|
||||||
ends: Optional[Union[tuple[str, ...], dict[str, str]]] = EMPTY_SEQ
|
ends: tuple[str, ...] | dict[str, str] | None = EMPTY_SEQ
|
||||||
defer: bool = False
|
defer: bool = False
|
||||||
|
|
||||||
|
|
||||||
class StateGraph(Generic[StateT, InputT]):
|
class StateGraph(Generic[StateT, InputT, OutputT]):
|
||||||
"""A graph whose nodes communicate by reading and writing to a shared state.
|
"""A graph whose nodes communicate by reading and writing to a shared state.
|
||||||
The signature of each node is State -> Partial<State>.
|
The signature of each node is State -> Partial<State>.
|
||||||
|
|
||||||
@@ -239,35 +241,58 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
branches: defaultdict[str, dict[str, Branch]]
|
branches: defaultdict[str, dict[str, Branch]]
|
||||||
channels: dict[str, BaseChannel]
|
channels: dict[str, BaseChannel]
|
||||||
managed: dict[str, ManagedValueSpec]
|
managed: dict[str, ManagedValueSpec]
|
||||||
schemas: dict[type[Any], dict[str, Union[BaseChannel, ManagedValueSpec]]]
|
schemas: dict[type[Any], dict[str, BaseChannel | ManagedValueSpec]]
|
||||||
|
waiting_edges: set[tuple[tuple[str, ...], str]]
|
||||||
|
|
||||||
|
compiled: bool
|
||||||
|
state_schema: type[StateT]
|
||||||
|
input_schema: type[InputT]
|
||||||
|
output_schema: type[OutputT]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
state_schema: type[StateT],
|
state_schema: type[StateT],
|
||||||
config_schema: type[Any] | None = None,
|
config_schema: type[Any] | None = None,
|
||||||
*,
|
*,
|
||||||
input: type[InputT] | None = None,
|
input_schema: type[InputT] | None = None,
|
||||||
output: type[Any] | None = None,
|
output_schema: type[OutputT] | None = None,
|
||||||
|
**kwargs: Unpack[DeprecatedKwargs],
|
||||||
) -> None:
|
) -> None:
|
||||||
input = input or state_schema
|
if (input_ := kwargs.get("input", UNSET)) is not UNSET:
|
||||||
output = output or state_schema
|
warnings.warn(
|
||||||
|
"`input` is deprecated and will be removed. Please use `input_schema` instead.",
|
||||||
|
category=LangGraphDeprecatedSinceV10,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
if input_schema is None:
|
||||||
|
input_schema = cast(Union[type[InputT], None], input_)
|
||||||
|
|
||||||
|
if (output := kwargs.get("output", UNSET)) is not UNSET:
|
||||||
|
warnings.warn(
|
||||||
|
"`output` is deprecated and will be removed. Please use `output_schema` instead.",
|
||||||
|
category=LangGraphDeprecatedSinceV10,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
if output_schema is None:
|
||||||
|
output_schema = cast(Union[type[OutputT], None], output)
|
||||||
|
|
||||||
self.nodes = {}
|
self.nodes = {}
|
||||||
self.edges = set[tuple[str, str]]()
|
self.edges = set()
|
||||||
self.branches = defaultdict(dict)
|
self.branches = defaultdict(dict)
|
||||||
self.support_multiple_edges = False
|
|
||||||
self.compiled = False
|
|
||||||
self.schemas = {}
|
self.schemas = {}
|
||||||
self.channels = {}
|
self.channels = {}
|
||||||
self.managed = {}
|
self.managed = {}
|
||||||
self.schema = state_schema
|
self.compiled = False
|
||||||
self.input = input
|
self.waiting_edges = set()
|
||||||
self.output = output
|
|
||||||
self._add_schema(state_schema)
|
self.state_schema = state_schema
|
||||||
self._add_schema(input, allow_managed=False)
|
self.input_schema = cast(type[InputT], input_schema or state_schema)
|
||||||
self._add_schema(output, allow_managed=False)
|
self.output_schema = cast(type[OutputT], output_schema or state_schema)
|
||||||
self.config_schema = config_schema
|
self.config_schema = config_schema
|
||||||
self.waiting_edges: set[tuple[tuple[str, ...], str]] = set()
|
|
||||||
|
self._add_schema(self.state_schema)
|
||||||
|
self._add_schema(self.input_schema, allow_managed=False)
|
||||||
|
self._add_schema(self.output_schema, allow_managed=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _all_edges(self) -> set[tuple[str, str]]:
|
def _all_edges(self) -> set[tuple[str, str]]:
|
||||||
@@ -313,11 +338,11 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
node: StateNode[StateT],
|
node: StateNode[StateT],
|
||||||
*,
|
*,
|
||||||
defer: bool = False,
|
defer: bool = False,
|
||||||
metadata: Optional[dict[str, Any]] = None,
|
metadata: dict[str, Any] | None = None,
|
||||||
input: Optional[type[Any]] = None,
|
input_schema: type[Any] | None = None,
|
||||||
retry_policy: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
|
retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
|
||||||
cache_policy: Optional[CachePolicy] = None,
|
cache_policy: CachePolicy | None = None,
|
||||||
destinations: Optional[Union[dict[str, str], tuple[str, ...]]] = None,
|
destinations: dict[str, str] | tuple[str, ...] | None = None,
|
||||||
**kwargs: Unpack[DeprecatedKwargs],
|
**kwargs: Unpack[DeprecatedKwargs],
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""Add a new node to the state graph.
|
"""Add a new node to the state graph.
|
||||||
@@ -332,11 +357,11 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
action: StateNode[StateT],
|
action: StateNode[StateT],
|
||||||
*,
|
*,
|
||||||
defer: bool = False,
|
defer: bool = False,
|
||||||
metadata: Optional[dict[str, Any]] = None,
|
metadata: dict[str, Any] | None = None,
|
||||||
input: Optional[type[Any]] = None,
|
input_schema: type[Any] | None = None,
|
||||||
retry_policy: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
|
retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
|
||||||
cache_policy: Optional[CachePolicy] = None,
|
cache_policy: CachePolicy | None = None,
|
||||||
destinations: Optional[Union[dict[str, str], tuple[str, ...]]] = None,
|
destinations: dict[str, str] | tuple[str, ...] | None = None,
|
||||||
**kwargs: Unpack[DeprecatedKwargs],
|
**kwargs: Unpack[DeprecatedKwargs],
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""Add a new node to the state graph."""
|
"""Add a new node to the state graph."""
|
||||||
@@ -344,15 +369,15 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
|
|
||||||
def add_node(
|
def add_node(
|
||||||
self,
|
self,
|
||||||
node: Union[str, StateNode[StateT]],
|
node: str | StateNode[StateT],
|
||||||
action: Optional[StateNode[StateT]] = None,
|
action: StateNode[StateT] | None = None,
|
||||||
*,
|
*,
|
||||||
defer: bool = False,
|
defer: bool = False,
|
||||||
metadata: Optional[dict[str, Any]] = None,
|
metadata: dict[str, Any] | None = None,
|
||||||
input: Optional[type[Any]] = None,
|
input_schema: type[Any] | None = None,
|
||||||
retry_policy: Optional[Union[RetryPolicy, Sequence[RetryPolicy]]] = None,
|
retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None,
|
||||||
cache_policy: Optional[CachePolicy] = None,
|
cache_policy: CachePolicy | None = None,
|
||||||
destinations: Optional[Union[dict[str, str], tuple[str, ...]]] = None,
|
destinations: dict[str, str] | tuple[str, ...] | None = None,
|
||||||
**kwargs: Unpack[DeprecatedKwargs],
|
**kwargs: Unpack[DeprecatedKwargs],
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""Add a new node to the state graph.
|
"""Add a new node to the state graph.
|
||||||
@@ -364,7 +389,7 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
Will be used as the node function or runnable if `node` is a string (node name).
|
Will be used as the node function or runnable if `node` is a string (node name).
|
||||||
defer: Whether to defer the execution of the node until the run is about to end.
|
defer: Whether to defer the execution of the node until the run is about to end.
|
||||||
metadata: The metadata associated with the node. (default: None)
|
metadata: The metadata associated with the node. (default: None)
|
||||||
input: The input schema for the node. (default: the graph's input schema)
|
input_schema: The input schema for the node. (default: the graph's state schema)
|
||||||
retry_policy: The retry policy for the node. (default: None)
|
retry_policy: The retry policy for the node. (default: None)
|
||||||
If a sequence is provided, the first matching policy will be applied.
|
If a sequence is provided, the first matching policy will be applied.
|
||||||
cache_policy: The cache policy for the node. (default: None)
|
cache_policy: The cache policy for the node. (default: None)
|
||||||
@@ -376,12 +401,18 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
|
|
||||||
Example:
|
Example:
|
||||||
```python
|
```python
|
||||||
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
|
from langchain_core.runnables import RunnableConfig
|
||||||
from langgraph.graph import START, StateGraph
|
from langgraph.graph import START, StateGraph
|
||||||
|
|
||||||
def my_node(state, config):
|
class State(TypedDict):
|
||||||
|
x: int
|
||||||
|
|
||||||
|
def my_node(state: State, config: RunnableConfig) -> State:
|
||||||
return {"x": state["x"] + 1}
|
return {"x": state["x"] + 1}
|
||||||
|
|
||||||
builder = StateGraph(dict)
|
builder = StateGraph(State)
|
||||||
builder.add_node(my_node) # node name will be 'my_node'
|
builder.add_node(my_node) # node name will be 'my_node'
|
||||||
builder.add_edge(START, "my_node")
|
builder.add_edge(START, "my_node")
|
||||||
graph = builder.compile()
|
graph = builder.compile()
|
||||||
@@ -391,7 +422,7 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
|
|
||||||
Example: Customize the name:
|
Example: Customize the name:
|
||||||
```python
|
```python
|
||||||
builder = StateGraph(dict)
|
builder = StateGraph(State)
|
||||||
builder.add_node("my_fair_node", my_node)
|
builder.add_node("my_fair_node", my_node)
|
||||||
builder.add_edge(START, "my_fair_node")
|
builder.add_edge(START, "my_fair_node")
|
||||||
graph = builder.compile()
|
graph = builder.compile()
|
||||||
@@ -402,7 +433,7 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
Returns:
|
Returns:
|
||||||
Self: The instance of the state graph, allowing for method chaining.
|
Self: The instance of the state graph, allowing for method chaining.
|
||||||
"""
|
"""
|
||||||
if (retry := kwargs.get("retry")) is not None:
|
if (retry := kwargs.get("retry", UNSET)) is not UNSET:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"`retry` is deprecated and will be removed. Please use `retry_policy` instead.",
|
"`retry` is deprecated and will be removed. Please use `retry_policy` instead.",
|
||||||
category=LangGraphDeprecatedSinceV10,
|
category=LangGraphDeprecatedSinceV10,
|
||||||
@@ -410,6 +441,14 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
if retry_policy is None:
|
if retry_policy is None:
|
||||||
retry_policy = retry # type: ignore[assignment]
|
retry_policy = retry # type: ignore[assignment]
|
||||||
|
|
||||||
|
if (input_ := kwargs.get("input", UNSET)) is not UNSET:
|
||||||
|
warnings.warn(
|
||||||
|
"`input` is deprecated and will be removed. Please use `input_schema` instead.",
|
||||||
|
category=LangGraphDeprecatedSinceV10,
|
||||||
|
)
|
||||||
|
if input_schema is None:
|
||||||
|
input_schema = cast(Union[type[InputT], None], input_)
|
||||||
|
|
||||||
if not isinstance(node, str):
|
if not isinstance(node, str):
|
||||||
action = node
|
action = node
|
||||||
if isinstance(action, Runnable):
|
if isinstance(action, Runnable):
|
||||||
@@ -445,7 +484,7 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
f"'{character}' is a reserved character and is not allowed in the node names."
|
f"'{character}' is a reserved character and is not allowed in the node names."
|
||||||
)
|
)
|
||||||
|
|
||||||
ends: Union[tuple[str, ...], dict[str, str]] = EMPTY_SEQ
|
ends: tuple[str, ...] | dict[str, str] = EMPTY_SEQ
|
||||||
try:
|
try:
|
||||||
if (
|
if (
|
||||||
isfunction(action)
|
isfunction(action)
|
||||||
@@ -455,7 +494,7 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
hints := get_type_hints(getattr(action, "__call__"))
|
hints := get_type_hints(getattr(action, "__call__"))
|
||||||
or get_type_hints(action)
|
or get_type_hints(action)
|
||||||
):
|
):
|
||||||
if input is None:
|
if input_schema is None:
|
||||||
first_parameter_name = next(
|
first_parameter_name = next(
|
||||||
iter(
|
iter(
|
||||||
inspect.signature(
|
inspect.signature(
|
||||||
@@ -465,7 +504,7 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
)
|
)
|
||||||
if input_hint := hints.get(first_parameter_name):
|
if input_hint := hints.get(first_parameter_name):
|
||||||
if isinstance(input_hint, type) and get_type_hints(input_hint):
|
if isinstance(input_hint, type) and get_type_hints(input_hint):
|
||||||
input = input_hint
|
input_schema = input_hint
|
||||||
if rtn := hints.get("return"):
|
if rtn := hints.get("return"):
|
||||||
# Handle Union types
|
# Handle Union types
|
||||||
rtn_origin = get_origin(rtn)
|
rtn_origin = get_origin(rtn)
|
||||||
@@ -493,12 +532,12 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
if destinations is not None:
|
if destinations is not None:
|
||||||
ends = destinations
|
ends = destinations
|
||||||
|
|
||||||
if input is not None:
|
if input_schema is not None:
|
||||||
self._add_schema(input)
|
self._add_schema(input_schema)
|
||||||
self.nodes[node] = StateNodeSpec(
|
self.nodes[node] = StateNodeSpec(
|
||||||
coerce_to_runnable(action, name=node, trace=False), # type: ignore
|
coerce_to_runnable(action, name=node, trace=False), # type: ignore
|
||||||
metadata,
|
metadata,
|
||||||
input=input or self.schema,
|
input=input_schema or self.state_schema,
|
||||||
retry_policy=retry_policy,
|
retry_policy=retry_policy,
|
||||||
cache_policy=cache_policy,
|
cache_policy=cache_policy,
|
||||||
ends=ends,
|
ends=ends,
|
||||||
@@ -506,7 +545,7 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def add_edge(self, start_key: Union[str, list[str]], end_key: str) -> Self:
|
def add_edge(self, start_key: str | list[str], end_key: str) -> Self:
|
||||||
"""Add a directed edge from the start node (or list of start nodes) to the end node.
|
"""Add a directed edge from the start node (or list of start nodes) to the end node.
|
||||||
|
|
||||||
When a single start node is provided, the graph will wait for that node to complete
|
When a single start node is provided, the graph will wait for that node to complete
|
||||||
@@ -563,12 +602,10 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
def add_conditional_edges(
|
def add_conditional_edges(
|
||||||
self,
|
self,
|
||||||
source: str,
|
source: str,
|
||||||
path: Union[
|
path: Callable[..., Hashable | list[Hashable]]
|
||||||
Callable[..., Union[Hashable, list[Hashable]]],
|
| Callable[..., Awaitable[Hashable | list[Hashable]]]
|
||||||
Callable[..., Awaitable[Union[Hashable, list[Hashable]]]],
|
| Runnable[Any, Hashable | list[Hashable]],
|
||||||
Runnable[Any, Union[Hashable, list[Hashable]]],
|
path_map: dict[Hashable, str] | list[str] | None = None,
|
||||||
],
|
|
||||||
path_map: Optional[Union[dict[Hashable, str], list[str]]] = None,
|
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""Add a conditional edge from the starting node to any number of destination nodes.
|
"""Add a conditional edge from the starting node to any number of destination nodes.
|
||||||
|
|
||||||
@@ -610,7 +647,7 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
|
|
||||||
def add_sequence(
|
def add_sequence(
|
||||||
self,
|
self,
|
||||||
nodes: Sequence[Union[StateNode[StateT], tuple[str, StateNode[StateT]]]],
|
nodes: Sequence[StateNode[StateT] | tuple[str, StateNode[StateT]]],
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""Add a sequence of nodes that will be executed in the provided order.
|
"""Add a sequence of nodes that will be executed in the provided order.
|
||||||
|
|
||||||
@@ -629,7 +666,7 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
if len(nodes) < 1:
|
if len(nodes) < 1:
|
||||||
raise ValueError("Sequence requires at least one node.")
|
raise ValueError("Sequence requires at least one node.")
|
||||||
|
|
||||||
previous_name: Optional[str] = None
|
previous_name: str | None = None
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
if isinstance(node, tuple) and len(node) == 2:
|
if isinstance(node, tuple) and len(node) == 2:
|
||||||
name, node = node
|
name, node = node
|
||||||
@@ -665,12 +702,10 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
|
|
||||||
def set_conditional_entry_point(
|
def set_conditional_entry_point(
|
||||||
self,
|
self,
|
||||||
path: Union[
|
path: Callable[..., Hashable | list[Hashable]]
|
||||||
Callable[..., Union[Hashable, list[Hashable]]],
|
| Callable[..., Awaitable[Hashable | list[Hashable]]]
|
||||||
Callable[..., Awaitable[Union[Hashable, list[Hashable]]]],
|
| Runnable[Any, Hashable | list[Hashable]],
|
||||||
Runnable[Any, Union[Hashable, list[Hashable]]],
|
path_map: dict[Hashable, str] | list[str] | None = None,
|
||||||
],
|
|
||||||
path_map: Optional[Union[dict[Hashable, str], list[str]]] = None,
|
|
||||||
) -> Self:
|
) -> Self:
|
||||||
"""Sets a conditional entry point in the graph.
|
"""Sets a conditional entry point in the graph.
|
||||||
|
|
||||||
@@ -699,7 +734,7 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
"""
|
"""
|
||||||
return self.add_edge(key, END)
|
return self.add_edge(key, END)
|
||||||
|
|
||||||
def validate(self, interrupt: Optional[Sequence[str]] = None) -> Self:
|
def validate(self, interrupt: Sequence[str] | None = None) -> Self:
|
||||||
# assemble sources
|
# assemble sources
|
||||||
all_sources = {src for src, _ in self._all_edges}
|
all_sources = {src for src, _ in self._all_edges}
|
||||||
for start, branches in self.branches.items():
|
for start, branches in self.branches.items():
|
||||||
@@ -748,43 +783,17 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
self.compiled = True
|
self.compiled = True
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@overload
|
|
||||||
def compile(
|
|
||||||
self: StateGraph[StateT, Unset],
|
|
||||||
checkpointer: Checkpointer = None,
|
|
||||||
*,
|
|
||||||
cache: Optional[BaseCache] = None,
|
|
||||||
store: Optional[BaseStore] = None,
|
|
||||||
interrupt_before: Optional[Union[All, list[str]]] = None,
|
|
||||||
interrupt_after: Optional[Union[All, list[str]]] = None,
|
|
||||||
debug: bool = False,
|
|
||||||
name: Optional[str] = None,
|
|
||||||
) -> CompiledStateGraph[StateT, StateT]: ...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def compile(
|
|
||||||
self: StateGraph[StateT, InputT],
|
|
||||||
checkpointer: Checkpointer = None,
|
|
||||||
*,
|
|
||||||
cache: Optional[BaseCache] = None,
|
|
||||||
store: Optional[BaseStore] = None,
|
|
||||||
interrupt_before: Optional[Union[All, list[str]]] = None,
|
|
||||||
interrupt_after: Optional[Union[All, list[str]]] = None,
|
|
||||||
debug: bool = False,
|
|
||||||
name: Optional[str] = None,
|
|
||||||
) -> CompiledStateGraph[StateT, InputT]: ...
|
|
||||||
|
|
||||||
def compile(
|
def compile(
|
||||||
self,
|
self,
|
||||||
checkpointer: Checkpointer = None,
|
checkpointer: Checkpointer = None,
|
||||||
*,
|
*,
|
||||||
cache: Optional[BaseCache] = None,
|
cache: BaseCache | None = None,
|
||||||
store: Optional[BaseStore] = None,
|
store: BaseStore | None = None,
|
||||||
interrupt_before: Optional[Union[All, list[str]]] = None,
|
interrupt_before: All | list[str] | None = None,
|
||||||
interrupt_after: Optional[Union[All, list[str]]] = None,
|
interrupt_after: All | list[str] | None = None,
|
||||||
debug: bool = False,
|
debug: bool = False,
|
||||||
name: Optional[str] = None,
|
name: str | None = None,
|
||||||
) -> Union[CompiledStateGraph[StateT, StateT], CompiledStateGraph[StateT, InputT]]:
|
) -> CompiledStateGraph[StateT, InputT]:
|
||||||
"""Compiles the state graph into a `CompiledStateGraph` object.
|
"""Compiles the state graph into a `CompiledStateGraph` object.
|
||||||
|
|
||||||
The compiled graph implements the `Runnable` interface and can be invoked,
|
The compiled graph implements the `Runnable` interface and can be invoked,
|
||||||
@@ -820,11 +829,11 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
# prepare output channels
|
# prepare output channels
|
||||||
output_channels = (
|
output_channels = (
|
||||||
"__root__"
|
"__root__"
|
||||||
if len(self.schemas[self.output]) == 1
|
if len(self.schemas[self.output_schema]) == 1
|
||||||
and "__root__" in self.schemas[self.output]
|
and "__root__" in self.schemas[self.output_schema]
|
||||||
else [
|
else [
|
||||||
key
|
key
|
||||||
for key, val in self.schemas[self.output].items()
|
for key, val in self.schemas[self.output_schema].items()
|
||||||
if not is_managed_value(val)
|
if not is_managed_value(val)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -836,23 +845,22 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
ResolvedInputT: Union[type[InputT], type[StateT]] = self.input or self.schema
|
compiled = CompiledStateGraph[StateT, InputT, OutputT](
|
||||||
compiled = CompiledStateGraph[StateT, ResolvedInputT]( # type: ignore[valid-type]
|
|
||||||
builder=self,
|
builder=self,
|
||||||
schema_to_mapper={},
|
schema_to_mapper={},
|
||||||
config_type=self.config_schema,
|
config_type=self.config_schema,
|
||||||
input_model=(
|
input_model=(
|
||||||
self.input
|
self.input_schema
|
||||||
if len(self.channels) > 1
|
if len(self.channels) > 1
|
||||||
and isclass(self.input)
|
and isclass(self.input_schema)
|
||||||
and issubclass(self.input, BaseModel)
|
and issubclass(self.input_schema, BaseModel)
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
nodes={},
|
nodes={},
|
||||||
channels={
|
channels={
|
||||||
**self.channels,
|
**self.channels,
|
||||||
**self.managed,
|
**self.managed,
|
||||||
START: EphemeralValue(self.input),
|
START: EphemeralValue(self.input_schema),
|
||||||
},
|
},
|
||||||
input_channels=START,
|
input_channels=START,
|
||||||
stream_mode="updates",
|
stream_mode="updates",
|
||||||
@@ -885,46 +893,46 @@ class StateGraph(Generic[StateT, InputT]):
|
|||||||
return compiled.validate()
|
return compiled.validate()
|
||||||
|
|
||||||
|
|
||||||
class CompiledStateGraph(Pregel[InputT], Generic[StateT, InputT]):
|
class CompiledStateGraph(
|
||||||
builder: StateGraph[StateT, InputT]
|
Pregel[StateT, InputT, OutputT], Generic[StateT, InputT, OutputT]
|
||||||
schema_to_mapper: dict[type[Any], Optional[Callable[[Any], Any]]]
|
):
|
||||||
|
builder: StateGraph[StateT, InputT, OutputT]
|
||||||
|
schema_to_mapper: dict[type[Any], Callable[[Any], Any] | None]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
builder: StateGraph[StateT, InputT],
|
builder: StateGraph[StateT, InputT, OutputT],
|
||||||
schema_to_mapper: dict[type[Any], Optional[Callable[[Any], Any]]],
|
schema_to_mapper: dict[type[Any], Callable[[Any], Any] | None],
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.builder = builder
|
self.builder = builder
|
||||||
self.schema_to_mapper = schema_to_mapper
|
self.schema_to_mapper = schema_to_mapper
|
||||||
|
|
||||||
def get_input_schema(
|
def get_input_schema(self, config: RunnableConfig | None = None) -> type[BaseModel]:
|
||||||
self, config: Optional[RunnableConfig] = None
|
|
||||||
) -> type[BaseModel]:
|
|
||||||
return _get_schema(
|
return _get_schema(
|
||||||
typ=self.builder.input,
|
typ=self.builder.input_schema,
|
||||||
schemas=self.builder.schemas,
|
schemas=self.builder.schemas,
|
||||||
channels=self.builder.channels,
|
channels=self.builder.channels,
|
||||||
name=self.get_name("Input"),
|
name=self.get_name("Input"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_output_schema(
|
def get_output_schema(
|
||||||
self, config: Optional[RunnableConfig] = None
|
self, config: RunnableConfig | None = None
|
||||||
) -> type[BaseModel]:
|
) -> type[BaseModel]:
|
||||||
return _get_schema(
|
return _get_schema(
|
||||||
typ=self.builder.output,
|
typ=self.builder.output_schema,
|
||||||
schemas=self.builder.schemas,
|
schemas=self.builder.schemas,
|
||||||
channels=self.builder.channels,
|
channels=self.builder.channels,
|
||||||
name=self.get_name("Output"),
|
name=self.get_name("Output"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def attach_node(self, key: str, node: Optional[StateNodeSpec]) -> None:
|
def attach_node(self, key: str, node: StateNodeSpec | None) -> None:
|
||||||
if key == START:
|
if key == START:
|
||||||
output_keys = [
|
output_keys = [
|
||||||
k
|
k
|
||||||
for k, v in self.builder.schemas[self.builder.input].items()
|
for k, v in self.builder.schemas[self.builder.input_schema].items()
|
||||||
if not is_managed_value(v)
|
if not is_managed_value(v)
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
@@ -933,8 +941,8 @@ class CompiledStateGraph(Pregel[InputT], Generic[StateT, InputT]):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def _get_updates(
|
def _get_updates(
|
||||||
input: Union[None, dict, Any],
|
input: None | dict | Any,
|
||||||
) -> Optional[Sequence[tuple[str, Any]]]:
|
) -> Sequence[tuple[str, Any]] | None:
|
||||||
if input is None:
|
if input is None:
|
||||||
return None
|
return None
|
||||||
elif isinstance(input, dict):
|
elif isinstance(input, dict):
|
||||||
@@ -971,7 +979,7 @@ class CompiledStateGraph(Pregel[InputT], Generic[StateT, InputT]):
|
|||||||
raise InvalidUpdateError(msg)
|
raise InvalidUpdateError(msg)
|
||||||
|
|
||||||
# state updaters
|
# state updaters
|
||||||
write_entries: tuple[Union[ChannelWriteEntry, ChannelWriteTupleEntry], ...] = (
|
write_entries: tuple[ChannelWriteEntry | ChannelWriteTupleEntry, ...] = (
|
||||||
ChannelWriteTupleEntry(
|
ChannelWriteTupleEntry(
|
||||||
mapper=_get_root if output_keys == ["__root__"] else _get_updates
|
mapper=_get_root if output_keys == ["__root__"] else _get_updates
|
||||||
),
|
),
|
||||||
@@ -992,7 +1000,7 @@ class CompiledStateGraph(Pregel[InputT], Generic[StateT, InputT]):
|
|||||||
writers=[ChannelWrite(write_entries)],
|
writers=[ChannelWrite(write_entries)],
|
||||||
)
|
)
|
||||||
elif node is not None:
|
elif node is not None:
|
||||||
input_schema = node.input if node else self.builder.schema
|
input_schema = node.input if node else self.builder._state_schema
|
||||||
input_values = {k: k for k in self.builder.schemas[input_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
|
is_single_input = len(input_values) == 1 and "__root__" in input_values
|
||||||
if input_schema in self.schema_to_mapper:
|
if input_schema in self.schema_to_mapper:
|
||||||
@@ -1026,7 +1034,7 @@ class CompiledStateGraph(Pregel[InputT], Generic[StateT, InputT]):
|
|||||||
else:
|
else:
|
||||||
raise RuntimeError
|
raise RuntimeError
|
||||||
|
|
||||||
def attach_edge(self, starts: Union[str, Sequence[str]], end: str) -> None:
|
def attach_edge(self, starts: str | Sequence[str], end: str) -> None:
|
||||||
if isinstance(starts, str):
|
if isinstance(starts, str):
|
||||||
# subscribe to start channel
|
# subscribe to start channel
|
||||||
if end != END:
|
if end != END:
|
||||||
@@ -1056,8 +1064,8 @@ class CompiledStateGraph(Pregel[InputT], Generic[StateT, InputT]):
|
|||||||
self, start: str, name: str, branch: Branch, *, with_reader: bool = True
|
self, start: str, name: str, branch: Branch, *, with_reader: bool = True
|
||||||
) -> None:
|
) -> None:
|
||||||
def get_writes(
|
def get_writes(
|
||||||
packets: Sequence[Union[str, Send]], static: bool = False
|
packets: Sequence[str | Send], static: bool = False
|
||||||
) -> Sequence[Union[ChannelWriteEntry, Send]]:
|
) -> Sequence[ChannelWriteEntry | Send]:
|
||||||
writes = [
|
writes = [
|
||||||
(
|
(
|
||||||
ChannelWriteEntry(
|
ChannelWriteEntry(
|
||||||
@@ -1078,7 +1086,7 @@ class CompiledStateGraph(Pregel[InputT], Generic[StateT, InputT]):
|
|||||||
schema = branch.input_schema or (
|
schema = branch.input_schema or (
|
||||||
self.builder.nodes[start].input
|
self.builder.nodes[start].input
|
||||||
if start in self.builder.nodes
|
if start in self.builder.nodes
|
||||||
else self.builder.schema
|
else self.builder.state_schema
|
||||||
)
|
)
|
||||||
channels = list(self.builder.schemas[schema])
|
channels = list(self.builder.schemas[schema])
|
||||||
# get mapper
|
# get mapper
|
||||||
@@ -1088,7 +1096,7 @@ class CompiledStateGraph(Pregel[InputT], Generic[StateT, InputT]):
|
|||||||
mapper = _pick_mapper(channels, schema)
|
mapper = _pick_mapper(channels, schema)
|
||||||
self.schema_to_mapper[schema] = mapper
|
self.schema_to_mapper[schema] = mapper
|
||||||
# create reader
|
# create reader
|
||||||
reader: Optional[Callable[[RunnableConfig], Any]] = partial(
|
reader: Callable[[RunnableConfig], Any] | None = partial(
|
||||||
ChannelRead.do_read,
|
ChannelRead.do_read,
|
||||||
select=channels[0] if channels == ["__root__"] else channels,
|
select=channels[0] if channels == ["__root__"] else channels,
|
||||||
fresh=True,
|
fresh=True,
|
||||||
@@ -1208,7 +1216,7 @@ class CompiledStateGraph(Pregel[InputT], Generic[StateT, InputT]):
|
|||||||
|
|
||||||
def _pick_mapper(
|
def _pick_mapper(
|
||||||
state_keys: Sequence[str], schema: type[Any]
|
state_keys: Sequence[str], schema: type[Any]
|
||||||
) -> Optional[Callable[[Any], Any]]:
|
) -> Callable[[Any], Any] | None:
|
||||||
if state_keys == ["__root__"]:
|
if state_keys == ["__root__"]:
|
||||||
return None
|
return None
|
||||||
if isclass(schema) and issubclass(schema, dict):
|
if isclass(schema) and issubclass(schema, dict):
|
||||||
@@ -1249,8 +1257,8 @@ def _control_branch(value: Any) -> Sequence[tuple[str, Any]]:
|
|||||||
|
|
||||||
|
|
||||||
def _control_static(
|
def _control_static(
|
||||||
ends: Union[tuple[str, ...], dict[str, str]],
|
ends: tuple[str, ...] | dict[str, str],
|
||||||
) -> Sequence[tuple[str, Any, Optional[str]]]:
|
) -> Sequence[tuple[str, Any, str | None]]:
|
||||||
if isinstance(ends, dict):
|
if isinstance(ends, dict):
|
||||||
return [
|
return [
|
||||||
(k if k == END else CHANNEL_BRANCH_TO.format(k), None, label)
|
(k if k == END else CHANNEL_BRANCH_TO.format(k), None, label)
|
||||||
@@ -1262,7 +1270,7 @@ def _control_static(
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _get_root(input: Any) -> Optional[Sequence[tuple[str, Any]]]:
|
def _get_root(input: Any) -> Sequence[tuple[str, Any]] | None:
|
||||||
if isinstance(input, Command):
|
if isinstance(input, Command):
|
||||||
if input.graph == Command.PARENT:
|
if input.graph == Command.PARENT:
|
||||||
return ()
|
return ()
|
||||||
@@ -1317,12 +1325,12 @@ def _get_channel(
|
|||||||
@overload
|
@overload
|
||||||
def _get_channel(
|
def _get_channel(
|
||||||
name: str, annotation: Any, *, allow_managed: Literal[True] = True
|
name: str, annotation: Any, *, allow_managed: Literal[True] = True
|
||||||
) -> Union[BaseChannel, ManagedValueSpec]: ...
|
) -> BaseChannel | ManagedValueSpec: ...
|
||||||
|
|
||||||
|
|
||||||
def _get_channel(
|
def _get_channel(
|
||||||
name: str, annotation: Any, *, allow_managed: bool = True
|
name: str, annotation: Any, *, allow_managed: bool = True
|
||||||
) -> Union[BaseChannel, ManagedValueSpec]:
|
) -> BaseChannel | ManagedValueSpec:
|
||||||
if manager := _is_field_managed_value(name, annotation):
|
if manager := _is_field_managed_value(name, annotation):
|
||||||
if allow_managed:
|
if allow_managed:
|
||||||
return manager
|
return manager
|
||||||
@@ -1340,7 +1348,7 @@ def _get_channel(
|
|||||||
return fallback
|
return fallback
|
||||||
|
|
||||||
|
|
||||||
def _is_field_channel(typ: type[Any]) -> Optional[BaseChannel]:
|
def _is_field_channel(typ: type[Any]) -> BaseChannel | None:
|
||||||
if hasattr(typ, "__metadata__"):
|
if hasattr(typ, "__metadata__"):
|
||||||
meta = typ.__metadata__
|
meta = typ.__metadata__
|
||||||
if len(meta) >= 1 and isinstance(meta[-1], BaseChannel):
|
if len(meta) >= 1 and isinstance(meta[-1], BaseChannel):
|
||||||
@@ -1350,7 +1358,7 @@ def _is_field_channel(typ: type[Any]) -> Optional[BaseChannel]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _is_field_binop(typ: type[Any]) -> Optional[BinaryOperatorAggregate]:
|
def _is_field_binop(typ: type[Any]) -> BinaryOperatorAggregate | None:
|
||||||
if hasattr(typ, "__metadata__"):
|
if hasattr(typ, "__metadata__"):
|
||||||
meta = typ.__metadata__
|
meta = typ.__metadata__
|
||||||
if len(meta) >= 1 and callable(meta[-1]):
|
if len(meta) >= 1 and callable(meta[-1]):
|
||||||
@@ -1371,7 +1379,7 @@ def _is_field_binop(typ: type[Any]) -> Optional[BinaryOperatorAggregate]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _is_field_managed_value(name: str, typ: type[Any]) -> Optional[ManagedValueSpec]:
|
def _is_field_managed_value(name: str, typ: type[Any]) -> ManagedValueSpec | None:
|
||||||
if hasattr(typ, "__metadata__"):
|
if hasattr(typ, "__metadata__"):
|
||||||
meta = typ.__metadata__
|
meta = typ.__metadata__
|
||||||
if len(meta) >= 1:
|
if len(meta) >= 1:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
from typing import Any, Literal, Optional, Union, cast
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Literal, Union, cast
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from langchain_core.messages import AnyMessage
|
from langchain_core.messages import AnyMessage
|
||||||
@@ -51,10 +53,10 @@ def push_ui_message(
|
|||||||
name: str,
|
name: str,
|
||||||
props: dict[str, Any],
|
props: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
id: Optional[str] = None,
|
id: str | None = None,
|
||||||
metadata: Optional[dict[str, Any]] = None,
|
metadata: dict[str, Any] | None = None,
|
||||||
message: Optional[AnyMessage] = None,
|
message: AnyMessage | None = None,
|
||||||
state_key: Optional[str] = "ui",
|
state_key: str | None = "ui",
|
||||||
merge: bool = False,
|
merge: bool = False,
|
||||||
) -> UIMessage:
|
) -> UIMessage:
|
||||||
"""Push a new UI message to update the UI state.
|
"""Push a new UI message to update the UI state.
|
||||||
@@ -149,8 +151,8 @@ def delete_ui_message(id: str, *, state_key: str = "ui") -> RemoveUIMessage:
|
|||||||
|
|
||||||
|
|
||||||
def ui_message_reducer(
|
def ui_message_reducer(
|
||||||
left: Union[list[AnyUIMessage], AnyUIMessage],
|
left: list[AnyUIMessage] | AnyUIMessage,
|
||||||
right: Union[list[AnyUIMessage], AnyUIMessage],
|
right: list[AnyUIMessage] | AnyUIMessage,
|
||||||
) -> list[AnyUIMessage]:
|
) -> list[AnyUIMessage]:
|
||||||
"""Merge two lists of UI messages, supporting removing UI messages.
|
"""Merge two lists of UI messages, supporting removing UI messages.
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ from langgraph.types import (
|
|||||||
StreamChunk,
|
StreamChunk,
|
||||||
StreamMode,
|
StreamMode,
|
||||||
)
|
)
|
||||||
from langgraph.typing import InputT
|
from langgraph.typing import InputT, OutputT, StateT
|
||||||
from langgraph.utils.config import (
|
from langgraph.utils.config import (
|
||||||
ensure_config,
|
ensure_config,
|
||||||
merge_configs,
|
merge_configs,
|
||||||
@@ -298,7 +298,7 @@ class NodeBuilder:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Pregel(PregelProtocol[InputT], Generic[InputT]):
|
class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, OutputT]):
|
||||||
"""Pregel manages the runtime behavior for LangGraph applications.
|
"""Pregel manages the runtime behavior for LangGraph applications.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
@@ -2766,8 +2766,8 @@ class Pregel(PregelProtocol[InputT], Generic[InputT]):
|
|||||||
"""
|
"""
|
||||||
output_keys = output_keys if output_keys is not None else self.output_channels
|
output_keys = output_keys if output_keys is not None else self.output_channels
|
||||||
|
|
||||||
latest: Union[dict[str, Any], Any] = None
|
latest: dict[str, Any] | Any = None
|
||||||
chunks: list[Union[dict[str, Any], Any]] = []
|
chunks: list[dict[str, Any] | Any] = []
|
||||||
interrupts: list[Interrupt] = []
|
interrupts: list[Interrupt] = []
|
||||||
|
|
||||||
for chunk in self.stream(
|
for chunk in self.stream(
|
||||||
@@ -2833,8 +2833,8 @@ class Pregel(PregelProtocol[InputT], Generic[InputT]):
|
|||||||
|
|
||||||
output_keys = output_keys if output_keys is not None else self.output_channels
|
output_keys = output_keys if output_keys is not None else self.output_channels
|
||||||
|
|
||||||
latest: Union[dict[str, Any], Any] = None
|
latest: dict[str, Any] | Any = None
|
||||||
chunks: list[Union[dict[str, Any], Any]] = []
|
chunks: list[dict[str, Any] | Any] = []
|
||||||
interrupts: list[Interrupt] = []
|
interrupts: list[Interrupt] = []
|
||||||
|
|
||||||
async for chunk in self.astream(
|
async for chunk in self.astream(
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import binascii
|
import binascii
|
||||||
import itertools
|
import itertools
|
||||||
import sys
|
import sys
|
||||||
@@ -14,7 +16,6 @@ from typing import (
|
|||||||
NamedTuple,
|
NamedTuple,
|
||||||
Optional,
|
Optional,
|
||||||
Protocol,
|
Protocol,
|
||||||
Union,
|
|
||||||
cast,
|
cast,
|
||||||
overload,
|
overload,
|
||||||
)
|
)
|
||||||
@@ -91,7 +92,7 @@ class WritesProtocol(Protocol):
|
|||||||
Implemented by PregelTaskWrites and PregelExecutableTask."""
|
Implemented by PregelTaskWrites and PregelExecutableTask."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path(self) -> tuple[Union[str, int, tuple], ...]: ...
|
def path(self) -> tuple[str | int | tuple, ...]: ...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str: ...
|
def name(self) -> str: ...
|
||||||
@@ -107,7 +108,7 @@ class PregelTaskWrites(NamedTuple):
|
|||||||
"""Simplest implementation of WritesProtocol, for usage with writes that
|
"""Simplest implementation of WritesProtocol, for usage with writes that
|
||||||
don't originate from a runnable task, eg. graph input, update_state, etc."""
|
don't originate from a runnable task, eg. graph input, update_state, etc."""
|
||||||
|
|
||||||
path: tuple[Union[str, int, tuple], ...]
|
path: tuple[str | int | tuple, ...]
|
||||||
name: str
|
name: str
|
||||||
writes: Sequence[tuple[str, Any]]
|
writes: Sequence[tuple[str, Any]]
|
||||||
triggers: Sequence[str]
|
triggers: Sequence[str]
|
||||||
@@ -118,8 +119,8 @@ class Call:
|
|||||||
|
|
||||||
func: Callable
|
func: Callable
|
||||||
input: tuple[tuple[Any, ...], dict[str, Any]]
|
input: tuple[tuple[Any, ...], dict[str, Any]]
|
||||||
retry_policy: Optional[Sequence[RetryPolicy]]
|
retry_policy: Sequence[RetryPolicy] | None
|
||||||
cache_policy: Optional[CachePolicy]
|
cache_policy: CachePolicy | None
|
||||||
callbacks: Callbacks
|
callbacks: Callbacks
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -127,8 +128,8 @@ class Call:
|
|||||||
func: Callable,
|
func: Callable,
|
||||||
input: tuple[tuple[Any, ...], dict[str, Any]],
|
input: tuple[tuple[Any, ...], dict[str, Any]],
|
||||||
*,
|
*,
|
||||||
retry_policy: Optional[Sequence[RetryPolicy]],
|
retry_policy: Sequence[RetryPolicy] | None,
|
||||||
cache_policy: Optional[CachePolicy],
|
cache_policy: CachePolicy | None,
|
||||||
callbacks: Callbacks,
|
callbacks: Callbacks,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.func = func
|
self.func = func
|
||||||
@@ -140,7 +141,7 @@ class Call:
|
|||||||
|
|
||||||
def should_interrupt(
|
def should_interrupt(
|
||||||
checkpoint: Checkpoint,
|
checkpoint: Checkpoint,
|
||||||
interrupt_nodes: Union[All, Sequence[str]],
|
interrupt_nodes: All | Sequence[str],
|
||||||
tasks: Iterable[PregelExecutableTask],
|
tasks: Iterable[PregelExecutableTask],
|
||||||
) -> list[PregelExecutableTask]:
|
) -> list[PregelExecutableTask]:
|
||||||
"""Check if the graph should be interrupted based on current state."""
|
"""Check if the graph should be interrupted based on current state."""
|
||||||
@@ -176,9 +177,9 @@ def local_read(
|
|||||||
channels: Mapping[str, BaseChannel],
|
channels: Mapping[str, BaseChannel],
|
||||||
managed: ManagedValueMapping,
|
managed: ManagedValueMapping,
|
||||||
task: WritesProtocol,
|
task: WritesProtocol,
|
||||||
select: Union[list[str], str],
|
select: list[str] | str,
|
||||||
fresh: bool = False,
|
fresh: bool = False,
|
||||||
) -> Union[dict[str, Any], Any]:
|
) -> dict[str, Any] | Any:
|
||||||
"""Function injected under CONFIG_KEY_READ in task config, to read current state.
|
"""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
|
Used by conditional edges to read a copy of the state with reflecting the writes
|
||||||
from that node only."""
|
from that node only."""
|
||||||
@@ -213,7 +214,7 @@ def local_read(
|
|||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
def increment(current: Optional[int]) -> int:
|
def increment(current: int | None) -> int:
|
||||||
"""Default channel versioning function, increments the current int version."""
|
"""Default channel versioning function, increments the current int version."""
|
||||||
return current + 1 if current is not None else 1
|
return current + 1 if current is not None else 1
|
||||||
|
|
||||||
@@ -222,7 +223,7 @@ def apply_writes(
|
|||||||
checkpoint: Checkpoint,
|
checkpoint: Checkpoint,
|
||||||
channels: Mapping[str, BaseChannel],
|
channels: Mapping[str, BaseChannel],
|
||||||
tasks: Iterable[WritesProtocol],
|
tasks: Iterable[WritesProtocol],
|
||||||
get_next_version: Optional[GetNextVersion],
|
get_next_version: GetNextVersion | None,
|
||||||
trigger_to_nodes: Mapping[str, Sequence[str]],
|
trigger_to_nodes: Mapping[str, Sequence[str]],
|
||||||
) -> set[str]:
|
) -> set[str]:
|
||||||
"""Apply writes from a set of tasks (usually the tasks from a Pregel step)
|
"""Apply writes from a set of tasks (usually the tasks from a Pregel step)
|
||||||
@@ -338,8 +339,8 @@ def prepare_next_tasks(
|
|||||||
store: Literal[None] = None,
|
store: Literal[None] = None,
|
||||||
checkpointer: Literal[None] = None,
|
checkpointer: Literal[None] = None,
|
||||||
manager: Literal[None] = None,
|
manager: Literal[None] = None,
|
||||||
trigger_to_nodes: Optional[Mapping[str, Sequence[str]]] = None,
|
trigger_to_nodes: Mapping[str, Sequence[str]] | None = None,
|
||||||
updated_channels: Optional[set[str]] = None,
|
updated_channels: set[str] | None = None,
|
||||||
retry_policy: Sequence[RetryPolicy] = (),
|
retry_policy: Sequence[RetryPolicy] = (),
|
||||||
cache_policy: Literal[None] = None,
|
cache_policy: Literal[None] = None,
|
||||||
) -> dict[str, PregelTask]: ...
|
) -> dict[str, PregelTask]: ...
|
||||||
@@ -357,13 +358,13 @@ def prepare_next_tasks(
|
|||||||
stop: int,
|
stop: int,
|
||||||
*,
|
*,
|
||||||
for_execution: Literal[True],
|
for_execution: Literal[True],
|
||||||
store: Optional[BaseStore],
|
store: BaseStore | None,
|
||||||
checkpointer: Optional[BaseCheckpointSaver],
|
checkpointer: BaseCheckpointSaver | None,
|
||||||
manager: Union[None, ParentRunManager, AsyncParentRunManager],
|
manager: None | ParentRunManager | AsyncParentRunManager,
|
||||||
trigger_to_nodes: Optional[Mapping[str, Sequence[str]]] = None,
|
trigger_to_nodes: Mapping[str, Sequence[str]] | None = None,
|
||||||
updated_channels: Optional[set[str]] = None,
|
updated_channels: set[str] | None = None,
|
||||||
retry_policy: Sequence[RetryPolicy] = (),
|
retry_policy: Sequence[RetryPolicy] = (),
|
||||||
cache_policy: Optional[CachePolicy] = None,
|
cache_policy: CachePolicy | None = None,
|
||||||
) -> dict[str, PregelExecutableTask]: ...
|
) -> dict[str, PregelExecutableTask]: ...
|
||||||
|
|
||||||
|
|
||||||
@@ -378,14 +379,14 @@ def prepare_next_tasks(
|
|||||||
stop: int,
|
stop: int,
|
||||||
*,
|
*,
|
||||||
for_execution: bool,
|
for_execution: bool,
|
||||||
store: Optional[BaseStore] = None,
|
store: BaseStore | None = None,
|
||||||
checkpointer: Optional[BaseCheckpointSaver] = None,
|
checkpointer: BaseCheckpointSaver | None = None,
|
||||||
manager: Union[None, ParentRunManager, AsyncParentRunManager] = None,
|
manager: None | ParentRunManager | AsyncParentRunManager = None,
|
||||||
trigger_to_nodes: Optional[Mapping[str, Sequence[str]]] = None,
|
trigger_to_nodes: Mapping[str, Sequence[str]] | None = None,
|
||||||
updated_channels: Optional[set[str]] = None,
|
updated_channels: set[str] | None = None,
|
||||||
retry_policy: Sequence[RetryPolicy] = (),
|
retry_policy: Sequence[RetryPolicy] = (),
|
||||||
cache_policy: Optional[CachePolicy] = None,
|
cache_policy: CachePolicy | None = None,
|
||||||
) -> Union[dict[str, PregelTask], dict[str, PregelExecutableTask]]:
|
) -> dict[str, PregelTask] | dict[str, PregelExecutableTask]:
|
||||||
"""Prepare the set of tasks that will make up the next Pregel step.
|
"""Prepare the set of tasks that will make up the next Pregel step.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -415,7 +416,7 @@ def prepare_next_tasks(
|
|||||||
input_cache: dict[INPUT_CACHE_KEY_TYPE, Any] = {}
|
input_cache: dict[INPUT_CACHE_KEY_TYPE, Any] = {}
|
||||||
checkpoint_id_bytes = binascii.unhexlify(checkpoint["id"].replace("-", ""))
|
checkpoint_id_bytes = binascii.unhexlify(checkpoint["id"].replace("-", ""))
|
||||||
null_version = checkpoint_null_version(checkpoint)
|
null_version = checkpoint_null_version(checkpoint)
|
||||||
tasks: list[Union[PregelTask, PregelExecutableTask]] = []
|
tasks: list[PregelTask | PregelExecutableTask] = []
|
||||||
# Consume pending tasks
|
# Consume pending tasks
|
||||||
tasks_channel = cast(Optional[Topic[Send]], channels.get(TASKS))
|
tasks_channel = cast(Optional[Topic[Send]], channels.get(TASKS))
|
||||||
if tasks_channel and tasks_channel.is_available():
|
if tasks_channel and tasks_channel.is_available():
|
||||||
@@ -496,11 +497,11 @@ PUSH_TRIGGER = (PUSH,)
|
|||||||
|
|
||||||
def prepare_single_task(
|
def prepare_single_task(
|
||||||
task_path: tuple[Any, ...],
|
task_path: tuple[Any, ...],
|
||||||
task_id_checksum: Optional[str],
|
task_id_checksum: str | None,
|
||||||
*,
|
*,
|
||||||
checkpoint: Checkpoint,
|
checkpoint: Checkpoint,
|
||||||
checkpoint_id_bytes: bytes,
|
checkpoint_id_bytes: bytes,
|
||||||
checkpoint_null_version: Optional[V],
|
checkpoint_null_version: V | None,
|
||||||
pending_writes: list[PendingWrite],
|
pending_writes: list[PendingWrite],
|
||||||
processes: Mapping[str, PregelNode],
|
processes: Mapping[str, PregelNode],
|
||||||
channels: Mapping[str, BaseChannel],
|
channels: Mapping[str, BaseChannel],
|
||||||
@@ -509,13 +510,13 @@ def prepare_single_task(
|
|||||||
step: int,
|
step: int,
|
||||||
stop: int,
|
stop: int,
|
||||||
for_execution: bool,
|
for_execution: bool,
|
||||||
store: Optional[BaseStore] = None,
|
store: BaseStore | None = None,
|
||||||
checkpointer: Optional[BaseCheckpointSaver] = None,
|
checkpointer: BaseCheckpointSaver | None = None,
|
||||||
manager: Union[None, ParentRunManager, AsyncParentRunManager] = None,
|
manager: None | ParentRunManager | AsyncParentRunManager = None,
|
||||||
input_cache: Optional[dict[INPUT_CACHE_KEY_TYPE, Any]] = None,
|
input_cache: dict[INPUT_CACHE_KEY_TYPE, Any] | None = None,
|
||||||
cache_policy: Optional[CachePolicy] = None,
|
cache_policy: CachePolicy | None = None,
|
||||||
retry_policy: Sequence[RetryPolicy] = (),
|
retry_policy: Sequence[RetryPolicy] = (),
|
||||||
) -> Union[None, PregelTask, PregelExecutableTask]:
|
) -> None | PregelTask | PregelExecutableTask:
|
||||||
"""Prepares a single task for the next Pregel step, given a task path, which
|
"""Prepares a single task for the next Pregel step, given a task path, which
|
||||||
uniquely identifies a PUSH or PULL task within the graph."""
|
uniquely identifies a PUSH or PULL task within the graph."""
|
||||||
configurable = config.get(CONF, {})
|
configurable = config.get(CONF, {})
|
||||||
@@ -560,7 +561,7 @@ def prepare_single_task(
|
|||||||
cache_policy = call.cache_policy or cache_policy
|
cache_policy = call.cache_policy or cache_policy
|
||||||
if cache_policy:
|
if cache_policy:
|
||||||
args_key = cache_policy.key_func(*call.input[0], **call.input[1])
|
args_key = cache_policy.key_func(*call.input[0], **call.input[1])
|
||||||
cache_key: Optional[CacheKey] = CacheKey(
|
cache_key: CacheKey | None = CacheKey(
|
||||||
(
|
(
|
||||||
CACHE_NS_WRITES,
|
CACHE_NS_WRITES,
|
||||||
(identifier(call.func) or "__dynamic__"),
|
(identifier(call.func) or "__dynamic__"),
|
||||||
@@ -908,7 +909,7 @@ def prepare_single_task(
|
|||||||
|
|
||||||
def checkpoint_null_version(
|
def checkpoint_null_version(
|
||||||
checkpoint: Checkpoint,
|
checkpoint: Checkpoint,
|
||||||
) -> Optional[V]:
|
) -> V | None:
|
||||||
"""Get the null version for the checkpoint, if available."""
|
"""Get the null version for the checkpoint, if available."""
|
||||||
for version in checkpoint["channel_versions"].values():
|
for version in checkpoint["channel_versions"].values():
|
||||||
return type(version)()
|
return type(version)()
|
||||||
@@ -918,7 +919,7 @@ def checkpoint_null_version(
|
|||||||
def _triggers(
|
def _triggers(
|
||||||
channels: Mapping[str, BaseChannel],
|
channels: Mapping[str, BaseChannel],
|
||||||
versions: ChannelVersions,
|
versions: ChannelVersions,
|
||||||
seen: Optional[ChannelVersions],
|
seen: ChannelVersions | None,
|
||||||
null_version: V,
|
null_version: V,
|
||||||
proc: PregelNode,
|
proc: PregelNode,
|
||||||
) -> Sequence[str]:
|
) -> Sequence[str]:
|
||||||
@@ -936,11 +937,11 @@ def _triggers(
|
|||||||
|
|
||||||
|
|
||||||
def _scratchpad(
|
def _scratchpad(
|
||||||
parent_scratchpad: Optional[PregelScratchpad],
|
parent_scratchpad: PregelScratchpad | None,
|
||||||
pending_writes: list[PendingWrite],
|
pending_writes: list[PendingWrite],
|
||||||
task_id: str,
|
task_id: str,
|
||||||
namespace_hash: str,
|
namespace_hash: str,
|
||||||
resume_map: Optional[dict[str, Any]],
|
resume_map: dict[str, Any] | None,
|
||||||
step: int,
|
step: int,
|
||||||
stop: int,
|
stop: int,
|
||||||
) -> PregelScratchpad:
|
) -> PregelScratchpad:
|
||||||
@@ -1010,7 +1011,7 @@ def _proc_input(
|
|||||||
*,
|
*,
|
||||||
for_execution: bool,
|
for_execution: bool,
|
||||||
scratchpad: PregelScratchpad,
|
scratchpad: PregelScratchpad,
|
||||||
input_cache: Optional[dict[INPUT_CACHE_KEY_TYPE, Any]],
|
input_cache: dict[INPUT_CACHE_KEY_TYPE, Any] | None,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Prepare input for a PULL task, based on the process's channels and triggers."""
|
"""Prepare input for a PULL task, based on the process's channels and triggers."""
|
||||||
# if in cache return shallow copy
|
# if in cache return shallow copy
|
||||||
@@ -1053,7 +1054,7 @@ def _proc_input(
|
|||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
||||||
def _uuid5_str(namespace: bytes, *parts: Union[str, bytes]) -> str:
|
def _uuid5_str(namespace: bytes, *parts: str | bytes) -> str:
|
||||||
"""Generate a UUID from the SHA-1 hash of a namespace and str parts."""
|
"""Generate a UUID from the SHA-1 hash of a namespace and str parts."""
|
||||||
|
|
||||||
sha = sha1(namespace, usedforsecurity=False)
|
sha = sha1(namespace, usedforsecurity=False)
|
||||||
@@ -1062,7 +1063,7 @@ def _uuid5_str(namespace: bytes, *parts: Union[str, bytes]) -> str:
|
|||||||
return f"{hex[:8]}-{hex[8:12]}-{hex[12:16]}-{hex[16:20]}-{hex[20:32]}"
|
return f"{hex[:8]}-{hex[8:12]}-{hex[12:16]}-{hex[16:20]}-{hex[20:32]}"
|
||||||
|
|
||||||
|
|
||||||
def _xxhash_str(namespace: bytes, *parts: Union[str, bytes]) -> str:
|
def _xxhash_str(namespace: bytes, *parts: str | bytes) -> str:
|
||||||
"""Generate a UUID from the XXH3 hash of a namespace and str parts."""
|
"""Generate a UUID from the XXH3 hash of a namespace and str parts."""
|
||||||
hex = xxh3_128_hexdigest(
|
hex = xxh3_128_hexdigest(
|
||||||
namespace + b"".join(p.encode() if isinstance(p, str) else p for p in parts)
|
namespace + b"".join(p.encode() if isinstance(p, str) else p for p in parts)
|
||||||
@@ -1070,7 +1071,7 @@ def _xxhash_str(namespace: bytes, *parts: Union[str, bytes]) -> str:
|
|||||||
return f"{hex[:8]}-{hex[8:12]}-{hex[12:16]}-{hex[16:20]}-{hex[20:32]}"
|
return f"{hex[:8]}-{hex[8:12]}-{hex[12:16]}-{hex[16:20]}-{hex[20:32]}"
|
||||||
|
|
||||||
|
|
||||||
def task_path_str(tup: Union[str, int, tuple]) -> str:
|
def task_path_str(tup: str | int | tuple) -> str:
|
||||||
"""Generate a string representation of the task path."""
|
"""Generate a string representation of the task path."""
|
||||||
return (
|
return (
|
||||||
f"~{', '.join(task_path_str(x) for x in tup)}"
|
f"~{', '.join(task_path_str(x) for x in tup)}"
|
||||||
@@ -1087,7 +1088,7 @@ LAZY_ATOMIC_COUNTER_LOCK = threading.Lock()
|
|||||||
class LazyAtomicCounter:
|
class LazyAtomicCounter:
|
||||||
__slots__ = ("_counter",)
|
__slots__ = ("_counter",)
|
||||||
|
|
||||||
_counter: Optional[Callable[[], int]]
|
_counter: Callable[[], int] | None
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._counter = None
|
self._counter = None
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
"""Utility to convert a user provided function into a Runnable with a ChannelWrite."""
|
"""Utility to convert a user provided function into a Runnable with a ChannelWrite."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
from collections.abc import Generator, Sequence
|
from collections.abc import Generator, Sequence
|
||||||
from typing import Any, Callable, Generic, Optional, TypeVar, cast
|
from typing import Any, Callable, Generic, TypeVar, cast
|
||||||
|
|
||||||
from langchain_core.runnables import Runnable
|
from langchain_core.runnables import Runnable
|
||||||
from typing_extensions import ParamSpec
|
from typing_extensions import ParamSpec
|
||||||
@@ -40,7 +42,7 @@ def _getattribute(obj: Any, name: str) -> Any:
|
|||||||
return obj, parent
|
return obj, parent
|
||||||
|
|
||||||
|
|
||||||
def _whichmodule(obj: Any, name: str) -> Optional[str]:
|
def _whichmodule(obj: Any, name: str) -> str | None:
|
||||||
"""Find the module an object belongs to.
|
"""Find the module an object belongs to.
|
||||||
|
|
||||||
This function differs from ``pickle.whichmodule`` in two ways:
|
This function differs from ``pickle.whichmodule`` in two ways:
|
||||||
@@ -74,7 +76,7 @@ def _whichmodule(obj: Any, name: str) -> Optional[str]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def identifier(obj: Any, name: Optional[str] = None) -> Optional[str]:
|
def identifier(obj: Any, name: str | None = None) -> str | None:
|
||||||
"""Return the module and name of an object."""
|
"""Return the module and name of an object."""
|
||||||
from langgraph.pregel.read import PregelNode
|
from langgraph.pregel.read import PregelNode
|
||||||
from langgraph.utils.runnable import RunnableCallable, RunnableSeq
|
from langgraph.utils.runnable import RunnableCallable, RunnableSeq
|
||||||
@@ -104,8 +106,8 @@ def identifier(obj: Any, name: Optional[str] = None) -> Optional[str]:
|
|||||||
|
|
||||||
|
|
||||||
def _lookup_module_and_qualname(
|
def _lookup_module_and_qualname(
|
||||||
obj: Any, name: Optional[str] = None
|
obj: Any, name: str | None = None
|
||||||
) -> Optional[tuple[types.ModuleType, str]]:
|
) -> tuple[types.ModuleType, str] | None:
|
||||||
if name is None:
|
if name is None:
|
||||||
name = getattr(obj, "__qualname__", None)
|
name = getattr(obj, "__qualname__", None)
|
||||||
if name is None: # pragma: no cover
|
if name is None: # pragma: no cover
|
||||||
@@ -251,8 +253,8 @@ class SyncAsyncFuture(Generic[T], concurrent.futures.Future[T]):
|
|||||||
def call(
|
def call(
|
||||||
func: Callable[P, T],
|
func: Callable[P, T],
|
||||||
*args: Any,
|
*args: Any,
|
||||||
retry_policy: Optional[Sequence[RetryPolicy]] = None,
|
retry_policy: Sequence[RetryPolicy] | None = None,
|
||||||
cache_policy: Optional[CachePolicy] = None,
|
cache_policy: CachePolicy | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> SyncAsyncFuture[T]:
|
) -> SyncAsyncFuture[T]:
|
||||||
config = get_config()
|
config = get_config()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Optional, Union
|
|
||||||
|
|
||||||
from langgraph.channels.base import BaseChannel
|
from langgraph.channels.base import BaseChannel
|
||||||
from langgraph.checkpoint.base import Checkpoint
|
from langgraph.checkpoint.base import Checkpoint
|
||||||
@@ -24,10 +25,10 @@ def empty_checkpoint() -> Checkpoint:
|
|||||||
|
|
||||||
def create_checkpoint(
|
def create_checkpoint(
|
||||||
checkpoint: Checkpoint,
|
checkpoint: Checkpoint,
|
||||||
channels: Optional[Mapping[str, BaseChannel]],
|
channels: Mapping[str, BaseChannel] | None,
|
||||||
step: int,
|
step: int,
|
||||||
*,
|
*,
|
||||||
id: Optional[str] = None,
|
id: str | None = None,
|
||||||
) -> Checkpoint:
|
) -> Checkpoint:
|
||||||
"""Create a checkpoint for the given channels."""
|
"""Create a checkpoint for the given channels."""
|
||||||
ts = datetime.now(timezone.utc).isoformat()
|
ts = datetime.now(timezone.utc).isoformat()
|
||||||
@@ -52,7 +53,7 @@ def create_checkpoint(
|
|||||||
|
|
||||||
|
|
||||||
def channels_from_checkpoint(
|
def channels_from_checkpoint(
|
||||||
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]],
|
specs: Mapping[str, BaseChannel | ManagedValueSpec],
|
||||||
checkpoint: Checkpoint,
|
checkpoint: Checkpoint,
|
||||||
) -> tuple[Mapping[str, BaseChannel], ManagedValueMapping]:
|
) -> tuple[Mapping[str, BaseChannel], ManagedValueMapping]:
|
||||||
"""Get channels from a checkpoint."""
|
"""Get channels from a checkpoint."""
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
@@ -6,7 +8,6 @@ from pprint import pformat
|
|||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Literal,
|
Literal,
|
||||||
Optional,
|
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
@@ -43,7 +44,7 @@ class TaskPayload(TypedDict):
|
|||||||
class TaskResultPayload(TypedDict):
|
class TaskResultPayload(TypedDict):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
error: Optional[str]
|
error: str | None
|
||||||
interrupts: list[dict]
|
interrupts: list[dict]
|
||||||
result: list[tuple[str, Any]]
|
result: list[tuple[str, Any]]
|
||||||
|
|
||||||
@@ -51,17 +52,17 @@ class TaskResultPayload(TypedDict):
|
|||||||
class CheckpointTask(TypedDict):
|
class CheckpointTask(TypedDict):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
error: Optional[str]
|
error: str | None
|
||||||
interrupts: list[dict]
|
interrupts: list[dict]
|
||||||
state: Optional[RunnableConfig]
|
state: RunnableConfig | None
|
||||||
|
|
||||||
|
|
||||||
class CheckpointPayload(TypedDict):
|
class CheckpointPayload(TypedDict):
|
||||||
config: Optional[RunnableConfig]
|
config: RunnableConfig | None
|
||||||
metadata: CheckpointMetadata
|
metadata: CheckpointMetadata
|
||||||
values: dict[str, Any]
|
values: dict[str, Any]
|
||||||
next: list[str]
|
next: list[str]
|
||||||
parent_config: Optional[RunnableConfig]
|
parent_config: RunnableConfig | None
|
||||||
tasks: list[CheckpointTask]
|
tasks: list[CheckpointTask]
|
||||||
|
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ def map_debug_tasks(
|
|||||||
def map_debug_task_results(
|
def map_debug_task_results(
|
||||||
step: int,
|
step: int,
|
||||||
task_tup: tuple[PregelExecutableTask, Sequence[tuple[str, Any]]],
|
task_tup: tuple[PregelExecutableTask, Sequence[tuple[str, Any]]],
|
||||||
stream_keys: Union[str, Sequence[str]],
|
stream_keys: str | Sequence[str],
|
||||||
) -> Iterator[DebugOutputTaskResult]:
|
) -> Iterator[DebugOutputTaskResult]:
|
||||||
"""Produce "task_result" events for stream_mode=debug."""
|
"""Produce "task_result" events for stream_mode=debug."""
|
||||||
stream_channels_list = (
|
stream_channels_list = (
|
||||||
@@ -144,7 +145,7 @@ def map_debug_task_results(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def rm_pregel_keys(config: Optional[RunnableConfig]) -> Optional[RunnableConfig]:
|
def rm_pregel_keys(config: RunnableConfig | None) -> RunnableConfig | None:
|
||||||
"""Remove pregel-specific keys from the config."""
|
"""Remove pregel-specific keys from the config."""
|
||||||
if config is None:
|
if config is None:
|
||||||
return config
|
return config
|
||||||
@@ -161,18 +162,18 @@ def map_debug_checkpoint(
|
|||||||
step: int,
|
step: int,
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
channels: Mapping[str, BaseChannel],
|
channels: Mapping[str, BaseChannel],
|
||||||
stream_channels: Union[str, Sequence[str]],
|
stream_channels: str | Sequence[str],
|
||||||
metadata: CheckpointMetadata,
|
metadata: CheckpointMetadata,
|
||||||
checkpoint: Checkpoint,
|
checkpoint: Checkpoint,
|
||||||
tasks: Iterable[PregelExecutableTask],
|
tasks: Iterable[PregelExecutableTask],
|
||||||
pending_writes: list[PendingWrite],
|
pending_writes: list[PendingWrite],
|
||||||
parent_config: Optional[RunnableConfig],
|
parent_config: RunnableConfig | None,
|
||||||
output_keys: Union[str, Sequence[str]],
|
output_keys: str | Sequence[str],
|
||||||
) -> Iterator[DebugOutputCheckpoint]:
|
) -> Iterator[DebugOutputCheckpoint]:
|
||||||
"""Produce "checkpoint" events for stream_mode=debug."""
|
"""Produce "checkpoint" events for stream_mode=debug."""
|
||||||
|
|
||||||
parent_ns = config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "")
|
parent_ns = config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "")
|
||||||
task_states: dict[str, Union[RunnableConfig, StateSnapshot]] = {}
|
task_states: dict[str, RunnableConfig | StateSnapshot] = {}
|
||||||
|
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
if not task.subgraphs:
|
if not task.subgraphs:
|
||||||
@@ -278,10 +279,10 @@ def print_step_checkpoint(
|
|||||||
|
|
||||||
|
|
||||||
def tasks_w_writes(
|
def tasks_w_writes(
|
||||||
tasks: Iterable[Union[PregelTask, PregelExecutableTask]],
|
tasks: Iterable[PregelTask | PregelExecutableTask],
|
||||||
pending_writes: Optional[list[PendingWrite]],
|
pending_writes: list[PendingWrite] | None,
|
||||||
states: Optional[dict[str, Union[RunnableConfig, StateSnapshot]]],
|
states: dict[str, RunnableConfig | StateSnapshot] | None,
|
||||||
output_keys: Union[str, Sequence[str]],
|
output_keys: str | Sequence[str],
|
||||||
) -> tuple[PregelTask, ...]:
|
) -> tuple[PregelTask, ...]:
|
||||||
"""Apply writes / subgraph states to tasks to be returned in a StateSnapshot."""
|
"""Apply writes / subgraph states to tasks to be returned in a StateSnapshot."""
|
||||||
pending_writes = pending_writes or []
|
pending_writes = pending_writes or []
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
from typing import Any, Optional, Union, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from langchain_core.runnables.config import RunnableConfig
|
from langchain_core.runnables.config import RunnableConfig
|
||||||
from langchain_core.runnables.graph import Graph, Node
|
from langchain_core.runnables.graph import Graph, Node
|
||||||
@@ -26,10 +28,10 @@ def draw_graph(
|
|||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
*,
|
*,
|
||||||
nodes: dict[str, PregelNode],
|
nodes: dict[str, PregelNode],
|
||||||
specs: dict[str, Union[BaseChannel, ManagedValueSpec]],
|
specs: dict[str, BaseChannel | ManagedValueSpec],
|
||||||
input_channels: Union[str, Sequence[str]],
|
input_channels: str | Sequence[str],
|
||||||
interrupt_after_nodes: Union[All, Sequence[str]],
|
interrupt_after_nodes: All | Sequence[str],
|
||||||
interrupt_before_nodes: Union[All, Sequence[str]],
|
interrupt_before_nodes: All | Sequence[str],
|
||||||
trigger_to_nodes: Mapping[str, Sequence[str]],
|
trigger_to_nodes: Mapping[str, Sequence[str]],
|
||||||
checkpointer: Checkpointer,
|
checkpointer: Checkpointer,
|
||||||
subgraphs: dict[str, Graph],
|
subgraphs: dict[str, Graph],
|
||||||
@@ -46,7 +48,7 @@ def draw_graph(
|
|||||||
The graph for this Pregel instance.
|
The graph for this Pregel instance.
|
||||||
"""
|
"""
|
||||||
# (src, dest, is_conditional, label)
|
# (src, dest, is_conditional, label)
|
||||||
edges: set[tuple[str, str, bool, Optional[str]]] = set()
|
edges: set[tuple[str, str, bool, str | None]] = set()
|
||||||
|
|
||||||
step = -1
|
step = -1
|
||||||
checkpoint = empty_checkpoint()
|
checkpoint = empty_checkpoint()
|
||||||
@@ -60,8 +62,8 @@ def draw_graph(
|
|||||||
checkpoint,
|
checkpoint,
|
||||||
)
|
)
|
||||||
static_seen: set[Any] = set()
|
static_seen: set[Any] = set()
|
||||||
sources: dict[str, set[tuple[str, bool, Optional[str]]]] = {}
|
sources: dict[str, set[tuple[str, bool, str | None]]] = {}
|
||||||
step_sources: dict[str, set[tuple[str, bool, Optional[str]]]] = {}
|
step_sources: dict[str, set[tuple[str, bool, str | None]]] = {}
|
||||||
# remove node mappers
|
# remove node mappers
|
||||||
nodes = {
|
nodes = {
|
||||||
k: v.copy(update={"mapper": None}) if v.mapper is not None else v
|
k: v.copy(update={"mapper": None}) if v.mapper is not None else v
|
||||||
@@ -100,7 +102,7 @@ def draw_graph(
|
|||||||
for step in range(step, limit):
|
for step in range(step, limit):
|
||||||
if not tasks:
|
if not tasks:
|
||||||
break
|
break
|
||||||
conditionals: dict[tuple[str, str, Any], Optional[str]] = {}
|
conditionals: dict[tuple[str, str, Any], str | None] = {}
|
||||||
# run task writers
|
# run task writers
|
||||||
for task in tasks.values():
|
for task in tasks.values():
|
||||||
for w in task.writers:
|
for w in task.writers:
|
||||||
@@ -140,8 +142,8 @@ def draw_graph(
|
|||||||
}
|
}
|
||||||
sources.update(step_sources)
|
sources.update(step_sources)
|
||||||
# invert triggers
|
# invert triggers
|
||||||
trigger_to_sources: dict[str, set[tuple[str, bool, Optional[str]]]] = (
|
trigger_to_sources: dict[str, set[tuple[str, bool, str | None]]] = defaultdict(
|
||||||
defaultdict(set)
|
set
|
||||||
)
|
)
|
||||||
for src, triggers in sources.items():
|
for src, triggers in sources.items():
|
||||||
for trigger, cond, label in triggers:
|
for trigger, cond, label in triggers:
|
||||||
@@ -246,7 +248,7 @@ def add_edge(
|
|||||||
source: str,
|
source: str,
|
||||||
target: str,
|
target: str,
|
||||||
*,
|
*,
|
||||||
data: Optional[Any] = None,
|
data: Any | None = None,
|
||||||
conditional: bool = False,
|
conditional: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add an edge to the graph."""
|
"""Add an edge to the graph."""
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import time
|
import time
|
||||||
@@ -7,7 +9,6 @@ from contextvars import copy_context
|
|||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import (
|
from typing import (
|
||||||
Callable,
|
Callable,
|
||||||
Optional,
|
|
||||||
Protocol,
|
Protocol,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
cast,
|
cast,
|
||||||
@@ -29,7 +30,7 @@ class Submit(Protocol[P, T]):
|
|||||||
self,
|
self,
|
||||||
fn: Callable[P, T],
|
fn: Callable[P, T],
|
||||||
*args: P.args,
|
*args: P.args,
|
||||||
__name__: Optional[str] = None,
|
__name__: str | None = None,
|
||||||
__cancel_on_exit__: bool = False,
|
__cancel_on_exit__: bool = False,
|
||||||
__reraise_on_exit__: bool = True,
|
__reraise_on_exit__: bool = True,
|
||||||
__next_tick__: bool = False,
|
__next_tick__: bool = False,
|
||||||
@@ -55,7 +56,7 @@ class BackgroundExecutor(AbstractContextManager):
|
|||||||
self,
|
self,
|
||||||
fn: Callable[P, T],
|
fn: Callable[P, T],
|
||||||
*args: P.args,
|
*args: P.args,
|
||||||
__name__: Optional[str] = None, # currently not used in sync version
|
__name__: str | None = None, # currently not used in sync version
|
||||||
__cancel_on_exit__: bool = False, # for sync, can cancel only if not started
|
__cancel_on_exit__: bool = False, # for sync, can cancel only if not started
|
||||||
__reraise_on_exit__: bool = True,
|
__reraise_on_exit__: bool = True,
|
||||||
__next_tick__: bool = False,
|
__next_tick__: bool = False,
|
||||||
@@ -92,10 +93,10 @@ class BackgroundExecutor(AbstractContextManager):
|
|||||||
|
|
||||||
def __exit__(
|
def __exit__(
|
||||||
self,
|
self,
|
||||||
exc_type: Optional[type[BaseException]],
|
exc_type: type[BaseException] | None,
|
||||||
exc_value: Optional[BaseException],
|
exc_value: BaseException | None,
|
||||||
traceback: Optional[TracebackType],
|
traceback: TracebackType | None,
|
||||||
) -> Optional[bool]:
|
) -> bool | None:
|
||||||
# copy the tasks as done() callback may modify the dict
|
# copy the tasks as done() callback may modify the dict
|
||||||
tasks = self.tasks.copy()
|
tasks = self.tasks.copy()
|
||||||
# cancel all tasks that should be cancelled
|
# cancel all tasks that should be cancelled
|
||||||
@@ -133,7 +134,7 @@ class AsyncBackgroundExecutor(AbstractAsyncContextManager):
|
|||||||
self.sentinel = object()
|
self.sentinel = object()
|
||||||
self.loop = asyncio.get_running_loop()
|
self.loop = asyncio.get_running_loop()
|
||||||
if max_concurrency := config.get("max_concurrency"):
|
if max_concurrency := config.get("max_concurrency"):
|
||||||
self.semaphore: Optional[asyncio.Semaphore] = asyncio.Semaphore(
|
self.semaphore: asyncio.Semaphore | None = asyncio.Semaphore(
|
||||||
max_concurrency
|
max_concurrency
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -143,7 +144,7 @@ class AsyncBackgroundExecutor(AbstractAsyncContextManager):
|
|||||||
self,
|
self,
|
||||||
fn: Callable[P, Awaitable[T]],
|
fn: Callable[P, Awaitable[T]],
|
||||||
*args: P.args,
|
*args: P.args,
|
||||||
__name__: Optional[str] = None,
|
__name__: str | None = None,
|
||||||
__cancel_on_exit__: bool = False,
|
__cancel_on_exit__: bool = False,
|
||||||
__reraise_on_exit__: bool = True,
|
__reraise_on_exit__: bool = True,
|
||||||
__next_tick__: bool = False, # noop in async (always True)
|
__next_tick__: bool = False, # noop in async (always True)
|
||||||
@@ -185,9 +186,9 @@ class AsyncBackgroundExecutor(AbstractAsyncContextManager):
|
|||||||
|
|
||||||
async def __aexit__(
|
async def __aexit__(
|
||||||
self,
|
self,
|
||||||
exc_type: Optional[type[BaseException]],
|
exc_type: type[BaseException] | None,
|
||||||
exc_value: Optional[BaseException],
|
exc_value: BaseException | None,
|
||||||
traceback: Optional[TracebackType],
|
traceback: TracebackType | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# copy the tasks as done() callback may modify the dict
|
# copy the tasks as done() callback may modify the dict
|
||||||
tasks = self.tasks.copy()
|
tasks = self.tasks.copy()
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from collections.abc import Iterator, Mapping, Sequence
|
from collections.abc import Iterator, Mapping, Sequence
|
||||||
from typing import Any, Literal, Optional, Union
|
from typing import Any, Literal
|
||||||
|
|
||||||
from langgraph.channels.base import BaseChannel, EmptyChannelError
|
from langgraph.channels.base import BaseChannel, EmptyChannelError
|
||||||
from langgraph.constants import (
|
from langgraph.constants import (
|
||||||
@@ -37,10 +39,10 @@ def read_channel(
|
|||||||
|
|
||||||
def read_channels(
|
def read_channels(
|
||||||
channels: Mapping[str, BaseChannel],
|
channels: Mapping[str, BaseChannel],
|
||||||
select: Union[Sequence[str], str],
|
select: Sequence[str] | str,
|
||||||
*,
|
*,
|
||||||
skip_empty: bool = True,
|
skip_empty: bool = True,
|
||||||
) -> Union[dict[str, Any], Any]:
|
) -> dict[str, Any] | Any:
|
||||||
if isinstance(select, str):
|
if isinstance(select, str):
|
||||||
return read_channel(channels, select)
|
return read_channel(channels, select)
|
||||||
else:
|
else:
|
||||||
@@ -79,8 +81,8 @@ def map_command(cmd: Command) -> Iterator[tuple[str, str, Any]]:
|
|||||||
|
|
||||||
|
|
||||||
def map_input(
|
def map_input(
|
||||||
input_channels: Union[str, Sequence[str]],
|
input_channels: str | Sequence[str],
|
||||||
chunk: Optional[Union[dict[str, Any], Any]],
|
chunk: dict[str, Any] | Any | None,
|
||||||
) -> Iterator[tuple[str, Any]]:
|
) -> Iterator[tuple[str, Any]]:
|
||||||
"""Map input chunk to a sequence of pending writes in the form (channel, value)."""
|
"""Map input chunk to a sequence of pending writes in the form (channel, value)."""
|
||||||
if chunk is None:
|
if chunk is None:
|
||||||
@@ -98,10 +100,10 @@ def map_input(
|
|||||||
|
|
||||||
|
|
||||||
def map_output_values(
|
def map_output_values(
|
||||||
output_channels: Union[str, Sequence[str]],
|
output_channels: str | Sequence[str],
|
||||||
pending_writes: Union[Literal[True], Sequence[tuple[str, Any]]],
|
pending_writes: Literal[True] | Sequence[tuple[str, Any]],
|
||||||
channels: Mapping[str, BaseChannel],
|
channels: Mapping[str, BaseChannel],
|
||||||
) -> Iterator[Union[dict[str, Any], Any]]:
|
) -> Iterator[dict[str, Any] | Any]:
|
||||||
"""Map pending writes (a sequence of tuples (channel, value)) to output chunk."""
|
"""Map pending writes (a sequence of tuples (channel, value)) to output chunk."""
|
||||||
if isinstance(output_channels, str):
|
if isinstance(output_channels, str):
|
||||||
if pending_writes is True or any(
|
if pending_writes is True or any(
|
||||||
@@ -116,10 +118,10 @@ def map_output_values(
|
|||||||
|
|
||||||
|
|
||||||
def map_output_updates(
|
def map_output_updates(
|
||||||
output_channels: Union[str, Sequence[str]],
|
output_channels: str | Sequence[str],
|
||||||
tasks: list[tuple[PregelExecutableTask, Sequence[tuple[str, Any]]]],
|
tasks: list[tuple[PregelExecutableTask, Sequence[tuple[str, Any]]]],
|
||||||
cached: bool = False,
|
cached: bool = False,
|
||||||
) -> Iterator[dict[str, Union[Any, dict[str, Any]]]]:
|
) -> Iterator[dict[str, Any | dict[str, Any]]]:
|
||||||
"""Map pending writes (a sequence of tuples (channel, value)) to output chunk."""
|
"""Map pending writes (a sequence of tuples (channel, value)) to output chunk."""
|
||||||
output_tasks = [
|
output_tasks = [
|
||||||
(t, ww)
|
(t, ww)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import binascii
|
import binascii
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
@@ -18,7 +20,6 @@ from typing import (
|
|||||||
Literal,
|
Literal,
|
||||||
Optional,
|
Optional,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
|
||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -148,36 +149,36 @@ def DuplexStream(*streams: StreamProtocol) -> StreamProtocol:
|
|||||||
|
|
||||||
class PregelLoop:
|
class PregelLoop:
|
||||||
config: RunnableConfig
|
config: RunnableConfig
|
||||||
store: Optional["BaseStore"]
|
store: BaseStore | None
|
||||||
stream: Optional[StreamProtocol]
|
stream: StreamProtocol | None
|
||||||
step: int
|
step: int
|
||||||
stop: int
|
stop: int
|
||||||
|
|
||||||
input: Optional[Any]
|
input: Any | None
|
||||||
input_model: Optional[type[BaseModel]]
|
input_model: type[BaseModel] | None
|
||||||
cache: Optional[BaseCache[WritesT]]
|
cache: BaseCache[WritesT] | None
|
||||||
checkpointer: Optional[BaseCheckpointSaver]
|
checkpointer: BaseCheckpointSaver | None
|
||||||
nodes: Mapping[str, PregelNode]
|
nodes: Mapping[str, PregelNode]
|
||||||
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]]
|
specs: Mapping[str, BaseChannel | ManagedValueSpec]
|
||||||
output_keys: Union[str, Sequence[str]]
|
output_keys: str | Sequence[str]
|
||||||
stream_keys: Union[str, Sequence[str]]
|
stream_keys: str | Sequence[str]
|
||||||
skip_done_tasks: bool
|
skip_done_tasks: bool
|
||||||
is_nested: bool
|
is_nested: bool
|
||||||
manager: Union[None, AsyncParentRunManager, ParentRunManager]
|
manager: None | AsyncParentRunManager | ParentRunManager
|
||||||
interrupt_after: Union[All, Sequence[str]]
|
interrupt_after: All | Sequence[str]
|
||||||
interrupt_before: Union[All, Sequence[str]]
|
interrupt_before: All | Sequence[str]
|
||||||
checkpoint_during: bool
|
checkpoint_during: bool
|
||||||
debug: bool
|
debug: bool
|
||||||
retry_policy: Sequence[RetryPolicy]
|
retry_policy: Sequence[RetryPolicy]
|
||||||
cache_policy: Optional[CachePolicy]
|
cache_policy: CachePolicy | None
|
||||||
|
|
||||||
checkpointer_get_next_version: GetNextVersion
|
checkpointer_get_next_version: GetNextVersion
|
||||||
checkpointer_put_writes: Optional[Callable[[RunnableConfig, WritesT, str], Any]]
|
checkpointer_put_writes: Callable[[RunnableConfig, WritesT, str], Any] | None
|
||||||
checkpointer_put_writes_accepts_task_path: bool
|
checkpointer_put_writes_accepts_task_path: bool
|
||||||
_checkpointer_put_after_previous: Optional[
|
_checkpointer_put_after_previous: (
|
||||||
Callable[
|
Callable[
|
||||||
[
|
[
|
||||||
Optional[concurrent.futures.Future],
|
concurrent.futures.Future | None,
|
||||||
RunnableConfig,
|
RunnableConfig,
|
||||||
Checkpoint,
|
Checkpoint,
|
||||||
str,
|
str,
|
||||||
@@ -185,8 +186,9 @@ class PregelLoop:
|
|||||||
],
|
],
|
||||||
Any,
|
Any,
|
||||||
]
|
]
|
||||||
]
|
| None
|
||||||
_migrate_checkpoint: Optional[Callable[[Checkpoint], None]]
|
)
|
||||||
|
_migrate_checkpoint: Callable[[Checkpoint], None] | None
|
||||||
submit: Submit
|
submit: Submit
|
||||||
channels: Mapping[str, BaseChannel]
|
channels: Mapping[str, BaseChannel]
|
||||||
managed: ManagedValueMapping
|
managed: ManagedValueMapping
|
||||||
@@ -196,40 +198,40 @@ class PregelLoop:
|
|||||||
checkpoint_config: RunnableConfig
|
checkpoint_config: RunnableConfig
|
||||||
checkpoint_metadata: CheckpointMetadata
|
checkpoint_metadata: CheckpointMetadata
|
||||||
checkpoint_pending_writes: list[PendingWrite]
|
checkpoint_pending_writes: list[PendingWrite]
|
||||||
checkpoint_previous_versions: dict[str, Union[str, float, int]]
|
checkpoint_previous_versions: dict[str, str | float | int]
|
||||||
prev_checkpoint_config: Optional[RunnableConfig]
|
prev_checkpoint_config: RunnableConfig | None
|
||||||
|
|
||||||
status: Literal[
|
status: Literal[
|
||||||
"pending", "done", "interrupt_before", "interrupt_after", "out_of_steps"
|
"pending", "done", "interrupt_before", "interrupt_after", "out_of_steps"
|
||||||
]
|
]
|
||||||
tasks: dict[str, PregelExecutableTask]
|
tasks: dict[str, PregelExecutableTask]
|
||||||
to_interrupt: list[PregelExecutableTask]
|
to_interrupt: list[PregelExecutableTask]
|
||||||
output: Union[None, dict[str, Any], Any] = None
|
output: None | dict[str, Any] | Any = None
|
||||||
|
|
||||||
# public
|
# public
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
input: Optional[Any],
|
input: Any | None,
|
||||||
*,
|
*,
|
||||||
stream: Optional[StreamProtocol],
|
stream: StreamProtocol | None,
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
store: Optional[BaseStore],
|
store: BaseStore | None,
|
||||||
cache: Optional[BaseCache],
|
cache: BaseCache | None,
|
||||||
checkpointer: Optional[BaseCheckpointSaver],
|
checkpointer: BaseCheckpointSaver | None,
|
||||||
nodes: Mapping[str, PregelNode],
|
nodes: Mapping[str, PregelNode],
|
||||||
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]],
|
specs: Mapping[str, BaseChannel | ManagedValueSpec],
|
||||||
output_keys: Union[str, Sequence[str]],
|
output_keys: str | Sequence[str],
|
||||||
stream_keys: Union[str, Sequence[str]],
|
stream_keys: str | Sequence[str],
|
||||||
trigger_to_nodes: Mapping[str, Sequence[str]],
|
trigger_to_nodes: Mapping[str, Sequence[str]],
|
||||||
interrupt_after: Union[All, Sequence[str]] = EMPTY_SEQ,
|
interrupt_after: All | Sequence[str] = EMPTY_SEQ,
|
||||||
interrupt_before: Union[All, Sequence[str]] = EMPTY_SEQ,
|
interrupt_before: All | Sequence[str] = EMPTY_SEQ,
|
||||||
manager: Union[None, AsyncParentRunManager, ParentRunManager] = None,
|
manager: None | AsyncParentRunManager | ParentRunManager = None,
|
||||||
input_model: Optional[type[BaseModel]] = None,
|
input_model: type[BaseModel] | None = None,
|
||||||
debug: bool = False,
|
debug: bool = False,
|
||||||
migrate_checkpoint: Optional[Callable[[Checkpoint], None]] = None,
|
migrate_checkpoint: Callable[[Checkpoint], None] | None = None,
|
||||||
retry_policy: Sequence[RetryPolicy] = (),
|
retry_policy: Sequence[RetryPolicy] = (),
|
||||||
cache_policy: Optional[CachePolicy] = None,
|
cache_policy: CachePolicy | None = None,
|
||||||
checkpoint_during: bool = True,
|
checkpoint_during: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.stream = stream
|
self.stream = stream
|
||||||
@@ -261,7 +263,7 @@ class PregelLoop:
|
|||||||
self.debug = debug
|
self.debug = debug
|
||||||
if self.stream is not None and CONFIG_KEY_STREAM in config[CONF]:
|
if self.stream is not None and CONFIG_KEY_STREAM in config[CONF]:
|
||||||
self.stream = DuplexStream(self.stream, config[CONF][CONFIG_KEY_STREAM])
|
self.stream = DuplexStream(self.stream, config[CONF][CONFIG_KEY_STREAM])
|
||||||
scratchpad: Optional[PregelScratchpad] = config[CONF].get(CONFIG_KEY_SCRATCHPAD)
|
scratchpad: PregelScratchpad | None = config[CONF].get(CONFIG_KEY_SCRATCHPAD)
|
||||||
if not self.config[CONF].get(CONFIG_KEY_DELEGATE) and isinstance(
|
if not self.config[CONF].get(CONFIG_KEY_DELEGATE) and isinstance(
|
||||||
scratchpad, PregelScratchpad
|
scratchpad, PregelScratchpad
|
||||||
):
|
):
|
||||||
@@ -399,8 +401,8 @@ class PregelLoop:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def accept_push(
|
def accept_push(
|
||||||
self, task: PregelExecutableTask, write_idx: int, call: Optional[Call] = None
|
self, task: PregelExecutableTask, write_idx: int, call: Call | None = None
|
||||||
) -> Optional[PregelExecutableTask]:
|
) -> PregelExecutableTask | None:
|
||||||
"""Accept a PUSH from a task, potentially returning a new task to start."""
|
"""Accept a PUSH from a task, potentially returning a new task to start."""
|
||||||
# don't start if we should interrupt *after* the original task
|
# don't start if we should interrupt *after* the original task
|
||||||
if self.interrupt_after and should_interrupt(
|
if self.interrupt_after and should_interrupt(
|
||||||
@@ -455,7 +457,7 @@ class PregelLoop:
|
|||||||
def tick(
|
def tick(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
input_keys: Union[str, Sequence[str]],
|
input_keys: str | Sequence[str],
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Execute a single iteration of the Pregel loop.
|
"""Execute a single iteration of the Pregel loop.
|
||||||
|
|
||||||
@@ -649,7 +651,7 @@ class PregelLoop:
|
|||||||
else:
|
else:
|
||||||
task.writes.append((k, v))
|
task.writes.append((k, v))
|
||||||
|
|
||||||
def _first(self, *, input_keys: Union[str, Sequence[str]]) -> Optional[set[str]]:
|
def _first(self, *, input_keys: str | Sequence[str]) -> set[str] | None:
|
||||||
# resuming from previous checkpoint requires
|
# resuming from previous checkpoint requires
|
||||||
# - finding a previous checkpoint
|
# - finding a previous checkpoint
|
||||||
# - receiving None input (outer graph) or RESUMING flag (subgraph)
|
# - receiving None input (outer graph) or RESUMING flag (subgraph)
|
||||||
@@ -667,7 +669,7 @@ class PregelLoop:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
# this can be set only when there are input_writes
|
# this can be set only when there are input_writes
|
||||||
updated_channels: Optional[set[str]] = None
|
updated_channels: set[str] | None = None
|
||||||
|
|
||||||
# map command to writes
|
# map command to writes
|
||||||
if isinstance(self.input, Command):
|
if isinstance(self.input, Command):
|
||||||
@@ -861,10 +863,10 @@ class PregelLoop:
|
|||||||
|
|
||||||
def _suppress_interrupt(
|
def _suppress_interrupt(
|
||||||
self,
|
self,
|
||||||
exc_type: Optional[type[BaseException]],
|
exc_type: type[BaseException] | None,
|
||||||
exc_value: Optional[BaseException],
|
exc_value: BaseException | None,
|
||||||
traceback: Optional[TracebackType],
|
traceback: TracebackType | None,
|
||||||
) -> Optional[bool]:
|
) -> bool | None:
|
||||||
# persist current checkpoint and writes
|
# persist current checkpoint and writes
|
||||||
if not self.checkpoint_during:
|
if not self.checkpoint_during:
|
||||||
self._put_checkpoint(self.checkpoint_metadata)
|
self._put_checkpoint(self.checkpoint_metadata)
|
||||||
@@ -977,26 +979,26 @@ class PregelLoop:
|
|||||||
class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
input: Optional[Any],
|
input: Any | None,
|
||||||
*,
|
*,
|
||||||
stream: Optional[StreamProtocol],
|
stream: StreamProtocol | None,
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
store: Optional[BaseStore],
|
store: BaseStore | None,
|
||||||
cache: Optional[BaseCache],
|
cache: BaseCache | None,
|
||||||
checkpointer: Optional[BaseCheckpointSaver],
|
checkpointer: BaseCheckpointSaver | None,
|
||||||
nodes: Mapping[str, PregelNode],
|
nodes: Mapping[str, PregelNode],
|
||||||
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]],
|
specs: Mapping[str, BaseChannel | ManagedValueSpec],
|
||||||
trigger_to_nodes: Mapping[str, Sequence[str]],
|
trigger_to_nodes: Mapping[str, Sequence[str]],
|
||||||
manager: Union[None, AsyncParentRunManager, ParentRunManager] = None,
|
manager: None | AsyncParentRunManager | ParentRunManager = None,
|
||||||
interrupt_after: Union[All, Sequence[str]] = EMPTY_SEQ,
|
interrupt_after: All | Sequence[str] = EMPTY_SEQ,
|
||||||
interrupt_before: Union[All, Sequence[str]] = EMPTY_SEQ,
|
interrupt_before: All | Sequence[str] = EMPTY_SEQ,
|
||||||
output_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
|
output_keys: str | Sequence[str] = EMPTY_SEQ,
|
||||||
stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
|
stream_keys: str | Sequence[str] = EMPTY_SEQ,
|
||||||
input_model: Optional[type[BaseModel]] = None,
|
input_model: type[BaseModel] | None = None,
|
||||||
debug: bool = False,
|
debug: bool = False,
|
||||||
migrate_checkpoint: Optional[Callable[[Checkpoint], None]] = None,
|
migrate_checkpoint: Callable[[Checkpoint], None] | None = None,
|
||||||
retry_policy: Sequence[RetryPolicy] = (),
|
retry_policy: Sequence[RetryPolicy] = (),
|
||||||
cache_policy: Optional[CachePolicy] = None,
|
cache_policy: CachePolicy | None = None,
|
||||||
checkpoint_during: bool = True,
|
checkpoint_during: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -1037,7 +1039,7 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
|||||||
|
|
||||||
def _checkpointer_put_after_previous(
|
def _checkpointer_put_after_previous(
|
||||||
self,
|
self,
|
||||||
prev: Optional[concurrent.futures.Future],
|
prev: concurrent.futures.Future | None,
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
checkpoint: Checkpoint,
|
checkpoint: Checkpoint,
|
||||||
metadata: CheckpointMetadata,
|
metadata: CheckpointMetadata,
|
||||||
@@ -1067,8 +1069,8 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
|||||||
return matched
|
return matched
|
||||||
|
|
||||||
def accept_push(
|
def accept_push(
|
||||||
self, task: PregelExecutableTask, write_idx: int, call: Optional[Call] = None
|
self, task: PregelExecutableTask, write_idx: int, call: Call | None = None
|
||||||
) -> Optional[PregelExecutableTask]:
|
) -> PregelExecutableTask | None:
|
||||||
if pushed := super().accept_push(task, write_idx, call):
|
if pushed := super().accept_push(task, write_idx, call):
|
||||||
for task in self.match_cached_writes():
|
for task in self.match_cached_writes():
|
||||||
self.output_writes(task.id, task.writes, cached=True)
|
self.output_writes(task.id, task.writes, cached=True)
|
||||||
@@ -1156,10 +1158,10 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
|||||||
|
|
||||||
def __exit__(
|
def __exit__(
|
||||||
self,
|
self,
|
||||||
exc_type: Optional[type[BaseException]],
|
exc_type: type[BaseException] | None,
|
||||||
exc_value: Optional[BaseException],
|
exc_value: BaseException | None,
|
||||||
traceback: Optional[TracebackType],
|
traceback: TracebackType | None,
|
||||||
) -> Optional[bool]:
|
) -> bool | None:
|
||||||
# unwind stack
|
# unwind stack
|
||||||
return self.stack.__exit__(exc_type, exc_value, traceback)
|
return self.stack.__exit__(exc_type, exc_value, traceback)
|
||||||
|
|
||||||
@@ -1167,26 +1169,26 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
|
|||||||
class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
|
class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
input: Optional[Any],
|
input: Any | None,
|
||||||
*,
|
*,
|
||||||
stream: Optional[StreamProtocol],
|
stream: StreamProtocol | None,
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
store: Optional[BaseStore],
|
store: BaseStore | None,
|
||||||
cache: Optional[BaseCache],
|
cache: BaseCache | None,
|
||||||
checkpointer: Optional[BaseCheckpointSaver],
|
checkpointer: BaseCheckpointSaver | None,
|
||||||
nodes: Mapping[str, PregelNode],
|
nodes: Mapping[str, PregelNode],
|
||||||
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]],
|
specs: Mapping[str, BaseChannel | ManagedValueSpec],
|
||||||
trigger_to_nodes: Mapping[str, Sequence[str]],
|
trigger_to_nodes: Mapping[str, Sequence[str]],
|
||||||
interrupt_after: Union[All, Sequence[str]] = EMPTY_SEQ,
|
interrupt_after: All | Sequence[str] = EMPTY_SEQ,
|
||||||
interrupt_before: Union[All, Sequence[str]] = EMPTY_SEQ,
|
interrupt_before: All | Sequence[str] = EMPTY_SEQ,
|
||||||
manager: Union[None, AsyncParentRunManager, ParentRunManager] = None,
|
manager: None | AsyncParentRunManager | ParentRunManager = None,
|
||||||
output_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
|
output_keys: str | Sequence[str] = EMPTY_SEQ,
|
||||||
stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
|
stream_keys: str | Sequence[str] = EMPTY_SEQ,
|
||||||
input_model: Optional[type[BaseModel]] = None,
|
input_model: type[BaseModel] | None = None,
|
||||||
debug: bool = False,
|
debug: bool = False,
|
||||||
migrate_checkpoint: Optional[Callable[[Checkpoint], None]] = None,
|
migrate_checkpoint: Callable[[Checkpoint], None] | None = None,
|
||||||
retry_policy: Sequence[RetryPolicy] = (),
|
retry_policy: Sequence[RetryPolicy] = (),
|
||||||
cache_policy: Optional[CachePolicy] = None,
|
cache_policy: CachePolicy | None = None,
|
||||||
checkpoint_during: bool = True,
|
checkpoint_during: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -1227,7 +1229,7 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
|
|||||||
|
|
||||||
async def _checkpointer_put_after_previous(
|
async def _checkpointer_put_after_previous(
|
||||||
self,
|
self,
|
||||||
prev: Optional[asyncio.Task],
|
prev: asyncio.Task | None,
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
checkpoint: Checkpoint,
|
checkpoint: Checkpoint,
|
||||||
metadata: CheckpointMetadata,
|
metadata: CheckpointMetadata,
|
||||||
@@ -1257,8 +1259,8 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
|
|||||||
return matched
|
return matched
|
||||||
|
|
||||||
async def aaccept_push(
|
async def aaccept_push(
|
||||||
self, task: PregelExecutableTask, write_idx: int, call: Optional[Call] = None
|
self, task: PregelExecutableTask, write_idx: int, call: Call | None = None
|
||||||
) -> Optional[PregelExecutableTask]:
|
) -> PregelExecutableTask | None:
|
||||||
if pushed := super().accept_push(task, write_idx, call):
|
if pushed := super().accept_push(task, write_idx, call):
|
||||||
for task in await self.amatch_cached_writes():
|
for task in await self.amatch_cached_writes():
|
||||||
self.output_writes(task.id, task.writes, cached=True)
|
self.output_writes(task.id, task.writes, cached=True)
|
||||||
@@ -1352,10 +1354,10 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
|
|||||||
|
|
||||||
async def __aexit__(
|
async def __aexit__(
|
||||||
self,
|
self,
|
||||||
exc_type: Optional[type[BaseException]],
|
exc_type: type[BaseException] | None,
|
||||||
exc_value: Optional[BaseException],
|
exc_value: BaseException | None,
|
||||||
traceback: Optional[TracebackType],
|
traceback: TracebackType | None,
|
||||||
) -> Optional[bool]:
|
) -> bool | None:
|
||||||
# unwind stack
|
# unwind stack
|
||||||
exit_task = asyncio.create_task(
|
exit_task = asyncio.create_task(
|
||||||
self.stack.__aexit__(exc_type, exc_value, traceback)
|
self.stack.__aexit__(exc_type, exc_value, traceback)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Callable,
|
Callable,
|
||||||
Optional,
|
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
|
||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
@@ -36,7 +36,7 @@ class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
|
|||||||
self.stream = stream
|
self.stream = stream
|
||||||
self.subgraphs = subgraphs
|
self.subgraphs = subgraphs
|
||||||
self.metadata: dict[UUID, Meta] = {}
|
self.metadata: dict[UUID, Meta] = {}
|
||||||
self.seen: set[Union[int, str]] = set()
|
self.seen: set[int | str] = set()
|
||||||
|
|
||||||
def _emit(self, meta: Meta, message: BaseMessage, *, dedupe: bool = False) -> None:
|
def _emit(self, meta: Meta, message: BaseMessage, *, dedupe: bool = False) -> None:
|
||||||
if dedupe and message.id in self.seen:
|
if dedupe and message.id in self.seen:
|
||||||
@@ -89,9 +89,9 @@ class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
|
|||||||
messages: list[list[BaseMessage]],
|
messages: list[list[BaseMessage]],
|
||||||
*,
|
*,
|
||||||
run_id: UUID,
|
run_id: UUID,
|
||||||
parent_run_id: Optional[UUID] = None,
|
parent_run_id: UUID | None = None,
|
||||||
tags: Optional[list[str]] = None,
|
tags: list[str] | None = None,
|
||||||
metadata: Optional[dict[str, Any]] = None,
|
metadata: dict[str, Any] | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
if metadata and (
|
if metadata and (
|
||||||
@@ -111,10 +111,10 @@ class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
|
|||||||
self,
|
self,
|
||||||
token: str,
|
token: str,
|
||||||
*,
|
*,
|
||||||
chunk: Optional[ChatGenerationChunk] = None,
|
chunk: ChatGenerationChunk | None = None,
|
||||||
run_id: UUID,
|
run_id: UUID,
|
||||||
parent_run_id: Optional[UUID] = None,
|
parent_run_id: UUID | None = None,
|
||||||
tags: Optional[list[str]] = None,
|
tags: list[str] | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
if not isinstance(chunk, ChatGenerationChunk):
|
if not isinstance(chunk, ChatGenerationChunk):
|
||||||
@@ -127,7 +127,7 @@ class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
|
|||||||
response: LLMResult,
|
response: LLMResult,
|
||||||
*,
|
*,
|
||||||
run_id: UUID,
|
run_id: UUID,
|
||||||
parent_run_id: Optional[UUID] = None,
|
parent_run_id: UUID | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
if meta := self.metadata.get(run_id):
|
if meta := self.metadata.get(run_id):
|
||||||
@@ -142,7 +142,7 @@ class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
|
|||||||
error: BaseException,
|
error: BaseException,
|
||||||
*,
|
*,
|
||||||
run_id: UUID,
|
run_id: UUID,
|
||||||
parent_run_id: Optional[UUID] = None,
|
parent_run_id: UUID | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
self.metadata.pop(run_id, None)
|
self.metadata.pop(run_id, None)
|
||||||
@@ -153,9 +153,9 @@ class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
|
|||||||
inputs: dict[str, Any],
|
inputs: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
run_id: UUID,
|
run_id: UUID,
|
||||||
parent_run_id: Optional[UUID] = None,
|
parent_run_id: UUID | None = None,
|
||||||
tags: Optional[list[str]] = None,
|
tags: list[str] | None = None,
|
||||||
metadata: Optional[dict[str, Any]] = None,
|
metadata: dict[str, Any] | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
if (
|
if (
|
||||||
@@ -185,7 +185,7 @@ class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
|
|||||||
response: Any,
|
response: Any,
|
||||||
*,
|
*,
|
||||||
run_id: UUID,
|
run_id: UUID,
|
||||||
parent_run_id: Optional[UUID] = None,
|
parent_run_id: UUID | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
if meta := self.metadata.pop(run_id, None):
|
if meta := self.metadata.pop(run_id, None):
|
||||||
@@ -210,7 +210,7 @@ class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
|
|||||||
error: BaseException,
|
error: BaseException,
|
||||||
*,
|
*,
|
||||||
run_id: UUID,
|
run_id: UUID,
|
||||||
parent_run_id: Optional[UUID] = None,
|
parent_run_id: UUID | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
self.metadata.pop(run_id, None)
|
self.metadata.pop(run_id, None)
|
||||||
|
|||||||
@@ -2,37 +2,37 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||||
from typing import Any, Generic, Optional, Union
|
from typing import Any, Generic
|
||||||
|
|
||||||
from langchain_core.runnables import Runnable, RunnableConfig
|
from langchain_core.runnables import Runnable, RunnableConfig
|
||||||
from langchain_core.runnables.graph import Graph as DrawableGraph
|
from langchain_core.runnables.graph import Graph as DrawableGraph
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
from langgraph.pregel.types import All, StateSnapshot, StateUpdate, StreamMode
|
from langgraph.pregel.types import All, StateSnapshot, StateUpdate, StreamMode
|
||||||
from langgraph.typing import InputT
|
from langgraph.typing import InputT, OutputT, StateT
|
||||||
|
|
||||||
|
|
||||||
# TODO: remove Runnable inheritance here!
|
# TODO: remove Runnable inheritance here!
|
||||||
class PregelProtocol(Runnable[InputT, Any], Generic[InputT], ABC):
|
class PregelProtocol(Runnable[InputT, Any], Generic[StateT, InputT, OutputT], ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def with_config(
|
def with_config(
|
||||||
self, config: Optional[RunnableConfig] = None, **kwargs: Any
|
self, config: RunnableConfig | None = None, **kwargs: Any
|
||||||
) -> Self: ...
|
) -> Self: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_graph(
|
def get_graph(
|
||||||
self,
|
self,
|
||||||
config: Optional[RunnableConfig] = None,
|
config: RunnableConfig | None = None,
|
||||||
*,
|
*,
|
||||||
xray: Union[int, bool] = False,
|
xray: int | bool = False,
|
||||||
) -> DrawableGraph: ...
|
) -> DrawableGraph: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def aget_graph(
|
async def aget_graph(
|
||||||
self,
|
self,
|
||||||
config: Optional[RunnableConfig] = None,
|
config: RunnableConfig | None = None,
|
||||||
*,
|
*,
|
||||||
xray: Union[int, bool] = False,
|
xray: int | bool = False,
|
||||||
) -> DrawableGraph: ...
|
) -> DrawableGraph: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -50,9 +50,9 @@ class PregelProtocol(Runnable[InputT, Any], Generic[InputT], ABC):
|
|||||||
self,
|
self,
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
*,
|
*,
|
||||||
filter: Optional[dict[str, Any]] = None,
|
filter: dict[str, Any] | None = None,
|
||||||
before: Optional[RunnableConfig] = None,
|
before: RunnableConfig | None = None,
|
||||||
limit: Optional[int] = None,
|
limit: int | None = None,
|
||||||
) -> Iterator[StateSnapshot]: ...
|
) -> Iterator[StateSnapshot]: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -60,9 +60,9 @@ class PregelProtocol(Runnable[InputT, Any], Generic[InputT], ABC):
|
|||||||
self,
|
self,
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
*,
|
*,
|
||||||
filter: Optional[dict[str, Any]] = None,
|
filter: dict[str, Any] | None = None,
|
||||||
before: Optional[RunnableConfig] = None,
|
before: RunnableConfig | None = None,
|
||||||
limit: Optional[int] = None,
|
limit: int | None = None,
|
||||||
) -> AsyncIterator[StateSnapshot]: ...
|
) -> AsyncIterator[StateSnapshot]: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -83,58 +83,58 @@ class PregelProtocol(Runnable[InputT, Any], Generic[InputT], ABC):
|
|||||||
def update_state(
|
def update_state(
|
||||||
self,
|
self,
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
values: Optional[Union[dict[str, Any], Any]],
|
values: dict[str, Any] | Any | None,
|
||||||
as_node: Optional[str] = None,
|
as_node: str | None = None,
|
||||||
) -> RunnableConfig: ...
|
) -> RunnableConfig: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def aupdate_state(
|
async def aupdate_state(
|
||||||
self,
|
self,
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
values: Optional[Union[dict[str, Any], Any]],
|
values: dict[str, Any] | Any | None,
|
||||||
as_node: Optional[str] = None,
|
as_node: str | None = None,
|
||||||
) -> RunnableConfig: ...
|
) -> RunnableConfig: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def stream(
|
def stream(
|
||||||
self,
|
self,
|
||||||
input: InputT,
|
input: InputT,
|
||||||
config: Optional[RunnableConfig] = None,
|
config: RunnableConfig | None = None,
|
||||||
*,
|
*,
|
||||||
stream_mode: Optional[Union[StreamMode, list[StreamMode]]] = None,
|
stream_mode: StreamMode | list[StreamMode] | None = None,
|
||||||
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
|
interrupt_before: All | Sequence[str] | None = None,
|
||||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
interrupt_after: All | Sequence[str] | None = None,
|
||||||
subgraphs: bool = False,
|
subgraphs: bool = False,
|
||||||
) -> Iterator[Union[dict[str, Any], Any]]: ...
|
) -> Iterator[dict[str, Any] | Any]: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def astream(
|
def astream(
|
||||||
self,
|
self,
|
||||||
input: InputT,
|
input: InputT,
|
||||||
config: Optional[RunnableConfig] = None,
|
config: RunnableConfig | None = None,
|
||||||
*,
|
*,
|
||||||
stream_mode: Optional[Union[StreamMode, list[StreamMode]]] = None,
|
stream_mode: StreamMode | list[StreamMode] | None = None,
|
||||||
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
|
interrupt_before: All | Sequence[str] | None = None,
|
||||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
interrupt_after: All | Sequence[str] | None = None,
|
||||||
subgraphs: bool = False,
|
subgraphs: bool = False,
|
||||||
) -> AsyncIterator[Union[dict[str, Any], Any]]: ...
|
) -> AsyncIterator[dict[str, Any] | Any]: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def invoke(
|
def invoke(
|
||||||
self,
|
self,
|
||||||
input: InputT,
|
input: InputT,
|
||||||
config: Optional[RunnableConfig] = None,
|
config: RunnableConfig | None = None,
|
||||||
*,
|
*,
|
||||||
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
|
interrupt_before: All | Sequence[str] | None = None,
|
||||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
interrupt_after: All | Sequence[str] | None = None,
|
||||||
) -> Union[dict[str, Any], Any]: ...
|
) -> dict[str, Any] | Any: ...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def ainvoke(
|
async def ainvoke(
|
||||||
self,
|
self,
|
||||||
input: InputT,
|
input: InputT,
|
||||||
config: Optional[RunnableConfig] = None,
|
config: RunnableConfig | None = None,
|
||||||
*,
|
*,
|
||||||
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
|
interrupt_before: All | Sequence[str] | None = None,
|
||||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
interrupt_after: All | Sequence[str] | None = None,
|
||||||
) -> Union[dict[str, Any], Any]: ...
|
) -> dict[str, Any] | Any: ...
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import AsyncIterator, Iterator, Sequence
|
from collections.abc import AsyncIterator, Iterator, Sequence
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Literal,
|
Literal,
|
||||||
Optional,
|
|
||||||
Union,
|
|
||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -96,20 +96,20 @@ class RemoteGraph(PregelProtocol):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
assistant_id: str
|
assistant_id: str
|
||||||
name: Optional[str]
|
name: str | None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
assistant_id: str, # graph_id
|
assistant_id: str, # graph_id
|
||||||
/,
|
/,
|
||||||
*,
|
*,
|
||||||
url: Optional[str] = None,
|
url: str | None = None,
|
||||||
api_key: Optional[str] = None,
|
api_key: str | None = None,
|
||||||
headers: Optional[dict[str, str]] = None,
|
headers: dict[str, str] | None = None,
|
||||||
client: Optional[LangGraphClient] = None,
|
client: LangGraphClient | None = None,
|
||||||
sync_client: Optional[SyncLangGraphClient] = None,
|
sync_client: SyncLangGraphClient | None = None,
|
||||||
config: Optional[RunnableConfig] = None,
|
config: RunnableConfig | None = None,
|
||||||
name: Optional[str] = None,
|
name: str | None = None,
|
||||||
):
|
):
|
||||||
"""Specify `url`, `api_key`, and/or `headers` to create default sync and async clients.
|
"""Specify `url`, `api_key`, and/or `headers` to create default sync and async clients.
|
||||||
|
|
||||||
@@ -162,9 +162,7 @@ class RemoteGraph(PregelProtocol):
|
|||||||
attrs = {**self.__dict__, **update}
|
attrs = {**self.__dict__, **update}
|
||||||
return self.__class__(attrs.pop("assistant_id"), **attrs)
|
return self.__class__(attrs.pop("assistant_id"), **attrs)
|
||||||
|
|
||||||
def with_config(
|
def with_config(self, config: RunnableConfig | None = None, **kwargs: Any) -> Self:
|
||||||
self, config: Optional[RunnableConfig] = None, **kwargs: Any
|
|
||||||
) -> Self:
|
|
||||||
return self.copy(
|
return self.copy(
|
||||||
{"config": merge_configs(self.config, config, cast(RunnableConfig, kwargs))}
|
{"config": merge_configs(self.config, config, cast(RunnableConfig, kwargs))}
|
||||||
)
|
)
|
||||||
@@ -195,9 +193,9 @@ class RemoteGraph(PregelProtocol):
|
|||||||
|
|
||||||
def get_graph(
|
def get_graph(
|
||||||
self,
|
self,
|
||||||
config: Optional[RunnableConfig] = None,
|
config: RunnableConfig | None = None,
|
||||||
*,
|
*,
|
||||||
xray: Union[int, bool] = False,
|
xray: int | bool = False,
|
||||||
) -> DrawableGraph:
|
) -> DrawableGraph:
|
||||||
"""Get graph by graph name.
|
"""Get graph by graph name.
|
||||||
|
|
||||||
@@ -224,9 +222,9 @@ class RemoteGraph(PregelProtocol):
|
|||||||
|
|
||||||
async def aget_graph(
|
async def aget_graph(
|
||||||
self,
|
self,
|
||||||
config: Optional[RunnableConfig] = None,
|
config: RunnableConfig | None = None,
|
||||||
*,
|
*,
|
||||||
xray: Union[int, bool] = False,
|
xray: int | bool = False,
|
||||||
) -> DrawableGraph:
|
) -> DrawableGraph:
|
||||||
"""Get graph by graph name.
|
"""Get graph by graph name.
|
||||||
|
|
||||||
@@ -309,7 +307,7 @@ class RemoteGraph(PregelProtocol):
|
|||||||
interrupts=tuple([i for task in tasks for i in task.interrupts]),
|
interrupts=tuple([i for task in tasks for i in task.interrupts]),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_checkpoint(self, config: Optional[RunnableConfig]) -> Optional[Checkpoint]:
|
def _get_checkpoint(self, config: RunnableConfig | None) -> Checkpoint | None:
|
||||||
if config is None:
|
if config is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -423,9 +421,9 @@ class RemoteGraph(PregelProtocol):
|
|||||||
self,
|
self,
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
*,
|
*,
|
||||||
filter: Optional[dict[str, Any]] = None,
|
filter: dict[str, Any] | None = None,
|
||||||
before: Optional[RunnableConfig] = None,
|
before: RunnableConfig | None = None,
|
||||||
limit: Optional[int] = None,
|
limit: int | None = None,
|
||||||
) -> Iterator[StateSnapshot]:
|
) -> Iterator[StateSnapshot]:
|
||||||
"""Get the state history of a thread.
|
"""Get the state history of a thread.
|
||||||
|
|
||||||
@@ -458,9 +456,9 @@ class RemoteGraph(PregelProtocol):
|
|||||||
self,
|
self,
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
*,
|
*,
|
||||||
filter: Optional[dict[str, Any]] = None,
|
filter: dict[str, Any] | None = None,
|
||||||
before: Optional[RunnableConfig] = None,
|
before: RunnableConfig | None = None,
|
||||||
limit: Optional[int] = None,
|
limit: int | None = None,
|
||||||
) -> AsyncIterator[StateSnapshot]:
|
) -> AsyncIterator[StateSnapshot]:
|
||||||
"""Get the state history of a thread.
|
"""Get the state history of a thread.
|
||||||
|
|
||||||
@@ -492,22 +490,22 @@ class RemoteGraph(PregelProtocol):
|
|||||||
def bulk_update_state(
|
def bulk_update_state(
|
||||||
self,
|
self,
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
updates: list[tuple[Optional[dict[str, Any]], Optional[str]]],
|
updates: list[tuple[dict[str, Any] | None, str | None]],
|
||||||
) -> RunnableConfig:
|
) -> RunnableConfig:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def abulk_update_state(
|
async def abulk_update_state(
|
||||||
self,
|
self,
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
updates: list[tuple[Optional[dict[str, Any]], Optional[str]]],
|
updates: list[tuple[dict[str, Any] | None, str | None]],
|
||||||
) -> RunnableConfig:
|
) -> RunnableConfig:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def update_state(
|
def update_state(
|
||||||
self,
|
self,
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
values: Optional[Union[dict[str, Any], Any]],
|
values: dict[str, Any] | Any | None,
|
||||||
as_node: Optional[str] = None,
|
as_node: str | None = None,
|
||||||
) -> RunnableConfig:
|
) -> RunnableConfig:
|
||||||
"""Update the state of a thread.
|
"""Update the state of a thread.
|
||||||
|
|
||||||
@@ -536,8 +534,8 @@ class RemoteGraph(PregelProtocol):
|
|||||||
async def aupdate_state(
|
async def aupdate_state(
|
||||||
self,
|
self,
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
values: Optional[Union[dict[str, Any], Any]],
|
values: dict[str, Any] | Any | None,
|
||||||
as_node: Optional[str] = None,
|
as_node: str | None = None,
|
||||||
) -> RunnableConfig:
|
) -> RunnableConfig:
|
||||||
"""Update the state of a thread.
|
"""Update the state of a thread.
|
||||||
|
|
||||||
@@ -565,12 +563,10 @@ class RemoteGraph(PregelProtocol):
|
|||||||
|
|
||||||
def _get_stream_modes(
|
def _get_stream_modes(
|
||||||
self,
|
self,
|
||||||
stream_mode: Optional[Union[StreamMode, list[StreamMode]]],
|
stream_mode: StreamMode | list[StreamMode] | None,
|
||||||
config: Optional[RunnableConfig],
|
config: RunnableConfig | None,
|
||||||
default: StreamMode = "updates",
|
default: StreamMode = "updates",
|
||||||
) -> tuple[
|
) -> tuple[list[StreamModeSDK], list[StreamModeSDK], bool, StreamProtocol | None]:
|
||||||
list[StreamModeSDK], list[StreamModeSDK], bool, Optional[StreamProtocol]
|
|
||||||
]:
|
|
||||||
"""Return a tuple of the final list of stream modes sent to the
|
"""Return a tuple of the final list of stream modes sent to the
|
||||||
remote graph and a boolean flag indicating if stream mode 'updates'
|
remote graph and a boolean flag indicating if stream mode 'updates'
|
||||||
was present in the original list of stream modes.
|
was present in the original list of stream modes.
|
||||||
@@ -591,7 +587,7 @@ class RemoteGraph(PregelProtocol):
|
|||||||
updated_stream_modes.append(default)
|
updated_stream_modes.append(default)
|
||||||
requested_stream_modes = updated_stream_modes.copy()
|
requested_stream_modes = updated_stream_modes.copy()
|
||||||
# add any from parent graph
|
# add any from parent graph
|
||||||
stream: Optional[StreamProtocol] = (
|
stream: StreamProtocol | None = (
|
||||||
(config or {}).get(CONF, {}).get(CONFIG_KEY_STREAM)
|
(config or {}).get(CONF, {}).get(CONFIG_KEY_STREAM)
|
||||||
)
|
)
|
||||||
if stream:
|
if stream:
|
||||||
@@ -618,15 +614,15 @@ class RemoteGraph(PregelProtocol):
|
|||||||
|
|
||||||
def stream(
|
def stream(
|
||||||
self,
|
self,
|
||||||
input: Union[dict[str, Any], Any],
|
input: dict[str, Any] | Any,
|
||||||
config: Optional[RunnableConfig] = None,
|
config: RunnableConfig | None = None,
|
||||||
*,
|
*,
|
||||||
stream_mode: Optional[Union[StreamMode, list[StreamMode]]] = None,
|
stream_mode: StreamMode | list[StreamMode] | None = None,
|
||||||
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
|
interrupt_before: All | Sequence[str] | None = None,
|
||||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
interrupt_after: All | Sequence[str] | None = None,
|
||||||
subgraphs: bool = False,
|
subgraphs: bool = False,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Iterator[Union[dict[str, Any], Any]]:
|
) -> Iterator[dict[str, Any] | Any]:
|
||||||
"""Create a run and stream the results.
|
"""Create a run and stream the results.
|
||||||
|
|
||||||
This method calls `POST /threads/{thread_id}/runs/stream` if a `thread_id`
|
This method calls `POST /threads/{thread_id}/runs/stream` if a `thread_id`
|
||||||
@@ -652,7 +648,7 @@ class RemoteGraph(PregelProtocol):
|
|||||||
stream_mode, config
|
stream_mode, config
|
||||||
)
|
)
|
||||||
if isinstance(input, Command):
|
if isinstance(input, Command):
|
||||||
command: Optional[CommandSDK] = cast(CommandSDK, asdict(input))
|
command: CommandSDK | None = cast(CommandSDK, asdict(input))
|
||||||
input = None
|
input = None
|
||||||
else:
|
else:
|
||||||
command = None
|
command = None
|
||||||
@@ -717,15 +713,15 @@ class RemoteGraph(PregelProtocol):
|
|||||||
|
|
||||||
async def astream(
|
async def astream(
|
||||||
self,
|
self,
|
||||||
input: Union[dict[str, Any], Any],
|
input: dict[str, Any] | Any,
|
||||||
config: Optional[RunnableConfig] = None,
|
config: RunnableConfig | None = None,
|
||||||
*,
|
*,
|
||||||
stream_mode: Optional[Union[StreamMode, list[StreamMode]]] = None,
|
stream_mode: StreamMode | list[StreamMode] | None = None,
|
||||||
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
|
interrupt_before: All | Sequence[str] | None = None,
|
||||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
interrupt_after: All | Sequence[str] | None = None,
|
||||||
subgraphs: bool = False,
|
subgraphs: bool = False,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> AsyncIterator[Union[dict[str, Any], Any]]:
|
) -> AsyncIterator[dict[str, Any] | Any]:
|
||||||
"""Create a run and stream the results.
|
"""Create a run and stream the results.
|
||||||
|
|
||||||
This method calls `POST /threads/{thread_id}/runs/stream` if a `thread_id`
|
This method calls `POST /threads/{thread_id}/runs/stream` if a `thread_id`
|
||||||
@@ -751,7 +747,7 @@ class RemoteGraph(PregelProtocol):
|
|||||||
stream_mode, config
|
stream_mode, config
|
||||||
)
|
)
|
||||||
if isinstance(input, Command):
|
if isinstance(input, Command):
|
||||||
command: Optional[CommandSDK] = cast(CommandSDK, asdict(input))
|
command: CommandSDK | None = cast(CommandSDK, asdict(input))
|
||||||
input = None
|
input = None
|
||||||
else:
|
else:
|
||||||
command = None
|
command = None
|
||||||
@@ -817,28 +813,28 @@ class RemoteGraph(PregelProtocol):
|
|||||||
async def astream_events(
|
async def astream_events(
|
||||||
self,
|
self,
|
||||||
input: Any,
|
input: Any,
|
||||||
config: Optional[RunnableConfig] = None,
|
config: RunnableConfig | None = None,
|
||||||
*,
|
*,
|
||||||
version: Literal["v1", "v2"],
|
version: Literal["v1", "v2"],
|
||||||
include_names: Optional[Sequence[All]] = None,
|
include_names: Sequence[All] | None = None,
|
||||||
include_types: Optional[Sequence[All]] = None,
|
include_types: Sequence[All] | None = None,
|
||||||
include_tags: Optional[Sequence[All]] = None,
|
include_tags: Sequence[All] | None = None,
|
||||||
exclude_names: Optional[Sequence[All]] = None,
|
exclude_names: Sequence[All] | None = None,
|
||||||
exclude_types: Optional[Sequence[All]] = None,
|
exclude_types: Sequence[All] | None = None,
|
||||||
exclude_tags: Optional[Sequence[All]] = None,
|
exclude_tags: Sequence[All] | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> AsyncIterator[dict[str, Any]]:
|
) -> AsyncIterator[dict[str, Any]]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def invoke(
|
def invoke(
|
||||||
self,
|
self,
|
||||||
input: Union[dict[str, Any], Any],
|
input: dict[str, Any] | Any,
|
||||||
config: Optional[RunnableConfig] = None,
|
config: RunnableConfig | None = None,
|
||||||
*,
|
*,
|
||||||
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
|
interrupt_before: All | Sequence[str] | None = None,
|
||||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
interrupt_after: All | Sequence[str] | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Union[dict[str, Any], Any]:
|
) -> dict[str, Any] | Any:
|
||||||
"""Create a run, wait until it finishes and return the final state.
|
"""Create a run, wait until it finishes and return the final state.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -867,13 +863,13 @@ class RemoteGraph(PregelProtocol):
|
|||||||
|
|
||||||
async def ainvoke(
|
async def ainvoke(
|
||||||
self,
|
self,
|
||||||
input: Union[dict[str, Any], Any],
|
input: dict[str, Any] | Any,
|
||||||
config: Optional[RunnableConfig] = None,
|
config: RunnableConfig | None = None,
|
||||||
*,
|
*,
|
||||||
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
|
interrupt_before: All | Sequence[str] | None = None,
|
||||||
interrupt_after: Optional[Union[All, Sequence[str]]] = None,
|
interrupt_after: All | Sequence[str] | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Union[dict[str, Any], Any]:
|
) -> dict[str, Any] | Any:
|
||||||
"""Create a run, wait until it finishes and return the final state.
|
"""Create a run, wait until it finishes and return the final state.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
@@ -5,7 +7,7 @@ import sys
|
|||||||
import time
|
import time
|
||||||
from collections.abc import Awaitable, Sequence
|
from collections.abc import Awaitable, Sequence
|
||||||
from dataclasses import replace
|
from dataclasses import replace
|
||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable
|
||||||
|
|
||||||
from langgraph.constants import (
|
from langgraph.constants import (
|
||||||
CONF,
|
CONF,
|
||||||
@@ -23,8 +25,8 @@ SUPPORTS_EXC_NOTES = sys.version_info >= (3, 11)
|
|||||||
|
|
||||||
def run_with_retry(
|
def run_with_retry(
|
||||||
task: PregelExecutableTask,
|
task: PregelExecutableTask,
|
||||||
retry_policy: Optional[Sequence[RetryPolicy]],
|
retry_policy: Sequence[RetryPolicy] | None,
|
||||||
configurable: Optional[dict[str, Any]] = None,
|
configurable: dict[str, Any] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Run a task with retries."""
|
"""Run a task with retries."""
|
||||||
retry_policy = task.retry_policy or retry_policy
|
retry_policy = task.retry_policy or retry_policy
|
||||||
@@ -104,12 +106,11 @@ def run_with_retry(
|
|||||||
|
|
||||||
async def arun_with_retry(
|
async def arun_with_retry(
|
||||||
task: PregelExecutableTask,
|
task: PregelExecutableTask,
|
||||||
retry_policy: Optional[Sequence[RetryPolicy]],
|
retry_policy: Sequence[RetryPolicy] | None,
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
match_cached_writes: Optional[
|
match_cached_writes: Callable[[], Awaitable[Sequence[PregelExecutableTask]]]
|
||||||
Callable[[], Awaitable[Sequence[PregelExecutableTask]]]
|
| None = None,
|
||||||
] = None,
|
configurable: dict[str, Any] | None = None,
|
||||||
configurable: Optional[dict[str, Any]] = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Run a task asynchronously with retries."""
|
"""Run a task asynchronously with retries."""
|
||||||
retry_policy = task.retry_policy or retry_policy
|
retry_policy = task.retry_policy or retry_policy
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import threading
|
import threading
|
||||||
@@ -56,16 +58,14 @@ EXCLUDED_FRAME_FNAMES = (
|
|||||||
"concurrent/futures/_base.py",
|
"concurrent/futures/_base.py",
|
||||||
)
|
)
|
||||||
|
|
||||||
SKIP_RERAISE_SET: weakref.WeakSet[Union[concurrent.futures.Future, asyncio.Future]] = (
|
SKIP_RERAISE_SET: weakref.WeakSet[concurrent.futures.Future | asyncio.Future] = (
|
||||||
weakref.WeakSet()
|
weakref.WeakSet()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class FuturesDict(Generic[F, E], dict[F, Optional[PregelExecutableTask]]):
|
class FuturesDict(Generic[F, E], dict[F, Optional[PregelExecutableTask]]):
|
||||||
event: E
|
event: E
|
||||||
callback: weakref.ref[
|
callback: weakref.ref[Callable[[PregelExecutableTask, BaseException | None], None]]
|
||||||
Callable[[PregelExecutableTask, Optional[BaseException]], None]
|
|
||||||
]
|
|
||||||
counter: int
|
counter: int
|
||||||
done: set[F]
|
done: set[F]
|
||||||
lock: threading.Lock
|
lock: threading.Lock
|
||||||
@@ -74,7 +74,7 @@ class FuturesDict(Generic[F, E], dict[F, Optional[PregelExecutableTask]]):
|
|||||||
self,
|
self,
|
||||||
event: E,
|
event: E,
|
||||||
callback: weakref.ref[
|
callback: weakref.ref[
|
||||||
Callable[[PregelExecutableTask, Optional[BaseException]], None]
|
Callable[[PregelExecutableTask, BaseException | None], None]
|
||||||
],
|
],
|
||||||
future_type: type[F],
|
future_type: type[F],
|
||||||
# used for generic typing, newer py supports FutureDict[...](...)
|
# used for generic typing, newer py supports FutureDict[...](...)
|
||||||
@@ -89,7 +89,7 @@ class FuturesDict(Generic[F, E], dict[F, Optional[PregelExecutableTask]]):
|
|||||||
def __setitem__(
|
def __setitem__(
|
||||||
self,
|
self,
|
||||||
key: F,
|
key: F,
|
||||||
value: Optional[PregelExecutableTask],
|
value: PregelExecutableTask | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__setitem__(key, value) # type: ignore[index]
|
super().__setitem__(key, value) # type: ignore[index]
|
||||||
if value is not None:
|
if value is not None:
|
||||||
@@ -124,7 +124,7 @@ class PregelRunner:
|
|||||||
submit: weakref.ref[Submit],
|
submit: weakref.ref[Submit],
|
||||||
put_writes: weakref.ref[Callable[[str, Sequence[tuple[str, Any]]], None]],
|
put_writes: weakref.ref[Callable[[str, Sequence[tuple[str, Any]]], None]],
|
||||||
use_astream: bool = False,
|
use_astream: bool = False,
|
||||||
node_finished: Optional[Callable[[str], None]] = None,
|
node_finished: Callable[[str], None] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.submit = submit
|
self.submit = submit
|
||||||
self.put_writes = put_writes
|
self.put_writes = put_writes
|
||||||
@@ -136,12 +136,12 @@ class PregelRunner:
|
|||||||
tasks: Iterable[PregelExecutableTask],
|
tasks: Iterable[PregelExecutableTask],
|
||||||
*,
|
*,
|
||||||
reraise: bool = True,
|
reraise: bool = True,
|
||||||
timeout: Optional[float] = None,
|
timeout: float | None = None,
|
||||||
retry_policy: Optional[Sequence[RetryPolicy]] = None,
|
retry_policy: Sequence[RetryPolicy] | None = None,
|
||||||
get_waiter: Optional[Callable[[], concurrent.futures.Future[None]]] = None,
|
get_waiter: Callable[[], concurrent.futures.Future[None]] | None = None,
|
||||||
schedule_task: Callable[
|
schedule_task: Callable[
|
||||||
[PregelExecutableTask, int, Optional[Call]],
|
[PregelExecutableTask, int, Call | None],
|
||||||
Optional[PregelExecutableTask],
|
PregelExecutableTask | None,
|
||||||
],
|
],
|
||||||
) -> Iterator[None]:
|
) -> Iterator[None]:
|
||||||
tasks = tuple(tasks)
|
tasks = tuple(tasks)
|
||||||
@@ -268,12 +268,12 @@ class PregelRunner:
|
|||||||
tasks: Iterable[PregelExecutableTask],
|
tasks: Iterable[PregelExecutableTask],
|
||||||
*,
|
*,
|
||||||
reraise: bool = True,
|
reraise: bool = True,
|
||||||
timeout: Optional[float] = None,
|
timeout: float | None = None,
|
||||||
retry_policy: Optional[Sequence[RetryPolicy]] = None,
|
retry_policy: Sequence[RetryPolicy] | None = None,
|
||||||
get_waiter: Optional[Callable[[], asyncio.Future[None]]] = None,
|
get_waiter: Callable[[], asyncio.Future[None]] | None = None,
|
||||||
schedule_task: Callable[
|
schedule_task: Callable[
|
||||||
[PregelExecutableTask, int, Optional[Call]],
|
[PregelExecutableTask, int, Call | None],
|
||||||
Awaitable[Optional[PregelExecutableTask]],
|
Awaitable[PregelExecutableTask | None],
|
||||||
],
|
],
|
||||||
) -> AsyncIterator[None]:
|
) -> AsyncIterator[None]:
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
@@ -415,7 +415,7 @@ class PregelRunner:
|
|||||||
def commit(
|
def commit(
|
||||||
self,
|
self,
|
||||||
task: PregelExecutableTask,
|
task: PregelExecutableTask,
|
||||||
exception: Optional[BaseException],
|
exception: BaseException | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if isinstance(exception, asyncio.CancelledError):
|
if isinstance(exception, asyncio.CancelledError):
|
||||||
# for cancelled tasks, also save error in task,
|
# for cancelled tasks, also save error in task,
|
||||||
@@ -465,8 +465,8 @@ def _should_stop_others(
|
|||||||
|
|
||||||
|
|
||||||
def _exception(
|
def _exception(
|
||||||
fut: Union[concurrent.futures.Future[Any], asyncio.Future[Any]],
|
fut: concurrent.futures.Future[Any] | asyncio.Future[Any],
|
||||||
) -> Optional[BaseException]:
|
) -> BaseException | None:
|
||||||
"""Return the exception from a future, without raising CancelledError."""
|
"""Return the exception from a future, without raising CancelledError."""
|
||||||
if fut.cancelled():
|
if fut.cancelled():
|
||||||
if isinstance(fut, asyncio.Future):
|
if isinstance(fut, asyncio.Future):
|
||||||
@@ -478,14 +478,14 @@ def _exception(
|
|||||||
|
|
||||||
|
|
||||||
def _panic_or_proceed(
|
def _panic_or_proceed(
|
||||||
futs: Union[set[concurrent.futures.Future], set[asyncio.Future]],
|
futs: set[concurrent.futures.Future] | set[asyncio.Future],
|
||||||
*,
|
*,
|
||||||
timeout_exc_cls: type[Exception] = TimeoutError,
|
timeout_exc_cls: type[Exception] = TimeoutError,
|
||||||
panic: bool = True,
|
panic: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Cancel remaining tasks if any failed, re-raise exception if panic is True."""
|
"""Cancel remaining tasks if any failed, re-raise exception if panic is True."""
|
||||||
done: set[Union[concurrent.futures.Future[Any], asyncio.Future[Any]]] = set()
|
done: set[concurrent.futures.Future[Any] | asyncio.Future[Any]] = set()
|
||||||
inflight: set[Union[concurrent.futures.Future[Any], asyncio.Future[Any]]] = set()
|
inflight: set[concurrent.futures.Future[Any] | asyncio.Future[Any]] = set()
|
||||||
for fut in futs:
|
for fut in futs:
|
||||||
if fut.cancelled():
|
if fut.cancelled():
|
||||||
continue
|
continue
|
||||||
@@ -522,22 +522,22 @@ def _panic_or_proceed(
|
|||||||
|
|
||||||
def _call(
|
def _call(
|
||||||
task: weakref.ref[PregelExecutableTask],
|
task: weakref.ref[PregelExecutableTask],
|
||||||
func: Callable[[Any], Union[Awaitable[Any], Any]],
|
func: Callable[[Any], Awaitable[Any] | Any],
|
||||||
input: Any,
|
input: Any,
|
||||||
*,
|
*,
|
||||||
retry_policy: Optional[Sequence[RetryPolicy]] = None,
|
retry_policy: Sequence[RetryPolicy] | None = None,
|
||||||
cache_policy: Optional[CachePolicy] = None,
|
cache_policy: CachePolicy | None = None,
|
||||||
callbacks: Callbacks = None,
|
callbacks: Callbacks = None,
|
||||||
futures: weakref.ref[FuturesDict],
|
futures: weakref.ref[FuturesDict],
|
||||||
schedule_task: Callable[
|
schedule_task: Callable[
|
||||||
[PregelExecutableTask, int, Optional[Call]], Optional[PregelExecutableTask]
|
[PregelExecutableTask, int, Call | None], PregelExecutableTask | None
|
||||||
],
|
],
|
||||||
submit: weakref.ref[Submit],
|
submit: weakref.ref[Submit],
|
||||||
) -> concurrent.futures.Future[Any]:
|
) -> concurrent.futures.Future[Any]:
|
||||||
if asyncio.iscoroutinefunction(func):
|
if asyncio.iscoroutinefunction(func):
|
||||||
raise RuntimeError("In an sync context async tasks cannot be called")
|
raise RuntimeError("In an sync context async tasks cannot be called")
|
||||||
|
|
||||||
fut: Optional[concurrent.futures.Future] = None
|
fut: concurrent.futures.Future | None = None
|
||||||
# schedule PUSH tasks, collect futures
|
# schedule PUSH tasks, collect futures
|
||||||
scratchpad: PregelScratchpad = task().config[CONF][CONFIG_KEY_SCRATCHPAD] # type: ignore[union-attr]
|
scratchpad: PregelScratchpad = task().config[CONF][CONFIG_KEY_SCRATCHPAD] # type: ignore[union-attr]
|
||||||
# schedule the next task, if the callback returns one
|
# schedule the next task, if the callback returns one
|
||||||
@@ -609,22 +609,22 @@ def _call(
|
|||||||
|
|
||||||
def _acall(
|
def _acall(
|
||||||
task: weakref.ref[PregelExecutableTask],
|
task: weakref.ref[PregelExecutableTask],
|
||||||
func: Callable[[Any], Union[Awaitable[Any], Any]],
|
func: Callable[[Any], Awaitable[Any] | Any],
|
||||||
input: Any,
|
input: Any,
|
||||||
*,
|
*,
|
||||||
retry_policy: Optional[Sequence[RetryPolicy]] = None,
|
retry_policy: Sequence[RetryPolicy] | None = None,
|
||||||
cache_policy: Optional[CachePolicy] = None,
|
cache_policy: CachePolicy | None = None,
|
||||||
callbacks: Callbacks = None,
|
callbacks: Callbacks = None,
|
||||||
# injected dependencies
|
# injected dependencies
|
||||||
futures: weakref.ref[FuturesDict],
|
futures: weakref.ref[FuturesDict],
|
||||||
schedule_task: Callable[
|
schedule_task: Callable[
|
||||||
[PregelExecutableTask, int, Optional[Call]],
|
[PregelExecutableTask, int, Call | None],
|
||||||
Awaitable[Optional[PregelExecutableTask]],
|
Awaitable[PregelExecutableTask | None],
|
||||||
],
|
],
|
||||||
submit: weakref.ref[Submit],
|
submit: weakref.ref[Submit],
|
||||||
loop: asyncio.AbstractEventLoop,
|
loop: asyncio.AbstractEventLoop,
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
) -> Union[asyncio.Future[Any], concurrent.futures.Future[Any]]:
|
) -> asyncio.Future[Any] | concurrent.futures.Future[Any]:
|
||||||
# return a chained future to ensure commit() callback is called
|
# return a chained future to ensure commit() callback is called
|
||||||
# before the returned future is resolved, to ensure stream order etc
|
# before the returned future is resolved, to ensure stream order etc
|
||||||
try:
|
try:
|
||||||
@@ -633,8 +633,8 @@ def _acall(
|
|||||||
in_async = False
|
in_async = False
|
||||||
# if in async context return an async future, otherwise return a sync future
|
# if in async context return an async future, otherwise return a sync future
|
||||||
if in_async:
|
if in_async:
|
||||||
fut: Union[asyncio.Future[Any], concurrent.futures.Future[Any]] = (
|
fut: asyncio.Future[Any] | concurrent.futures.Future[Any] = asyncio.Future(
|
||||||
asyncio.Future(loop=loop)
|
loop=loop
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
fut = concurrent.futures.Future()
|
fut = concurrent.futures.Future()
|
||||||
@@ -661,26 +661,26 @@ def _acall(
|
|||||||
|
|
||||||
|
|
||||||
async def _acall_impl(
|
async def _acall_impl(
|
||||||
destination: Union[asyncio.Future[Any], concurrent.futures.Future[Any]],
|
destination: asyncio.Future[Any] | concurrent.futures.Future[Any],
|
||||||
task: weakref.ref[PregelExecutableTask],
|
task: weakref.ref[PregelExecutableTask],
|
||||||
func: Callable[[Any], Union[Awaitable[Any], Any]],
|
func: Callable[[Any], Awaitable[Any] | Any],
|
||||||
input: Any,
|
input: Any,
|
||||||
*,
|
*,
|
||||||
retry_policy: Optional[Sequence[RetryPolicy]] = None,
|
retry_policy: Sequence[RetryPolicy] | None = None,
|
||||||
cache_policy: Optional[CachePolicy] = None,
|
cache_policy: CachePolicy | None = None,
|
||||||
callbacks: Callbacks = None,
|
callbacks: Callbacks = None,
|
||||||
# injected dependencies
|
# injected dependencies
|
||||||
futures: weakref.ref[FuturesDict[asyncio.Future, asyncio.Event]],
|
futures: weakref.ref[FuturesDict[asyncio.Future, asyncio.Event]],
|
||||||
schedule_task: Callable[
|
schedule_task: Callable[
|
||||||
[PregelExecutableTask, int, Optional[Call]],
|
[PregelExecutableTask, int, Call | None],
|
||||||
Awaitable[Optional[PregelExecutableTask]],
|
Awaitable[PregelExecutableTask | None],
|
||||||
],
|
],
|
||||||
submit: weakref.ref[Submit],
|
submit: weakref.ref[Submit],
|
||||||
loop: asyncio.AbstractEventLoop,
|
loop: asyncio.AbstractEventLoop,
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
try:
|
||||||
fut: Optional[asyncio.Future] = None
|
fut: asyncio.Future | None = None
|
||||||
# schedule PUSH tasks, collect futures
|
# schedule PUSH tasks, collect futures
|
||||||
scratchpad: PregelScratchpad = task().config[CONF][CONFIG_KEY_SCRATCHPAD] # type: ignore[union-attr]
|
scratchpad: PregelScratchpad = task().config[CONF][CONFIG_KEY_SCRATCHPAD] # type: ignore[union-attr]
|
||||||
# schedule the next task, if the callback returns one
|
# schedule the next task, if the callback returns one
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import ast
|
import ast
|
||||||
import inspect
|
import inspect
|
||||||
import re
|
import re
|
||||||
import textwrap
|
import textwrap
|
||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable
|
||||||
|
|
||||||
from langchain_core.runnables import RunnableLambda, RunnableSequence
|
from langchain_core.runnables import RunnableLambda, RunnableSequence
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
@@ -30,7 +32,7 @@ def get_new_channel_versions(
|
|||||||
return new_versions
|
return new_versions
|
||||||
|
|
||||||
|
|
||||||
def find_subgraph_pregel(candidate: Runnable) -> Optional[PregelProtocol]:
|
def find_subgraph_pregel(candidate: Runnable) -> PregelProtocol | None:
|
||||||
from langgraph.pregel import Pregel
|
from langgraph.pregel import Pregel
|
||||||
|
|
||||||
candidates: list[Runnable] = [candidate]
|
candidates: list[Runnable] = [candidate]
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
from typing import Any, Optional, Union
|
from typing import Any
|
||||||
|
|
||||||
from langgraph.channels.base import BaseChannel
|
from langgraph.channels.base import BaseChannel
|
||||||
from langgraph.constants import RESERVED
|
from langgraph.constants import RESERVED
|
||||||
@@ -10,11 +12,11 @@ from langgraph.types import All
|
|||||||
def validate_graph(
|
def validate_graph(
|
||||||
nodes: Mapping[str, PregelNode],
|
nodes: Mapping[str, PregelNode],
|
||||||
channels: dict[str, BaseChannel],
|
channels: dict[str, BaseChannel],
|
||||||
input_channels: Union[str, Sequence[str]],
|
input_channels: str | Sequence[str],
|
||||||
output_channels: Union[str, Sequence[str]],
|
output_channels: str | Sequence[str],
|
||||||
stream_channels: Optional[Union[str, Sequence[str]]],
|
stream_channels: str | Sequence[str] | None,
|
||||||
interrupt_after_nodes: Union[All, Sequence[str]],
|
interrupt_after_nodes: All | Sequence[str],
|
||||||
interrupt_before_nodes: Union[All, Sequence[str]],
|
interrupt_before_nodes: All | Sequence[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
for chan in channels:
|
for chan in channels:
|
||||||
if chan in RESERVED:
|
if chan in RESERVED:
|
||||||
@@ -88,7 +90,7 @@ def validate_graph(
|
|||||||
|
|
||||||
|
|
||||||
def validate_keys(
|
def validate_keys(
|
||||||
keys: Optional[Union[str, Sequence[str]]],
|
keys: str | Sequence[str] | None,
|
||||||
channels: Mapping[str, Any],
|
channels: Mapping[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
if isinstance(keys, str):
|
if isinstance(keys, str):
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class ChannelWriteTupleEntry(NamedTuple):
|
|||||||
"""Function to extract tuples from value."""
|
"""Function to extract tuples from value."""
|
||||||
value: Any = PASSTHROUGH
|
value: Any = PASSTHROUGH
|
||||||
"""Value to write, or PASSTHROUGH to use the input."""
|
"""Value to write, or PASSTHROUGH to use the input."""
|
||||||
static: Optional[Sequence[tuple[str, Any, Optional[str]]]] = None
|
static: Sequence[tuple[str, Any, str | None]] | None = None
|
||||||
"""Optional, declared writes for static analysis."""
|
"""Optional, declared writes for static analysis."""
|
||||||
|
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ class ChannelWrite(RunnableCallable):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_static_writes(
|
def get_static_writes(
|
||||||
runnable: Runnable,
|
runnable: Runnable,
|
||||||
) -> Optional[Sequence[tuple[str, Any, Optional[str]]]]:
|
) -> Sequence[tuple[str, Any, str | None]] | None:
|
||||||
"""Used to get conditional writes a writer declares for static analysis."""
|
"""Used to get conditional writes a writer declares for static analysis."""
|
||||||
if isinstance(runnable, ChannelWrite):
|
if isinstance(runnable, ChannelWrite):
|
||||||
return [
|
return [
|
||||||
@@ -160,9 +160,7 @@ class ChannelWrite(RunnableCallable):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def register_writer(
|
def register_writer(
|
||||||
runnable: R,
|
runnable: R,
|
||||||
static: Optional[
|
static: Sequence[tuple[ChannelWriteEntry | Send, str | None]] | None = None,
|
||||||
Sequence[tuple[Union[ChannelWriteEntry, Send], Optional[str]]]
|
|
||||||
] = None,
|
|
||||||
) -> R:
|
) -> R:
|
||||||
"""Used to mark a runnable as a writer, so that it can be detected by is_writer.
|
"""Used to mark a runnable as a writer, so that it can be detected by is_writer.
|
||||||
Instances of ChannelWrite are automatically marked as writers.
|
Instances of ChannelWrite are automatically marked as writers.
|
||||||
@@ -174,7 +172,7 @@ class ChannelWrite(RunnableCallable):
|
|||||||
|
|
||||||
|
|
||||||
def _assemble_writes(
|
def _assemble_writes(
|
||||||
writes: Sequence[Union[ChannelWriteEntry, ChannelWriteTupleEntry, Send]],
|
writes: Sequence[ChannelWriteEntry | ChannelWriteTupleEntry | Send],
|
||||||
) -> list[tuple[str, Any]]:
|
) -> list[tuple[str, Any]]:
|
||||||
"""Assembles the writes into a list of tuples."""
|
"""Assembles the writes into a list of tuples."""
|
||||||
tuples: list[tuple[str, Any]] = []
|
tuples: list[tuple[str, Any]] = []
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import sys
|
import sys
|
||||||
from collections import deque
|
from collections import deque
|
||||||
@@ -10,7 +12,6 @@ from typing import (
|
|||||||
Generic,
|
Generic,
|
||||||
Literal,
|
Literal,
|
||||||
NamedTuple,
|
NamedTuple,
|
||||||
Optional,
|
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
cast,
|
cast,
|
||||||
@@ -115,9 +116,9 @@ class RetryPolicy(NamedTuple):
|
|||||||
"""Maximum number of attempts to make before giving up, including the first."""
|
"""Maximum number of attempts to make before giving up, including the first."""
|
||||||
jitter: bool = True
|
jitter: bool = True
|
||||||
"""Whether to add random jitter to the interval between retries."""
|
"""Whether to add random jitter to the interval between retries."""
|
||||||
retry_on: Union[
|
retry_on: (
|
||||||
type[Exception], Sequence[type[Exception]], Callable[[Exception], bool]
|
type[Exception] | Sequence[type[Exception]] | Callable[[Exception], bool]
|
||||||
] = default_retry_on
|
) = default_retry_on
|
||||||
"""List of exception classes that should trigger a retry, or a callable that returns True for exceptions that should trigger a retry."""
|
"""List of exception classes that should trigger a retry, or a callable that returns True for exceptions that should trigger a retry."""
|
||||||
|
|
||||||
|
|
||||||
@@ -132,7 +133,7 @@ class CachePolicy(Generic[KeyFuncT]):
|
|||||||
"""Function to generate a cache key from the node's input.
|
"""Function to generate a cache key from the node's input.
|
||||||
Defaults to hashing the input with pickle."""
|
Defaults to hashing the input with pickle."""
|
||||||
|
|
||||||
ttl: Optional[int] = None
|
ttl: int | None = None
|
||||||
"""Time to live for the cache entry in seconds. If None, the entry never expires."""
|
"""Time to live for the cache entry in seconds. If None, the entry never expires."""
|
||||||
|
|
||||||
|
|
||||||
@@ -145,7 +146,7 @@ class Interrupt:
|
|||||||
|
|
||||||
value: Any
|
value: Any
|
||||||
resumable: bool = False
|
resumable: bool = False
|
||||||
ns: Optional[Sequence[str]] = None
|
ns: Sequence[str] | None = None
|
||||||
when: Literal["during"] = dataclasses.field(default="during", repr=False)
|
when: Literal["during"] = dataclasses.field(default="during", repr=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -157,8 +158,8 @@ class Interrupt:
|
|||||||
|
|
||||||
|
|
||||||
class StateUpdate(NamedTuple):
|
class StateUpdate(NamedTuple):
|
||||||
values: Optional[dict[str, Any]]
|
values: dict[str, Any] | None
|
||||||
as_node: Optional[str] = None
|
as_node: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class PregelTask(NamedTuple):
|
class PregelTask(NamedTuple):
|
||||||
@@ -166,11 +167,11 @@ class PregelTask(NamedTuple):
|
|||||||
|
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
path: tuple[Union[str, int, tuple], ...]
|
path: tuple[str | int | tuple, ...]
|
||||||
error: Optional[Exception] = None
|
error: Exception | None = None
|
||||||
interrupts: tuple[Interrupt, ...] = ()
|
interrupts: tuple[Interrupt, ...] = ()
|
||||||
state: Union[None, RunnableConfig, "StateSnapshot"] = None
|
state: None | RunnableConfig | StateSnapshot = None
|
||||||
result: Optional[Any] = None
|
result: Any | None = None
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info > (3, 11):
|
if sys.version_info > (3, 11):
|
||||||
@@ -186,7 +187,7 @@ class CacheKey(NamedTuple):
|
|||||||
"""Namespace for the cache entry."""
|
"""Namespace for the cache entry."""
|
||||||
key: str
|
key: str
|
||||||
"""Key for the cache entry."""
|
"""Key for the cache entry."""
|
||||||
ttl: Optional[int]
|
ttl: int | None
|
||||||
"""Time to live for the cache entry in seconds."""
|
"""Time to live for the cache entry in seconds."""
|
||||||
|
|
||||||
|
|
||||||
@@ -199,28 +200,28 @@ class PregelExecutableTask:
|
|||||||
config: RunnableConfig
|
config: RunnableConfig
|
||||||
triggers: Sequence[str]
|
triggers: Sequence[str]
|
||||||
retry_policy: Sequence[RetryPolicy]
|
retry_policy: Sequence[RetryPolicy]
|
||||||
cache_key: Optional[CacheKey]
|
cache_key: CacheKey | None
|
||||||
id: str
|
id: str
|
||||||
path: tuple[Union[str, int, tuple], ...]
|
path: tuple[str | int | tuple, ...]
|
||||||
scheduled: bool = False
|
scheduled: bool = False
|
||||||
writers: Sequence[Runnable] = ()
|
writers: Sequence[Runnable] = ()
|
||||||
subgraphs: Sequence["PregelProtocol"] = ()
|
subgraphs: Sequence[PregelProtocol] = ()
|
||||||
|
|
||||||
|
|
||||||
class StateSnapshot(NamedTuple):
|
class StateSnapshot(NamedTuple):
|
||||||
"""Snapshot of the state of the graph at the beginning of a step."""
|
"""Snapshot of the state of the graph at the beginning of a step."""
|
||||||
|
|
||||||
values: Union[dict[str, Any], Any]
|
values: dict[str, Any] | Any
|
||||||
"""Current values of channels."""
|
"""Current values of channels."""
|
||||||
next: tuple[str, ...]
|
next: tuple[str, ...]
|
||||||
"""The name of the node to execute in each task for this step."""
|
"""The name of the node to execute in each task for this step."""
|
||||||
config: RunnableConfig
|
config: RunnableConfig
|
||||||
"""Config used to fetch this snapshot."""
|
"""Config used to fetch this snapshot."""
|
||||||
metadata: Optional[CheckpointMetadata]
|
metadata: CheckpointMetadata | None
|
||||||
"""Metadata associated with this snapshot."""
|
"""Metadata associated with this snapshot."""
|
||||||
created_at: Optional[str]
|
created_at: str | None
|
||||||
"""Timestamp of snapshot creation."""
|
"""Timestamp of snapshot creation."""
|
||||||
parent_config: Optional[RunnableConfig]
|
parent_config: RunnableConfig | None
|
||||||
"""Config used to fetch the parent snapshot, if any."""
|
"""Config used to fetch the parent snapshot, if any."""
|
||||||
tasks: tuple[PregelTask, ...]
|
tasks: tuple[PregelTask, ...]
|
||||||
"""Tasks to execute in this step. If already attempted, may contain an error."""
|
"""Tasks to execute in this step. If already attempted, may contain an error."""
|
||||||
@@ -327,10 +328,10 @@ class Command(Generic[N], ToolOutputMixin):
|
|||||||
- sequence of `Send` objects
|
- sequence of `Send` objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
graph: Optional[str] = None
|
graph: str | None = None
|
||||||
update: Optional[Any] = None
|
update: Any | None = None
|
||||||
resume: Optional[Union[dict[str, Any], Any]] = None
|
resume: dict[str, Any] | Any | None = None
|
||||||
goto: Union[Send, Sequence[Union[Send, N]], N] = ()
|
goto: Send | Sequence[Send | N] | N = ()
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
# get all non-None values
|
# get all non-None values
|
||||||
|
|||||||
@@ -1,57 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import Field
|
from typing import Union
|
||||||
from typing import (
|
|
||||||
Any,
|
|
||||||
ClassVar,
|
|
||||||
Protocol,
|
|
||||||
Union,
|
|
||||||
)
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from typing_extensions import TypeVar
|
||||||
from typing_extensions import TypeAlias, TypedDict, TypeVar
|
|
||||||
|
|
||||||
|
from langgraph._typing import StateLike
|
||||||
class _TypedDictLikeV1(Protocol):
|
|
||||||
"""Protocol to represent types that behave like TypedDicts
|
|
||||||
|
|
||||||
Version 1: using `ClassVar` for keys."""
|
|
||||||
|
|
||||||
__required_keys__: ClassVar[frozenset[str]]
|
|
||||||
__optional_keys__: ClassVar[frozenset[str]]
|
|
||||||
|
|
||||||
|
|
||||||
class _TypedDictLikeV2(Protocol):
|
|
||||||
"""Protocol to represent types that behave like TypedDicts
|
|
||||||
|
|
||||||
Version 2: not using `ClassVar` for keys."""
|
|
||||||
|
|
||||||
__required_keys__: frozenset[str]
|
|
||||||
__optional_keys__: frozenset[str]
|
|
||||||
|
|
||||||
|
|
||||||
class _DataclassLike(Protocol):
|
|
||||||
"""Protocol to represent types that behave like dataclasses.
|
|
||||||
|
|
||||||
Inspired by the private _DataclassT from dataclasses that uses a similar protocol as a bound."""
|
|
||||||
|
|
||||||
__dataclass_fields__: ClassVar[dict[str, Field[Any]]]
|
|
||||||
|
|
||||||
|
|
||||||
StateLike: TypeAlias = Union[
|
|
||||||
_TypedDictLikeV1, _TypedDictLikeV2, _DataclassLike, BaseModel
|
|
||||||
]
|
|
||||||
"""Type alias for state-like types.
|
|
||||||
|
|
||||||
It can either be a `TypedDict`, `dataclass`, or Pydantic `BaseModel`.
|
|
||||||
Note: we cannot use either `TypedDict` or `dataclass` directly due to limitations in type checking."""
|
|
||||||
|
|
||||||
|
|
||||||
class Unset:
|
|
||||||
"""Sentinel representing an unset value."""
|
|
||||||
|
|
||||||
|
|
||||||
UNSET = Unset()
|
|
||||||
|
|
||||||
StateT = TypeVar("StateT", bound=StateLike)
|
StateT = TypeVar("StateT", bound=StateLike)
|
||||||
"""Type variable used to represent the state in a graph."""
|
"""Type variable used to represent the state in a graph."""
|
||||||
@@ -60,15 +13,18 @@ StateT_co = TypeVar("StateT_co", bound=StateLike, covariant=True)
|
|||||||
|
|
||||||
StateT_contra = TypeVar("StateT_contra", bound=StateLike, contravariant=True)
|
StateT_contra = TypeVar("StateT_contra", bound=StateLike, contravariant=True)
|
||||||
|
|
||||||
InputT = TypeVar("InputT", bound=Union[StateLike, Unset], default=Unset)
|
InputT = TypeVar("InputT", bound=StateLike, default=StateT)
|
||||||
"""Type variable used to represent the input to a graph.
|
"""Type variable used to represent the input to a state graph.
|
||||||
|
|
||||||
In practice, InputT might be represented by either the `input_type` or `state_type` of a graph.
|
Defaults to `StateT`.
|
||||||
If `input_type` is not specified, it defaults to `StateType`."""
|
"""
|
||||||
|
|
||||||
OutputT = TypeVar("OutputT", bound=Union[StateLike, Unset], default=Unset)
|
ResolvedInputT = TypeVar("ResolvedInputT", bound=StateLike)
|
||||||
"""Type variable used to represent the output of a graph."""
|
"""Type variable used to represent the resolved input to a state graph.
|
||||||
|
|
||||||
|
No default.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class DeprecatedKwargs(TypedDict):
|
OutputT = TypeVar("OutputT", bound=Union[StateLike, None], default=StateT)
|
||||||
"""TypedDict to use for extra keyword arguments, enabling type checking warnings for deprecated arguments."""
|
"""Type variable used to represent the output of a state graph."""
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import ChainMap
|
from collections import ChainMap
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from os import getenv
|
from os import getenv
|
||||||
from typing import Any, Optional, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from langchain_core.callbacks import (
|
from langchain_core.callbacks import (
|
||||||
AsyncCallbackManager,
|
AsyncCallbackManager,
|
||||||
@@ -45,7 +47,7 @@ def recast_checkpoint_ns(ns: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def patch_configurable(
|
def patch_configurable(
|
||||||
config: Optional[RunnableConfig], patch: dict[str, Any]
|
config: RunnableConfig | None, patch: dict[str, Any]
|
||||||
) -> RunnableConfig:
|
) -> RunnableConfig:
|
||||||
if config is None:
|
if config is None:
|
||||||
return {CONF: patch}
|
return {CONF: patch}
|
||||||
@@ -56,7 +58,7 @@ def patch_configurable(
|
|||||||
|
|
||||||
|
|
||||||
def patch_checkpoint_map(
|
def patch_checkpoint_map(
|
||||||
config: Optional[RunnableConfig], metadata: Optional[CheckpointMetadata]
|
config: RunnableConfig | None, metadata: CheckpointMetadata | None
|
||||||
) -> RunnableConfig:
|
) -> RunnableConfig:
|
||||||
if config is None:
|
if config is None:
|
||||||
return config
|
return config
|
||||||
@@ -75,7 +77,7 @@ def patch_checkpoint_map(
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def merge_configs(*configs: Optional[RunnableConfig]) -> RunnableConfig:
|
def merge_configs(*configs: RunnableConfig | None) -> RunnableConfig:
|
||||||
"""Merge multiple configs into one.
|
"""Merge multiple configs into one.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -148,13 +150,13 @@ def merge_configs(*configs: Optional[RunnableConfig]) -> RunnableConfig:
|
|||||||
|
|
||||||
|
|
||||||
def patch_config(
|
def patch_config(
|
||||||
config: Optional[RunnableConfig],
|
config: RunnableConfig | None,
|
||||||
*,
|
*,
|
||||||
callbacks: Callbacks = None,
|
callbacks: Callbacks = None,
|
||||||
recursion_limit: Optional[int] = None,
|
recursion_limit: int | None = None,
|
||||||
max_concurrency: Optional[int] = None,
|
max_concurrency: int | None = None,
|
||||||
run_name: Optional[str] = None,
|
run_name: str | None = None,
|
||||||
configurable: Optional[dict[str, Any]] = None,
|
configurable: dict[str, Any] | None = None,
|
||||||
) -> RunnableConfig:
|
) -> RunnableConfig:
|
||||||
"""Patch a config with new values.
|
"""Patch a config with new values.
|
||||||
|
|
||||||
@@ -194,7 +196,7 @@ def patch_config(
|
|||||||
|
|
||||||
|
|
||||||
def get_callback_manager_for_config(
|
def get_callback_manager_for_config(
|
||||||
config: RunnableConfig, tags: Optional[Sequence[str]] = None
|
config: RunnableConfig, tags: Sequence[str] | None = None
|
||||||
) -> CallbackManager:
|
) -> CallbackManager:
|
||||||
"""Get a callback manager for a config.
|
"""Get a callback manager for a config.
|
||||||
|
|
||||||
@@ -232,7 +234,7 @@ def get_callback_manager_for_config(
|
|||||||
|
|
||||||
def get_async_callback_manager_for_config(
|
def get_async_callback_manager_for_config(
|
||||||
config: RunnableConfig,
|
config: RunnableConfig,
|
||||||
tags: Optional[Sequence[str]] = None,
|
tags: Sequence[str] | None = None,
|
||||||
) -> AsyncCallbackManager:
|
) -> AsyncCallbackManager:
|
||||||
"""Get an async callback manager for a config.
|
"""Get an async callback manager for a config.
|
||||||
|
|
||||||
@@ -275,7 +277,7 @@ def _is_not_empty(value: Any) -> bool:
|
|||||||
return value is not None
|
return value is not None
|
||||||
|
|
||||||
|
|
||||||
def ensure_config(*configs: Optional[RunnableConfig]) -> RunnableConfig:
|
def ensure_config(*configs: RunnableConfig | None) -> RunnableConfig:
|
||||||
"""Return a config with all keys, merging any provided configs.
|
"""Return a config with all keys, merging any provided configs.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import types
|
import types
|
||||||
import weakref
|
import weakref
|
||||||
@@ -31,7 +33,7 @@ def _is_optional_type(type_: Any) -> bool:
|
|||||||
return type_ is None
|
return type_ is None
|
||||||
|
|
||||||
|
|
||||||
def _is_required_type(type_: Any) -> Optional[bool]:
|
def _is_required_type(type_: Any) -> bool | None:
|
||||||
"""Check if an annotation is marked as Required/NotRequired.
|
"""Check if an annotation is marked as Required/NotRequired.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -118,7 +120,7 @@ def get_field_default(name: str, type_: Any, schema: type[Any]) -> Any:
|
|||||||
|
|
||||||
def get_enhanced_type_hints(
|
def get_enhanced_type_hints(
|
||||||
type: type[Any],
|
type: type[Any],
|
||||||
) -> Generator[tuple[str, Any, Any, Optional[str]], None, None]:
|
) -> Generator[tuple[str, Any, Any, str | None], None, None]:
|
||||||
"""Attempt to extract default values and descriptions from provided type, used for config schema."""
|
"""Attempt to extract default values and descriptions from provided type, used for config schema."""
|
||||||
for name, typ in get_type_hints(type).items():
|
for name, typ in get_type_hints(type).items():
|
||||||
default = None
|
default = None
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import contextvars
|
import contextvars
|
||||||
@@ -5,7 +7,7 @@ import inspect
|
|||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
from collections.abc import Awaitable, Coroutine, Generator
|
from collections.abc import Awaitable, Coroutine, Generator
|
||||||
from typing import Optional, TypeVar, Union, cast
|
from typing import TypeVar, Union, cast
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
AnyFuture = Union[asyncio.Future, concurrent.futures.Future]
|
AnyFuture = Union[asyncio.Future, concurrent.futures.Future]
|
||||||
@@ -139,11 +141,11 @@ def chain_future(source: AnyFuture, destination: AnyFuture) -> AnyFuture:
|
|||||||
|
|
||||||
|
|
||||||
def _ensure_future(
|
def _ensure_future(
|
||||||
coro_or_future: Union[Coroutine[None, None, T], Awaitable[T]],
|
coro_or_future: Coroutine[None, None, T] | Awaitable[T],
|
||||||
*,
|
*,
|
||||||
loop: asyncio.AbstractEventLoop,
|
loop: asyncio.AbstractEventLoop,
|
||||||
name: Optional[str] = None,
|
name: str | None = None,
|
||||||
context: Optional[contextvars.Context] = None,
|
context: contextvars.Context | None = None,
|
||||||
lazy: bool = True,
|
lazy: bool = True,
|
||||||
) -> asyncio.Task[T]:
|
) -> asyncio.Task[T]:
|
||||||
called_wrap_awaitable = False
|
called_wrap_awaitable = False
|
||||||
@@ -189,8 +191,8 @@ def run_coroutine_threadsafe(
|
|||||||
loop: asyncio.AbstractEventLoop,
|
loop: asyncio.AbstractEventLoop,
|
||||||
*,
|
*,
|
||||||
lazy: bool,
|
lazy: bool,
|
||||||
name: Optional[str] = None,
|
name: str | None = None,
|
||||||
context: Optional[contextvars.Context] = None,
|
context: contextvars.Context | None = None,
|
||||||
) -> asyncio.Future[T]:
|
) -> asyncio.Future[T]:
|
||||||
"""Submit a coroutine object to a given event loop.
|
"""Submit a coroutine object to a given event loop.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import typing
|
import typing
|
||||||
import warnings
|
import warnings
|
||||||
@@ -6,8 +8,6 @@ from dataclasses import is_dataclass
|
|||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Optional,
|
|
||||||
Union,
|
|
||||||
cast,
|
cast,
|
||||||
overload,
|
overload,
|
||||||
)
|
)
|
||||||
@@ -39,7 +39,7 @@ def get_fields(model: BaseModel) -> dict[str, FieldInfo]: ...
|
|||||||
|
|
||||||
|
|
||||||
def get_fields(
|
def get_fields(
|
||||||
model: Union[type[BaseModel], BaseModel],
|
model: type[BaseModel] | BaseModel,
|
||||||
) -> dict[str, FieldInfo]:
|
) -> dict[str, FieldInfo]:
|
||||||
"""Get the field names of a Pydantic model."""
|
"""Get the field names of a Pydantic model."""
|
||||||
if hasattr(model, "model_fields"):
|
if hasattr(model, "model_fields"):
|
||||||
@@ -61,7 +61,7 @@ NO_DEFAULT = object()
|
|||||||
def _create_root_model(
|
def _create_root_model(
|
||||||
name: str,
|
name: str,
|
||||||
type_: Any,
|
type_: Any,
|
||||||
module_name: Optional[str] = None,
|
module_name: str | None = None,
|
||||||
default_: object = NO_DEFAULT,
|
default_: object = NO_DEFAULT,
|
||||||
) -> type[BaseModel]:
|
) -> type[BaseModel]:
|
||||||
"""Create a base class."""
|
"""Create a base class."""
|
||||||
@@ -115,7 +115,7 @@ def _create_root_model_cached(
|
|||||||
model_name: str,
|
model_name: str,
|
||||||
type_: Any,
|
type_: Any,
|
||||||
*,
|
*,
|
||||||
module_name: Optional[str] = None,
|
module_name: str | None = None,
|
||||||
default_: object = NO_DEFAULT,
|
default_: object = NO_DEFAULT,
|
||||||
) -> type[BaseModel]:
|
) -> type[BaseModel]:
|
||||||
return _create_root_model(
|
return _create_root_model(
|
||||||
@@ -181,8 +181,8 @@ def _remap_field_definitions(field_definitions: dict[str, Any]) -> dict[str, Any
|
|||||||
def create_model(
|
def create_model(
|
||||||
model_name: str,
|
model_name: str,
|
||||||
*,
|
*,
|
||||||
field_definitions: Optional[dict[str, Any]] = None,
|
field_definitions: dict[str, Any] | None = None,
|
||||||
root: Optional[Any] = None,
|
root: Any | None = None,
|
||||||
) -> type[BaseModel]:
|
) -> type[BaseModel]:
|
||||||
"""Create a pydantic model with the given field definitions.
|
"""Create a pydantic model with the given field definitions.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# type: ignore
|
# type: ignore
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import queue
|
import queue
|
||||||
@@ -7,7 +8,6 @@ import threading
|
|||||||
import types
|
import types
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from time import monotonic
|
from time import monotonic
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
PY_310 = sys.version_info >= (3, 10)
|
PY_310 = sys.version_info >= (3, 10)
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ class AsyncQueue(asyncio.Queue):
|
|||||||
class Semaphore(threading.Semaphore):
|
class Semaphore(threading.Semaphore):
|
||||||
"""Semaphore subclass with a wait() method."""
|
"""Semaphore subclass with a wait() method."""
|
||||||
|
|
||||||
def wait(self, blocking: bool = True, timeout: Optional[float] = None):
|
def wait(self, blocking: bool = True, timeout: float | None = None):
|
||||||
"""Block until the semaphore can be acquired, but don't acquire it."""
|
"""Block until the semaphore can be acquired, but don't acquire it."""
|
||||||
if not blocking and timeout is not None:
|
if not blocking and timeout is not None:
|
||||||
raise ValueError("can't specify timeout for non-blocking acquire")
|
raise ValueError("can't specify timeout for non-blocking acquire")
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import enum
|
import enum
|
||||||
import inspect
|
import inspect
|
||||||
@@ -63,7 +65,7 @@ except ImportError:
|
|||||||
|
|
||||||
def _set_config_context(
|
def _set_config_context(
|
||||||
config: RunnableConfig, run: Any = None
|
config: RunnableConfig, run: Any = None
|
||||||
) -> Token[Optional[RunnableConfig]]:
|
) -> Token[RunnableConfig | None]:
|
||||||
"""Set the child Runnable config + tracing context.
|
"""Set the child Runnable config + tracing context.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -77,9 +79,7 @@ def _set_config_context(
|
|||||||
return config_token
|
return config_token
|
||||||
|
|
||||||
|
|
||||||
def _unset_config_context(
|
def _unset_config_context(token: Token[RunnableConfig | None], run: Any = None) -> None:
|
||||||
token: Token[Optional[RunnableConfig]], run: Any = None
|
|
||||||
) -> None:
|
|
||||||
"""Set the child Runnable config + tracing context.
|
"""Set the child Runnable config + tracing context.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -242,15 +242,15 @@ class RunnableCallable(Runnable):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
func: Optional[Callable[..., Union[Any, Runnable]]],
|
func: Callable[..., Any | Runnable] | None,
|
||||||
afunc: Optional[Callable[..., Awaitable[Union[Any, Runnable]]]] = None,
|
afunc: Callable[..., Awaitable[Any | Runnable]] | None = None,
|
||||||
*,
|
*,
|
||||||
name: Optional[str] = None,
|
name: str | None = None,
|
||||||
tags: Optional[Sequence[str]] = None,
|
tags: Sequence[str] | None = None,
|
||||||
trace: bool = True,
|
trace: bool = True,
|
||||||
recurse: bool = True,
|
recurse: bool = True,
|
||||||
explode_args: bool = False,
|
explode_args: bool = False,
|
||||||
func_accepts_config: Optional[bool] = None,
|
func_accepts_config: bool | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -312,7 +312,7 @@ class RunnableCallable(Runnable):
|
|||||||
return f"{self.get_name()}({', '.join(f'{k}={v!r}' for k, v in repr_args.items())})"
|
return f"{self.get_name()}({', '.join(f'{k}={v!r}' for k, v in repr_args.items())})"
|
||||||
|
|
||||||
def invoke(
|
def invoke(
|
||||||
self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any
|
self, input: Any, config: RunnableConfig | None = None, **kwargs: Any
|
||||||
) -> Any:
|
) -> Any:
|
||||||
if self.func is None:
|
if self.func is None:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
@@ -380,7 +380,7 @@ class RunnableCallable(Runnable):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
async def ainvoke(
|
async def ainvoke(
|
||||||
self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any
|
self, input: Any, config: RunnableConfig | None = None, **kwargs: Any
|
||||||
) -> Any:
|
) -> Any:
|
||||||
if not self.afunc:
|
if not self.afunc:
|
||||||
return self.invoke(input, config)
|
return self.invoke(input, config)
|
||||||
@@ -466,7 +466,7 @@ def is_async_generator(
|
|||||||
|
|
||||||
|
|
||||||
def coerce_to_runnable(
|
def coerce_to_runnable(
|
||||||
thing: RunnableLike, *, name: Optional[str], trace: bool
|
thing: RunnableLike, *, name: str | None, trace: bool
|
||||||
) -> Runnable:
|
) -> Runnable:
|
||||||
"""Coerce a runnable-like object into a Runnable.
|
"""Coerce a runnable-like object into a Runnable.
|
||||||
|
|
||||||
@@ -509,8 +509,8 @@ class RunnableSeq(Runnable):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*steps: RunnableLike,
|
*steps: RunnableLike,
|
||||||
name: Optional[str] = None,
|
name: str | None = None,
|
||||||
trace_inputs: Optional[Callable[[Any], Any]] = None,
|
trace_inputs: Callable[[Any], Any] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create a new RunnableSeq.
|
"""Create a new RunnableSeq.
|
||||||
|
|
||||||
@@ -588,7 +588,7 @@ class RunnableSeq(Runnable):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def invoke(
|
def invoke(
|
||||||
self, input: Input, config: Optional[RunnableConfig] = None, **kwargs: Any
|
self, input: Input, config: RunnableConfig | None = None, **kwargs: Any
|
||||||
) -> Any:
|
) -> Any:
|
||||||
if config is None:
|
if config is None:
|
||||||
config = ensure_config()
|
config = ensure_config()
|
||||||
@@ -634,8 +634,8 @@ class RunnableSeq(Runnable):
|
|||||||
async def ainvoke(
|
async def ainvoke(
|
||||||
self,
|
self,
|
||||||
input: Input,
|
input: Input,
|
||||||
config: Optional[RunnableConfig] = None,
|
config: RunnableConfig | None = None,
|
||||||
**kwargs: Optional[Any],
|
**kwargs: Any | None,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
if config is None:
|
if config is None:
|
||||||
config = ensure_config()
|
config = ensure_config()
|
||||||
@@ -687,8 +687,8 @@ class RunnableSeq(Runnable):
|
|||||||
def stream(
|
def stream(
|
||||||
self,
|
self,
|
||||||
input: Input,
|
input: Input,
|
||||||
config: Optional[RunnableConfig] = None,
|
config: RunnableConfig | None = None,
|
||||||
**kwargs: Optional[Any],
|
**kwargs: Any | None,
|
||||||
) -> Iterator[Any]:
|
) -> Iterator[Any]:
|
||||||
if config is None:
|
if config is None:
|
||||||
config = ensure_config()
|
config = ensure_config()
|
||||||
@@ -747,8 +747,8 @@ class RunnableSeq(Runnable):
|
|||||||
async def astream(
|
async def astream(
|
||||||
self,
|
self,
|
||||||
input: Input,
|
input: Input,
|
||||||
config: Optional[RunnableConfig] = None,
|
config: RunnableConfig | None = None,
|
||||||
**kwargs: Optional[Any],
|
**kwargs: Any | None,
|
||||||
) -> AsyncIterator[Any]:
|
) -> AsyncIterator[Any]:
|
||||||
if config is None:
|
if config is None:
|
||||||
config = ensure_config()
|
config = ensure_config()
|
||||||
|
|||||||
@@ -63,12 +63,15 @@ langgraph-sdk = { path = "../sdk-py", editable = true }
|
|||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
lint.select = [ "E", "F", "I", "TID251", "UP" ]
|
lint.select = [ "E", "F", "I", "TID251", "UP" ]
|
||||||
lint.ignore = [ "E501", "UP007" ]
|
lint.ignore = [ "E501" ]
|
||||||
line-length = 88
|
line-length = 88
|
||||||
indent-width = 4
|
indent-width = 4
|
||||||
extend-include = ["*.ipynb"]
|
extend-include = ["*.ipynb"]
|
||||||
target-version = "py39"
|
target-version = "py39"
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
"tests/bench/*" = ["UP006", "UP007"]
|
||||||
|
|
||||||
[tool.ruff.format]
|
[tool.ruff.format]
|
||||||
quote-style = "double"
|
quote-style = "double"
|
||||||
indent-style = "space"
|
indent-style = "space"
|
||||||
|
|||||||
@@ -40,3 +40,29 @@ def test_entrypoint_retry_arg() -> None:
|
|||||||
@entrypoint(retry=RetryPolicy()) # type: ignore[arg-type]
|
@entrypoint(retry=RetryPolicy()) # type: ignore[arg-type]
|
||||||
def my_entrypoint(state: PlainState) -> PlainState:
|
def my_entrypoint(state: PlainState) -> PlainState:
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def test_state_graph_input_schema() -> None:
|
||||||
|
with pytest.warns(
|
||||||
|
LangGraphDeprecatedSinceV10,
|
||||||
|
match="`input` is deprecated and will be removed. Please use `input_schema` instead.",
|
||||||
|
):
|
||||||
|
StateGraph(PlainState, input=PlainState) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
|
def test_state_graph_output_schema() -> None:
|
||||||
|
with pytest.warns(
|
||||||
|
LangGraphDeprecatedSinceV10,
|
||||||
|
match="`output` is deprecated and will be removed. Please use `output_schema` instead.",
|
||||||
|
):
|
||||||
|
StateGraph(PlainState, output=PlainState) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_node_input_schema() -> None:
|
||||||
|
builder = StateGraph(PlainState)
|
||||||
|
|
||||||
|
with pytest.warns(
|
||||||
|
LangGraphDeprecatedSinceV10,
|
||||||
|
match="`input` is deprecated and will be removed. Please use `input_schema` instead.",
|
||||||
|
):
|
||||||
|
builder.add_node("test_node", lambda state: state, input=PlainState) # type: ignore[arg-type]
|
||||||
|
|||||||
@@ -570,7 +570,7 @@ def test_conditional_state_graph(
|
|||||||
workflow = StateGraph(AgentState)
|
workflow = StateGraph(AgentState)
|
||||||
|
|
||||||
workflow.add_node("agent", agent)
|
workflow.add_node("agent", agent)
|
||||||
workflow.add_node("tools", execute_tools, input=ToolState)
|
workflow.add_node("tools", execute_tools, input_schema=ToolState)
|
||||||
|
|
||||||
workflow.set_entry_point("agent")
|
workflow.set_entry_point("agent")
|
||||||
|
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ def test_node_schemas_custom_output() -> None:
|
|||||||
"now": 123,
|
"now": 123,
|
||||||
}
|
}
|
||||||
|
|
||||||
builder = StateGraph(State, output=Output)
|
builder = StateGraph(State, output_schema=Output)
|
||||||
builder.add_node("a", node_a)
|
builder.add_node("a", node_a)
|
||||||
builder.add_node("b", node_b)
|
builder.add_node("b", node_b)
|
||||||
builder.add_node("c", node_c)
|
builder.add_node("c", node_c)
|
||||||
@@ -301,7 +301,7 @@ def test_node_schemas_custom_output() -> None:
|
|||||||
"messages": [_AnyIdHumanMessage(content="hello")],
|
"messages": [_AnyIdHumanMessage(content="hello")],
|
||||||
}
|
}
|
||||||
|
|
||||||
builder = StateGraph(State, output=Output)
|
builder = StateGraph(State, output_schema=Output)
|
||||||
builder.add_node("a", node_a)
|
builder.add_node("a", node_a)
|
||||||
builder.add_node("b", node_b)
|
builder.add_node("b", node_b)
|
||||||
builder.add_node("c", node_c)
|
builder.add_node("c", node_c)
|
||||||
@@ -2492,7 +2492,7 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2(
|
|||||||
assert isinstance(data, State)
|
assert isinstance(data, State)
|
||||||
return "retriever_two"
|
return "retriever_two"
|
||||||
|
|
||||||
workflow = StateGraph(State, input=Input, output=Output)
|
workflow = StateGraph(State, input_schema=Input, output_schema=Output)
|
||||||
|
|
||||||
workflow.add_node("rewrite_query", rewrite_query)
|
workflow.add_node("rewrite_query", rewrite_query)
|
||||||
workflow.add_node("analyzer_one", analyzer_one)
|
workflow.add_node("analyzer_one", analyzer_one)
|
||||||
@@ -2621,7 +2621,7 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic_inp
|
|||||||
assert isinstance(data, State)
|
assert isinstance(data, State)
|
||||||
return "retriever_two"
|
return "retriever_two"
|
||||||
|
|
||||||
workflow = StateGraph(State, input=Input, output=Output)
|
workflow = StateGraph(State, input_schema=Input, output_schema=Output)
|
||||||
|
|
||||||
workflow.add_node("rewrite_query", rewrite_query)
|
workflow.add_node("rewrite_query", rewrite_query)
|
||||||
workflow.add_node("analyzer_one", analyzer_one)
|
workflow.add_node("analyzer_one", analyzer_one)
|
||||||
@@ -6185,14 +6185,17 @@ def test_multiple_subgraphs(sync_checkpointer: BaseCheckpointSaver) -> None:
|
|||||||
return {"result": state["a"] + state["b"]}
|
return {"result": state["a"] + state["b"]}
|
||||||
|
|
||||||
add_subgraph = (
|
add_subgraph = (
|
||||||
StateGraph(State, output=Output).add_node(add).add_edge(START, "add").compile()
|
StateGraph(State, output_schema=Output)
|
||||||
|
.add_node(add)
|
||||||
|
.add_edge(START, "add")
|
||||||
|
.compile()
|
||||||
)
|
)
|
||||||
|
|
||||||
def multiply(state):
|
def multiply(state):
|
||||||
return {"result": state["a"] * state["b"]}
|
return {"result": state["a"] * state["b"]}
|
||||||
|
|
||||||
multiply_subgraph = (
|
multiply_subgraph = (
|
||||||
StateGraph(State, output=Output)
|
StateGraph(State, output_schema=Output)
|
||||||
.add_node(multiply)
|
.add_node(multiply)
|
||||||
.add_edge(START, "multiply")
|
.add_edge(START, "multiply")
|
||||||
.compile()
|
.compile()
|
||||||
@@ -6205,7 +6208,7 @@ def test_multiple_subgraphs(sync_checkpointer: BaseCheckpointSaver) -> None:
|
|||||||
return another_result
|
return another_result
|
||||||
|
|
||||||
parent_call_same_subgraph = (
|
parent_call_same_subgraph = (
|
||||||
StateGraph(State, output=Output)
|
StateGraph(State, output_schema=Output)
|
||||||
.add_node(call_same_subgraph)
|
.add_node(call_same_subgraph)
|
||||||
.add_edge(START, "call_same_subgraph")
|
.add_edge(START, "call_same_subgraph")
|
||||||
.compile(checkpointer=sync_checkpointer)
|
.compile(checkpointer=sync_checkpointer)
|
||||||
@@ -6227,7 +6230,7 @@ def test_multiple_subgraphs(sync_checkpointer: BaseCheckpointSaver) -> None:
|
|||||||
}
|
}
|
||||||
|
|
||||||
parent_call_multiple_subgraphs = (
|
parent_call_multiple_subgraphs = (
|
||||||
StateGraph(State, output=Output)
|
StateGraph(State, output_schema=Output)
|
||||||
.add_node(call_multiple_subgraphs)
|
.add_node(call_multiple_subgraphs)
|
||||||
.add_edge(START, "call_multiple_subgraphs")
|
.add_edge(START, "call_multiple_subgraphs")
|
||||||
.compile(checkpointer=sync_checkpointer)
|
.compile(checkpointer=sync_checkpointer)
|
||||||
@@ -6301,14 +6304,17 @@ def test_multiple_subgraphs_mixed_entrypoint(
|
|||||||
return {"result": state["a"] + state["b"]}
|
return {"result": state["a"] + state["b"]}
|
||||||
|
|
||||||
add_subgraph = (
|
add_subgraph = (
|
||||||
StateGraph(State, output=Output).add_node(add).add_edge(START, "add").compile()
|
StateGraph(State, output_schema=Output)
|
||||||
|
.add_node(add)
|
||||||
|
.add_edge(START, "add")
|
||||||
|
.compile()
|
||||||
)
|
)
|
||||||
|
|
||||||
def multiply(state):
|
def multiply(state):
|
||||||
return {"result": state["a"] * state["b"]}
|
return {"result": state["a"] * state["b"]}
|
||||||
|
|
||||||
multiply_subgraph = (
|
multiply_subgraph = (
|
||||||
StateGraph(State, output=Output)
|
StateGraph(State, output_schema=Output)
|
||||||
.add_node(multiply)
|
.add_node(multiply)
|
||||||
.add_edge(START, "multiply")
|
.add_edge(START, "multiply")
|
||||||
.compile()
|
.compile()
|
||||||
@@ -6377,7 +6383,7 @@ def test_multiple_subgraphs_mixed_state_graph(
|
|||||||
return {"result": another_result}
|
return {"result": another_result}
|
||||||
|
|
||||||
parent_call_same_subgraph = (
|
parent_call_same_subgraph = (
|
||||||
StateGraph(State, output=Output)
|
StateGraph(State, output_schema=Output)
|
||||||
.add_node(call_same_subgraph)
|
.add_node(call_same_subgraph)
|
||||||
.add_edge(START, "call_same_subgraph")
|
.add_edge(START, "call_same_subgraph")
|
||||||
.compile(checkpointer=sync_checkpointer)
|
.compile(checkpointer=sync_checkpointer)
|
||||||
@@ -6399,7 +6405,7 @@ def test_multiple_subgraphs_mixed_state_graph(
|
|||||||
}
|
}
|
||||||
|
|
||||||
parent_call_multiple_subgraphs = (
|
parent_call_multiple_subgraphs = (
|
||||||
StateGraph(State, output=Output)
|
StateGraph(State, output_schema=Output)
|
||||||
.add_node(call_multiple_subgraphs)
|
.add_node(call_multiple_subgraphs)
|
||||||
.add_edge(START, "call_multiple_subgraphs")
|
.add_edge(START, "call_multiple_subgraphs")
|
||||||
.compile(checkpointer=sync_checkpointer)
|
.compile(checkpointer=sync_checkpointer)
|
||||||
|
|||||||
@@ -4310,7 +4310,7 @@ async def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class(
|
|||||||
assert isinstance(data, State)
|
assert isinstance(data, State)
|
||||||
return "retriever_two"
|
return "retriever_two"
|
||||||
|
|
||||||
workflow = StateGraph(State, input=Input, output=Output)
|
workflow = StateGraph(State, input_schema=Input, output_schema=Output)
|
||||||
|
|
||||||
workflow.add_node("rewrite_query", rewrite_query)
|
workflow.add_node("rewrite_query", rewrite_query)
|
||||||
workflow.add_node("analyzer_one", analyzer_one)
|
workflow.add_node("analyzer_one", analyzer_one)
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ def test_runnable_callable_injectable_arguments() -> None:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Test Optional[BaseStore] annotation.
|
# Test Optional[BaseStore] annotation.
|
||||||
def func_optional_store(inputs: Any, store: Optional[BaseStore]) -> str:
|
def func_optional_store(inputs: Any, store: Optional[BaseStore]) -> str: # noqa: UP007
|
||||||
"""Test function that accepts an optional store parameter."""
|
"""Test function that accepts an optional store parameter."""
|
||||||
assert store is None
|
assert store is None
|
||||||
return "success"
|
return "success"
|
||||||
@@ -159,12 +159,12 @@ async def test_runnable_callable_injectable_arguments_async() -> None:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Test Optional[BaseStore] annotation.
|
# Test Optional[BaseStore] annotation.
|
||||||
def func_optional_store(inputs: Any, store: Optional[BaseStore]) -> str:
|
def func_optional_store(inputs: Any, store: Optional[BaseStore]) -> str: # noqa: UP007
|
||||||
"""Test function that accepts an optional store parameter."""
|
"""Test function that accepts an optional store parameter."""
|
||||||
assert store is None
|
assert store is None
|
||||||
return "success"
|
return "success"
|
||||||
|
|
||||||
async def afunc_optional_store(inputs: Any, store: Optional[BaseStore]) -> str:
|
async def afunc_optional_store(inputs: Any, store: BaseStore | None) -> str:
|
||||||
"""Async version of func_optional_store."""
|
"""Async version of func_optional_store."""
|
||||||
assert store is None
|
assert store is None
|
||||||
return "success"
|
return "success"
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ def test_state_schema_optional_values(total_: bool):
|
|||||||
class State(InputState): # this would be ignored
|
class State(InputState): # this would be ignored
|
||||||
val4: dict
|
val4: dict
|
||||||
|
|
||||||
builder = StateGraph(State, input=InputState, output=OutputState)
|
builder = StateGraph(State, input_schema=InputState, output_schema=OutputState)
|
||||||
builder.add_node("n", lambda x: x)
|
builder.add_node("n", lambda x: x)
|
||||||
builder.add_edge("__start__", "n")
|
builder.add_edge("__start__", "n")
|
||||||
graph = builder.compile()
|
graph = builder.compile()
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ def test_input_state_specified() -> None:
|
|||||||
|
|
||||||
def valid(state: State) -> Any: ...
|
def valid(state: State) -> Any: ...
|
||||||
|
|
||||||
new_builder = StateGraph(State, input=InputState)
|
new_builder = StateGraph(State, input_schema=InputState)
|
||||||
new_builder.add_node("valid", valid)
|
new_builder.add_node("valid", valid)
|
||||||
new_builder.set_entry_point("valid")
|
new_builder.set_entry_point("valid")
|
||||||
new_graph = new_builder.compile()
|
new_graph = new_builder.compile()
|
||||||
|
|||||||
@@ -591,11 +591,11 @@ def create_react_agent(
|
|||||||
workflow = StateGraph(state_schema, config_schema=config_schema)
|
workflow = StateGraph(state_schema, config_schema=config_schema)
|
||||||
workflow.add_node(
|
workflow.add_node(
|
||||||
"agent",
|
"agent",
|
||||||
RunnableCallable(call_model, acall_model),
|
RunnableCallable(call_model, acall_model), # type: ignore[call-overload]
|
||||||
input=input_schema,
|
input_schema=input_schema,
|
||||||
)
|
)
|
||||||
if pre_model_hook is not None:
|
if pre_model_hook is not None:
|
||||||
workflow.add_node("pre_model_hook", pre_model_hook)
|
workflow.add_node("pre_model_hook", pre_model_hook) # type: ignore[arg-type]
|
||||||
workflow.add_edge("pre_model_hook", "agent")
|
workflow.add_edge("pre_model_hook", "agent")
|
||||||
entrypoint = "pre_model_hook"
|
entrypoint = "pre_model_hook"
|
||||||
else:
|
else:
|
||||||
@@ -604,14 +604,15 @@ def create_react_agent(
|
|||||||
workflow.set_entry_point(entrypoint)
|
workflow.set_entry_point(entrypoint)
|
||||||
|
|
||||||
if post_model_hook is not None:
|
if post_model_hook is not None:
|
||||||
workflow.add_node("post_model_hook", post_model_hook)
|
workflow.add_node("post_model_hook", post_model_hook) # type: ignore[arg-type]
|
||||||
workflow.add_edge("agent", "post_model_hook")
|
workflow.add_edge("agent", "post_model_hook")
|
||||||
|
|
||||||
if response_format is not None:
|
if response_format is not None:
|
||||||
workflow.add_node(
|
workflow.add_node(
|
||||||
"generate_structured_response",
|
"generate_structured_response",
|
||||||
RunnableCallable(
|
RunnableCallable( # type: ignore[call-overload]
|
||||||
generate_structured_response, agenerate_structured_response
|
generate_structured_response,
|
||||||
|
agenerate_structured_response,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if post_model_hook is not None:
|
if post_model_hook is not None:
|
||||||
@@ -658,14 +659,16 @@ def create_react_agent(
|
|||||||
|
|
||||||
# Define the two nodes we will cycle between
|
# Define the two nodes we will cycle between
|
||||||
workflow.add_node(
|
workflow.add_node(
|
||||||
"agent", RunnableCallable(call_model, acall_model), input=input_schema
|
"agent",
|
||||||
|
RunnableCallable(call_model, acall_model), # type: ignore[call-overload]
|
||||||
|
input_schema=input_schema,
|
||||||
)
|
)
|
||||||
workflow.add_node("tools", tool_node)
|
workflow.add_node("tools", tool_node) # type: ignore[call-overload]
|
||||||
|
|
||||||
# Optionally add a pre-model hook node that will be called
|
# Optionally add a pre-model hook node that will be called
|
||||||
# every time before the "agent" (LLM-calling node)
|
# every time before the "agent" (LLM-calling node)
|
||||||
if pre_model_hook is not None:
|
if pre_model_hook is not None:
|
||||||
workflow.add_node("pre_model_hook", pre_model_hook)
|
workflow.add_node("pre_model_hook", pre_model_hook) # type: ignore[arg-type]
|
||||||
workflow.add_edge("pre_model_hook", "agent")
|
workflow.add_edge("pre_model_hook", "agent")
|
||||||
entrypoint = "pre_model_hook"
|
entrypoint = "pre_model_hook"
|
||||||
else:
|
else:
|
||||||
@@ -680,7 +683,7 @@ def create_react_agent(
|
|||||||
|
|
||||||
# Add a post model hook node if post_model_hook is provided
|
# Add a post model hook node if post_model_hook is provided
|
||||||
if post_model_hook is not None:
|
if post_model_hook is not None:
|
||||||
workflow.add_node("post_model_hook", post_model_hook)
|
workflow.add_node("post_model_hook", post_model_hook) # type: ignore[arg-type]
|
||||||
agent_paths.append("post_model_hook")
|
agent_paths.append("post_model_hook")
|
||||||
workflow.add_edge("agent", "post_model_hook")
|
workflow.add_edge("agent", "post_model_hook")
|
||||||
else:
|
else:
|
||||||
@@ -690,8 +693,9 @@ def create_react_agent(
|
|||||||
if response_format is not None:
|
if response_format is not None:
|
||||||
workflow.add_node(
|
workflow.add_node(
|
||||||
"generate_structured_response",
|
"generate_structured_response",
|
||||||
RunnableCallable(
|
RunnableCallable( # type: ignore[call-overload]
|
||||||
generate_structured_response, agenerate_structured_response
|
generate_structured_response,
|
||||||
|
agenerate_structured_response,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if post_model_hook is not None:
|
if post_model_hook is not None:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@langchain/langgraph-sdk",
|
"name": "@langchain/langgraph-sdk",
|
||||||
"version": "0.0.82",
|
"version": "0.0.84",
|
||||||
"description": "Client library for interacting with the LangGraph API",
|
"description": "Client library for interacting with the LangGraph API",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "yarn@1.22.19",
|
"packageManager": "yarn@1.22.19",
|
||||||
|
|||||||
@@ -979,7 +979,12 @@ export class RunsClient<
|
|||||||
after_seconds: payload?.afterSeconds,
|
after_seconds: payload?.afterSeconds,
|
||||||
if_not_exists: payload?.ifNotExists,
|
if_not_exists: payload?.ifNotExists,
|
||||||
checkpoint_during: payload?.checkpointDuring,
|
checkpoint_during: payload?.checkpointDuring,
|
||||||
langsmith_tracer: payload?._langsmithTracer,
|
langsmith_tracer: payload?._langsmithTracer
|
||||||
|
? {
|
||||||
|
project_name: payload?._langsmithTracer?.projectName,
|
||||||
|
example_id: payload?._langsmithTracer?.exampleId,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const [run, response] = await this.fetch<Run>(`/threads/${threadId}/runs`, {
|
const [run, response] = await this.fetch<Run>(`/threads/${threadId}/runs`, {
|
||||||
|
|||||||
@@ -70,6 +70,15 @@ export type ToolMessage = {
|
|||||||
tool_call_id: string;
|
tool_call_id: string;
|
||||||
additional_kwargs?: MessageAdditionalKwargs | undefined;
|
additional_kwargs?: MessageAdditionalKwargs | undefined;
|
||||||
response_metadata?: Record<string, unknown> | undefined;
|
response_metadata?: Record<string, unknown> | undefined;
|
||||||
|
/**
|
||||||
|
* Artifact of the Tool execution which is not meant to be sent to the model.
|
||||||
|
*
|
||||||
|
* Should only be specified if it is different from the message content, e.g. if only
|
||||||
|
* a subset of the full tool output is being passed as message content but the full
|
||||||
|
* output is needed in other parts of the code.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
artifact?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SystemMessage = {
|
export type SystemMessage = {
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ class Auth:
|
|||||||
# will be considered a breaking change.
|
# will be considered a breaking change.
|
||||||
self._handlers: dict[tuple[str, str], list[types.Handler]] = {}
|
self._handlers: dict[tuple[str, str], list[types.Handler]] = {}
|
||||||
self._global_handlers: list[types.Handler] = []
|
self._global_handlers: list[types.Handler] = []
|
||||||
self._authenticate_handler: typing.Optional[types.Authenticator] = None
|
self._authenticate_handler: types.Authenticator | None = None
|
||||||
self._handler_cache: dict[tuple[str, str], types.Handler] = {}
|
self._handler_cache: dict[tuple[str, str], types.Handler] = {}
|
||||||
|
|
||||||
def authenticate(self, fn: AH) -> AH:
|
def authenticate(self, fn: AH) -> AH:
|
||||||
@@ -301,7 +301,7 @@ class _ResourceOn(typing.Generic[VCreate, VRead, VUpdate, VDelete, VSearch]):
|
|||||||
Generic base class for resource-specific handlers.
|
Generic base class for resource-specific handlers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
value: type[typing.Union[VCreate, VUpdate, VRead, VDelete, VSearch]]
|
value: type[VCreate | VUpdate | VRead | VDelete | VSearch]
|
||||||
|
|
||||||
Create: type[VCreate]
|
Create: type[VCreate]
|
||||||
Read: type[VRead]
|
Read: type[VRead]
|
||||||
@@ -335,40 +335,36 @@ class _ResourceOn(typing.Generic[VCreate, VRead, VUpdate, VDelete, VSearch]):
|
|||||||
@typing.overload
|
@typing.overload
|
||||||
def __call__(
|
def __call__(
|
||||||
self,
|
self,
|
||||||
fn: typing.Union[
|
fn: _ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]
|
||||||
_ActionHandler[typing.Union[VCreate, VUpdate, VRead, VDelete, VSearch]],
|
| _ActionHandler[dict[str, typing.Any]],
|
||||||
_ActionHandler[dict[str, typing.Any]],
|
) -> _ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]: ...
|
||||||
],
|
|
||||||
) -> _ActionHandler[typing.Union[VCreate, VUpdate, VRead, VDelete, VSearch]]: ...
|
|
||||||
|
|
||||||
@typing.overload
|
@typing.overload
|
||||||
def __call__(
|
def __call__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
resources: typing.Union[str, Sequence[str]],
|
resources: str | Sequence[str],
|
||||||
actions: typing.Optional[typing.Union[str, Sequence[str]]] = None,
|
actions: str | Sequence[str] | None = None,
|
||||||
) -> Callable[
|
) -> Callable[
|
||||||
[_ActionHandler[typing.Union[VCreate, VUpdate, VRead, VDelete, VSearch]]],
|
[_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]],
|
||||||
_ActionHandler[typing.Union[VCreate, VUpdate, VRead, VDelete, VSearch]],
|
_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch],
|
||||||
]: ...
|
]: ...
|
||||||
|
|
||||||
def __call__(
|
def __call__(
|
||||||
self,
|
self,
|
||||||
fn: typing.Union[
|
fn: _ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]
|
||||||
_ActionHandler[typing.Union[VCreate, VUpdate, VRead, VDelete, VSearch]],
|
| _ActionHandler[dict[str, typing.Any]]
|
||||||
_ActionHandler[dict[str, typing.Any]],
|
| None = None,
|
||||||
None,
|
|
||||||
] = None,
|
|
||||||
*,
|
*,
|
||||||
resources: typing.Union[str, Sequence[str], None] = None,
|
resources: str | Sequence[str] | None = None,
|
||||||
actions: typing.Optional[typing.Union[str, Sequence[str]]] = None,
|
actions: str | Sequence[str] | None = None,
|
||||||
) -> typing.Union[
|
) -> (
|
||||||
_ActionHandler[typing.Union[VCreate, VUpdate, VRead, VDelete, VSearch]],
|
_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]
|
||||||
Callable[
|
| Callable[
|
||||||
[_ActionHandler[typing.Union[VCreate, VUpdate, VRead, VDelete, VSearch]]],
|
[_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]],
|
||||||
_ActionHandler[typing.Union[VCreate, VUpdate, VRead, VDelete, VSearch]],
|
_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch],
|
||||||
],
|
]
|
||||||
]:
|
):
|
||||||
if fn is not None:
|
if fn is not None:
|
||||||
_validate_handler(fn)
|
_validate_handler(fn)
|
||||||
return typing.cast(
|
return typing.cast(
|
||||||
@@ -377,10 +373,8 @@ class _ResourceOn(typing.Generic[VCreate, VRead, VUpdate, VDelete, VSearch]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def decorator(
|
def decorator(
|
||||||
handler: _ActionHandler[
|
handler: _ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch],
|
||||||
typing.Union[VCreate, VUpdate, VRead, VDelete, VSearch]
|
) -> _ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]:
|
||||||
],
|
|
||||||
) -> _ActionHandler[typing.Union[VCreate, VUpdate, VRead, VDelete, VSearch]]:
|
|
||||||
_validate_handler(handler)
|
_validate_handler(handler)
|
||||||
return typing.cast(
|
return typing.cast(
|
||||||
_ActionHandler[typing.Union[VCreate, VUpdate, VRead, VDelete, VSearch]],
|
_ActionHandler[typing.Union[VCreate, VUpdate, VRead, VDelete, VSearch]],
|
||||||
@@ -482,14 +476,9 @@ class _StoreOn:
|
|||||||
def __call__(
|
def __call__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
actions: typing.Optional[
|
actions: typing.Literal["put", "get", "search", "list_namespaces", "delete"]
|
||||||
typing.Union[
|
| Sequence[typing.Literal["put", "get", "search", "list_namespaces", "delete"]]
|
||||||
typing.Literal["put", "get", "search", "list_namespaces", "delete"],
|
| None = None,
|
||||||
Sequence[
|
|
||||||
typing.Literal["put", "get", "search", "list_namespaces", "delete"]
|
|
||||||
],
|
|
||||||
]
|
|
||||||
] = None,
|
|
||||||
) -> Callable[[AHO], AHO]: ...
|
) -> Callable[[AHO], AHO]: ...
|
||||||
|
|
||||||
@typing.overload
|
@typing.overload
|
||||||
@@ -497,17 +486,12 @@ class _StoreOn:
|
|||||||
|
|
||||||
def __call__(
|
def __call__(
|
||||||
self,
|
self,
|
||||||
fn: typing.Optional[AHO] = None,
|
fn: AHO | None = None,
|
||||||
*,
|
*,
|
||||||
actions: typing.Optional[
|
actions: typing.Literal["put", "get", "search", "list_namespaces", "delete"]
|
||||||
typing.Union[
|
| Sequence[typing.Literal["put", "get", "search", "list_namespaces", "delete"]]
|
||||||
typing.Literal["put", "get", "search", "list_namespaces", "delete"],
|
| None = None,
|
||||||
Sequence[
|
) -> AHO | Callable[[AHO], AHO]:
|
||||||
typing.Literal["put", "get", "search", "list_namespaces", "delete"]
|
|
||||||
],
|
|
||||||
]
|
|
||||||
] = None,
|
|
||||||
) -> typing.Union[AHO, Callable[[AHO], AHO]]:
|
|
||||||
"""Register a handler for specific resources and actions.
|
"""Register a handler for specific resources and actions.
|
||||||
|
|
||||||
Can be used as a decorator or with explicit resource/action parameters:
|
Can be used as a decorator or with explicit resource/action parameters:
|
||||||
@@ -620,8 +604,8 @@ class _On:
|
|||||||
def __call__(
|
def __call__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
resources: typing.Union[str, Sequence[str]],
|
resources: str | Sequence[str],
|
||||||
actions: typing.Optional[typing.Union[str, Sequence[str]]] = None,
|
actions: str | Sequence[str] | None = None,
|
||||||
) -> Callable[[AHO], AHO]: ...
|
) -> Callable[[AHO], AHO]: ...
|
||||||
|
|
||||||
@typing.overload
|
@typing.overload
|
||||||
@@ -629,11 +613,11 @@ class _On:
|
|||||||
|
|
||||||
def __call__(
|
def __call__(
|
||||||
self,
|
self,
|
||||||
fn: typing.Optional[AHO] = None,
|
fn: AHO | None = None,
|
||||||
*,
|
*,
|
||||||
resources: typing.Union[str, Sequence[str], None] = None,
|
resources: str | Sequence[str] | None = None,
|
||||||
actions: typing.Optional[typing.Union[str, Sequence[str]]] = None,
|
actions: str | Sequence[str] | None = None,
|
||||||
) -> typing.Union[AHO, Callable[[AHO], AHO]]:
|
) -> AHO | Callable[[AHO], AHO]:
|
||||||
"""Register a handler for specific resources and actions.
|
"""Register a handler for specific resources and actions.
|
||||||
|
|
||||||
Can be used as a decorator or with explicit resource/action parameters:
|
Can be used as a decorator or with explicit resource/action parameters:
|
||||||
@@ -675,8 +659,8 @@ class _On:
|
|||||||
|
|
||||||
def _register_handler(
|
def _register_handler(
|
||||||
auth: Auth,
|
auth: Auth,
|
||||||
resource: typing.Optional[str],
|
resource: str | None,
|
||||||
action: typing.Optional[str],
|
action: str | None,
|
||||||
fn: types.Handler,
|
fn: types.Handler,
|
||||||
) -> types.Handler:
|
) -> types.Handler:
|
||||||
_validate_handler(fn)
|
_validate_handler(fn)
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"""Exceptions used in the auth system."""
|
"""Exceptions used in the auth system."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import http
|
import http
|
||||||
import typing
|
from collections.abc import Mapping
|
||||||
|
|
||||||
|
|
||||||
class HTTPException(Exception):
|
class HTTPException(Exception):
|
||||||
@@ -37,8 +39,8 @@ class HTTPException(Exception):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
status_code: int = 401,
|
status_code: int = 401,
|
||||||
detail: typing.Optional[str] = None,
|
detail: str | None = None,
|
||||||
headers: typing.Optional[typing.Mapping[str, str]] = None,
|
headers: Mapping[str, str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if detail is None:
|
if detail is None:
|
||||||
detail = http.HTTPStatus(status_code).phrase
|
detail = http.HTTPStatus(status_code).phrase
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ Note:
|
|||||||
All typing.TypedDict classes use total=False to make all fields typing.Optional by default.
|
All typing.TypedDict classes use total=False to make all fields typing.Optional by default.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import sys
|
import sys
|
||||||
import typing
|
import typing
|
||||||
@@ -56,10 +58,8 @@ Values:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
FilterType = typing.Union[
|
FilterType = typing.Union[
|
||||||
typing.Dict[
|
dict[str, typing.Union[str, dict[typing.Literal["$eq", "$contains"], str]]],
|
||||||
str, typing.Union[str, typing.Dict[typing.Literal["$eq", "$contains"], str]]
|
dict[str, str],
|
||||||
],
|
|
||||||
typing.Dict[str, str],
|
|
||||||
]
|
]
|
||||||
"""Response type for authorization handlers.
|
"""Response type for authorization handlers.
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ Values:
|
|||||||
- error: Thread encountered an error
|
- error: Thread encountered an error
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MetadataInput = typing.Dict[str, typing.Any]
|
MetadataInput = dict[str, typing.Any]
|
||||||
"""Type for arbitrary metadata attached to entities.
|
"""Type for arbitrary metadata attached to entities.
|
||||||
|
|
||||||
Allows storing custom key-value pairs with any entity.
|
Allows storing custom key-value pairs with any entity.
|
||||||
@@ -434,7 +434,7 @@ class ThreadsRead(typing.TypedDict, total=False):
|
|||||||
thread_id: UUID
|
thread_id: UUID
|
||||||
"""Unique identifier for the thread."""
|
"""Unique identifier for the thread."""
|
||||||
|
|
||||||
run_id: typing.Optional[UUID]
|
run_id: UUID | None
|
||||||
"""Run ID to filter by. Only used when reading run information within a thread."""
|
"""Run ID to filter by. Only used when reading run information within a thread."""
|
||||||
|
|
||||||
|
|
||||||
@@ -451,7 +451,7 @@ class ThreadsUpdate(typing.TypedDict, total=False):
|
|||||||
metadata: MetadataInput
|
metadata: MetadataInput
|
||||||
"""typing.Optional metadata to update."""
|
"""typing.Optional metadata to update."""
|
||||||
|
|
||||||
action: typing.Optional[typing.Literal["interrupt", "rollback"]]
|
action: typing.Literal["interrupt", "rollback"] | None
|
||||||
"""typing.Optional action to perform on the thread."""
|
"""typing.Optional action to perform on the thread."""
|
||||||
|
|
||||||
|
|
||||||
@@ -464,7 +464,7 @@ class ThreadsDelete(typing.TypedDict, total=False):
|
|||||||
thread_id: UUID
|
thread_id: UUID
|
||||||
"""Unique identifier for the thread."""
|
"""Unique identifier for the thread."""
|
||||||
|
|
||||||
run_id: typing.Optional[UUID]
|
run_id: UUID | None
|
||||||
"""typing.Optional run ID to filter by."""
|
"""typing.Optional run ID to filter by."""
|
||||||
|
|
||||||
|
|
||||||
@@ -480,7 +480,7 @@ class ThreadsSearch(typing.TypedDict, total=False):
|
|||||||
values: MetadataInput
|
values: MetadataInput
|
||||||
"""typing.Optional values to filter by."""
|
"""typing.Optional values to filter by."""
|
||||||
|
|
||||||
status: typing.Optional[ThreadStatus]
|
status: ThreadStatus | None
|
||||||
"""typing.Optional status to filter by."""
|
"""typing.Optional status to filter by."""
|
||||||
|
|
||||||
limit: int
|
limit: int
|
||||||
@@ -489,7 +489,7 @@ class ThreadsSearch(typing.TypedDict, total=False):
|
|||||||
offset: int
|
offset: int
|
||||||
"""Offset for pagination."""
|
"""Offset for pagination."""
|
||||||
|
|
||||||
thread_id: typing.Optional[UUID]
|
thread_id: UUID | None
|
||||||
"""typing.Optional thread ID to filter by."""
|
"""typing.Optional thread ID to filter by."""
|
||||||
|
|
||||||
|
|
||||||
@@ -514,16 +514,16 @@ class RunsCreate(typing.TypedDict, total=False):
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
assistant_id: typing.Optional[UUID]
|
assistant_id: UUID | None
|
||||||
"""typing.Optional assistant ID to use for this run."""
|
"""typing.Optional assistant ID to use for this run."""
|
||||||
|
|
||||||
thread_id: typing.Optional[UUID]
|
thread_id: UUID | None
|
||||||
"""typing.Optional thread ID to use for this run."""
|
"""typing.Optional thread ID to use for this run."""
|
||||||
|
|
||||||
run_id: typing.Optional[UUID]
|
run_id: UUID | None
|
||||||
"""typing.Optional run ID to use for this run."""
|
"""typing.Optional run ID to use for this run."""
|
||||||
|
|
||||||
status: typing.Optional[RunStatus]
|
status: RunStatus | None
|
||||||
"""typing.Optional status for this run."""
|
"""typing.Optional status for this run."""
|
||||||
|
|
||||||
metadata: MetadataInput
|
metadata: MetadataInput
|
||||||
@@ -541,10 +541,10 @@ class RunsCreate(typing.TypedDict, total=False):
|
|||||||
after_seconds: int
|
after_seconds: int
|
||||||
"""Number of seconds to wait before creating the run."""
|
"""Number of seconds to wait before creating the run."""
|
||||||
|
|
||||||
kwargs: typing.Dict[str, typing.Any]
|
kwargs: dict[str, typing.Any]
|
||||||
"""Keyword arguments to pass to the run."""
|
"""Keyword arguments to pass to the run."""
|
||||||
|
|
||||||
action: typing.Optional[typing.Literal["interrupt", "rollback"]]
|
action: typing.Literal["interrupt", "rollback"] | None
|
||||||
"""Action to take if updating an existing run."""
|
"""Action to take if updating an existing run."""
|
||||||
|
|
||||||
|
|
||||||
@@ -570,7 +570,7 @@ class AssistantsCreate(typing.TypedDict, total=False):
|
|||||||
graph_id: str
|
graph_id: str
|
||||||
"""Graph ID to use for this assistant."""
|
"""Graph ID to use for this assistant."""
|
||||||
|
|
||||||
config: typing.Optional[typing.Union[typing.Dict[str, typing.Any], typing.Any]]
|
config: dict[str, typing.Any] | typing.Any | None
|
||||||
"""typing.Optional configuration for the assistant."""
|
"""typing.Optional configuration for the assistant."""
|
||||||
|
|
||||||
metadata: MetadataInput
|
metadata: MetadataInput
|
||||||
@@ -621,19 +621,19 @@ class AssistantsUpdate(typing.TypedDict, total=False):
|
|||||||
assistant_id: UUID
|
assistant_id: UUID
|
||||||
"""Unique identifier for the assistant."""
|
"""Unique identifier for the assistant."""
|
||||||
|
|
||||||
graph_id: typing.Optional[str]
|
graph_id: str | None
|
||||||
"""typing.Optional graph ID to update."""
|
"""typing.Optional graph ID to update."""
|
||||||
|
|
||||||
config: typing.Optional[typing.Union[typing.Dict[str, typing.Any], typing.Any]]
|
config: dict[str, typing.Any] | typing.Any | None
|
||||||
"""typing.Optional configuration to update."""
|
"""typing.Optional configuration to update."""
|
||||||
|
|
||||||
metadata: MetadataInput
|
metadata: MetadataInput
|
||||||
"""typing.Optional metadata to update."""
|
"""typing.Optional metadata to update."""
|
||||||
|
|
||||||
name: typing.Optional[str]
|
name: str | None
|
||||||
"""typing.Optional name to update."""
|
"""typing.Optional name to update."""
|
||||||
|
|
||||||
version: typing.Optional[int]
|
version: int | None
|
||||||
"""typing.Optional version to update."""
|
"""typing.Optional version to update."""
|
||||||
|
|
||||||
|
|
||||||
@@ -666,7 +666,7 @@ class AssistantsSearch(typing.TypedDict):
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
graph_id: typing.Optional[str]
|
graph_id: str | None
|
||||||
"""typing.Optional graph ID to filter by."""
|
"""typing.Optional graph ID to filter by."""
|
||||||
|
|
||||||
metadata: MetadataInput
|
metadata: MetadataInput
|
||||||
@@ -695,22 +695,22 @@ class CronsCreate(typing.TypedDict, total=False):
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
payload: typing.Dict[str, typing.Any]
|
payload: dict[str, typing.Any]
|
||||||
"""Payload for the cron job."""
|
"""Payload for the cron job."""
|
||||||
|
|
||||||
schedule: str
|
schedule: str
|
||||||
"""Schedule for the cron job."""
|
"""Schedule for the cron job."""
|
||||||
|
|
||||||
cron_id: typing.Optional[UUID]
|
cron_id: UUID | None
|
||||||
"""typing.Optional unique identifier for the cron job."""
|
"""typing.Optional unique identifier for the cron job."""
|
||||||
|
|
||||||
thread_id: typing.Optional[UUID]
|
thread_id: UUID | None
|
||||||
"""typing.Optional thread ID to use for this cron job."""
|
"""typing.Optional thread ID to use for this cron job."""
|
||||||
|
|
||||||
user_id: typing.Optional[str]
|
user_id: str | None
|
||||||
"""typing.Optional user ID to use for this cron job."""
|
"""typing.Optional user ID to use for this cron job."""
|
||||||
|
|
||||||
end_time: typing.Optional[datetime]
|
end_time: datetime | None
|
||||||
"""typing.Optional end time for the cron job."""
|
"""typing.Optional end time for the cron job."""
|
||||||
|
|
||||||
|
|
||||||
@@ -760,10 +760,10 @@ class CronsUpdate(typing.TypedDict, total=False):
|
|||||||
cron_id: UUID
|
cron_id: UUID
|
||||||
"""Unique identifier for the cron job."""
|
"""Unique identifier for the cron job."""
|
||||||
|
|
||||||
payload: typing.Optional[typing.Dict[str, typing.Any]]
|
payload: dict[str, typing.Any] | None
|
||||||
"""typing.Optional payload to update."""
|
"""typing.Optional payload to update."""
|
||||||
|
|
||||||
schedule: typing.Optional[str]
|
schedule: str | None
|
||||||
"""typing.Optional schedule to update."""
|
"""typing.Optional schedule to update."""
|
||||||
|
|
||||||
|
|
||||||
@@ -781,10 +781,10 @@ class CronsSearch(typing.TypedDict, total=False):
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
assistant_id: typing.Optional[UUID]
|
assistant_id: UUID | None
|
||||||
"""typing.Optional assistant ID to filter by."""
|
"""typing.Optional assistant ID to filter by."""
|
||||||
|
|
||||||
thread_id: typing.Optional[UUID]
|
thread_id: UUID | None
|
||||||
"""typing.Optional thread ID to filter by."""
|
"""typing.Optional thread ID to filter by."""
|
||||||
|
|
||||||
limit: int
|
limit: int
|
||||||
@@ -810,7 +810,7 @@ class StoreSearch(typing.TypedDict):
|
|||||||
namespace: tuple[str, ...]
|
namespace: tuple[str, ...]
|
||||||
"""Prefix filter for defining the search scope."""
|
"""Prefix filter for defining the search scope."""
|
||||||
|
|
||||||
filter: typing.Optional[dict[str, typing.Any]]
|
filter: dict[str, typing.Any] | None
|
||||||
"""Key-value pairs for filtering results based on exact matches or comparison operators."""
|
"""Key-value pairs for filtering results based on exact matches or comparison operators."""
|
||||||
|
|
||||||
limit: int
|
limit: int
|
||||||
@@ -819,20 +819,20 @@ class StoreSearch(typing.TypedDict):
|
|||||||
offset: int
|
offset: int
|
||||||
"""Number of matching items to skip for pagination."""
|
"""Number of matching items to skip for pagination."""
|
||||||
|
|
||||||
query: typing.Optional[str]
|
query: str | None
|
||||||
"""Naturalj language search query for semantic search capabilities."""
|
"""Naturalj language search query for semantic search capabilities."""
|
||||||
|
|
||||||
|
|
||||||
class StoreListNamespaces(typing.TypedDict):
|
class StoreListNamespaces(typing.TypedDict):
|
||||||
"""Operation to list and filter namespaces in the store."""
|
"""Operation to list and filter namespaces in the store."""
|
||||||
|
|
||||||
namespace: typing.Optional[tuple[str, ...]]
|
namespace: tuple[str, ...] | None
|
||||||
"""Prefix filter namespaces."""
|
"""Prefix filter namespaces."""
|
||||||
|
|
||||||
suffix: typing.Optional[tuple[str, ...]]
|
suffix: tuple[str, ...] | None
|
||||||
"""Optional conditions for filtering namespaces."""
|
"""Optional conditions for filtering namespaces."""
|
||||||
|
|
||||||
max_depth: typing.Optional[int]
|
max_depth: int | None
|
||||||
"""Maximum depth of namespace hierarchy to return.
|
"""Maximum depth of namespace hierarchy to return.
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
@@ -855,10 +855,10 @@ class StorePut(typing.TypedDict):
|
|||||||
key: str
|
key: str
|
||||||
"""Unique identifier for the item within its namespace."""
|
"""Unique identifier for the item within its namespace."""
|
||||||
|
|
||||||
value: typing.Optional[dict[str, typing.Any]]
|
value: dict[str, typing.Any] | None
|
||||||
"""The data to store, or None to mark the item for deletion."""
|
"""The data to store, or None to mark the item for deletion."""
|
||||||
|
|
||||||
index: typing.Optional[typing.Union[typing.Literal[False], list[str]]]
|
index: typing.Literal[False] | list[str] | None
|
||||||
"""Optional index configuration for full-text search."""
|
"""Optional index configuration for full-text search."""
|
||||||
|
|
||||||
|
|
||||||
@@ -900,7 +900,7 @@ class on:
|
|||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
value = typing.Dict[str, typing.Any]
|
value = dict[str, typing.Any]
|
||||||
|
|
||||||
class threads:
|
class threads:
|
||||||
"""Types for thread-related operations."""
|
"""Types for thread-related operations."""
|
||||||
|
|||||||
+538
-538
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
|||||||
"""Data models for interacting with the LangGraph API."""
|
"""Data models for interacting with the LangGraph API."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import (
|
from typing import (
|
||||||
@@ -8,7 +10,6 @@ from typing import (
|
|||||||
NamedTuple,
|
NamedTuple,
|
||||||
Optional,
|
Optional,
|
||||||
TypedDict,
|
TypedDict,
|
||||||
Union,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Json = Optional[dict[str, Any]]
|
Json = Optional[dict[str, Any]]
|
||||||
@@ -142,9 +143,9 @@ class Checkpoint(TypedDict):
|
|||||||
"""Unique identifier for the thread associated with this checkpoint."""
|
"""Unique identifier for the thread associated with this checkpoint."""
|
||||||
checkpoint_ns: str
|
checkpoint_ns: str
|
||||||
"""Namespace for the checkpoint; used internally to manage subgraph state."""
|
"""Namespace for the checkpoint; used internally to manage subgraph state."""
|
||||||
checkpoint_id: Optional[str]
|
checkpoint_id: str | None
|
||||||
"""Optional unique identifier for the checkpoint itself."""
|
"""Optional unique identifier for the checkpoint itself."""
|
||||||
checkpoint_map: Optional[dict[str, Any]]
|
checkpoint_map: dict[str, Any] | None
|
||||||
"""Optional dictionary containing checkpoint-specific data."""
|
"""Optional dictionary containing checkpoint-specific data."""
|
||||||
|
|
||||||
|
|
||||||
@@ -153,16 +154,16 @@ class GraphSchema(TypedDict):
|
|||||||
|
|
||||||
graph_id: str
|
graph_id: str
|
||||||
"""The ID of the graph."""
|
"""The ID of the graph."""
|
||||||
input_schema: Optional[dict]
|
input_schema: dict | None
|
||||||
"""The schema for the graph input.
|
"""The schema for the graph input.
|
||||||
Missing if unable to generate JSON schema from graph."""
|
Missing if unable to generate JSON schema from graph."""
|
||||||
output_schema: Optional[dict]
|
output_schema: dict | None
|
||||||
"""The schema for the graph output.
|
"""The schema for the graph output.
|
||||||
Missing if unable to generate JSON schema from graph."""
|
Missing if unable to generate JSON schema from graph."""
|
||||||
state_schema: Optional[dict]
|
state_schema: dict | None
|
||||||
"""The schema for the graph state.
|
"""The schema for the graph state.
|
||||||
Missing if unable to generate JSON schema from graph."""
|
Missing if unable to generate JSON schema from graph."""
|
||||||
config_schema: Optional[dict]
|
config_schema: dict | None
|
||||||
"""The schema for the graph config.
|
"""The schema for the graph config.
|
||||||
Missing if unable to generate JSON schema from graph."""
|
Missing if unable to generate JSON schema from graph."""
|
||||||
|
|
||||||
@@ -187,7 +188,7 @@ class AssistantBase(TypedDict):
|
|||||||
"""The version of the assistant"""
|
"""The version of the assistant"""
|
||||||
name: str
|
name: str
|
||||||
"""The name of the assistant"""
|
"""The name of the assistant"""
|
||||||
description: Optional[str]
|
description: str | None
|
||||||
"""The description of the assistant"""
|
"""The description of the assistant"""
|
||||||
|
|
||||||
|
|
||||||
@@ -213,7 +214,7 @@ class Interrupt(TypedDict, total=False):
|
|||||||
"""When the interrupt occurred."""
|
"""When the interrupt occurred."""
|
||||||
resumable: bool
|
resumable: bool
|
||||||
"""Whether the interrupt can be resumed."""
|
"""Whether the interrupt can be resumed."""
|
||||||
ns: Optional[list[str]]
|
ns: list[str] | None
|
||||||
"""Optional namespace for the interrupt."""
|
"""Optional namespace for the interrupt."""
|
||||||
|
|
||||||
|
|
||||||
@@ -241,17 +242,17 @@ class ThreadTask(TypedDict):
|
|||||||
|
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
error: Optional[str]
|
error: str | None
|
||||||
interrupts: list[Interrupt]
|
interrupts: list[Interrupt]
|
||||||
checkpoint: Optional[Checkpoint]
|
checkpoint: Checkpoint | None
|
||||||
state: Optional["ThreadState"]
|
state: ThreadState | None
|
||||||
result: Optional[dict[str, Any]]
|
result: dict[str, Any] | None
|
||||||
|
|
||||||
|
|
||||||
class ThreadState(TypedDict):
|
class ThreadState(TypedDict):
|
||||||
"""Represents the state of a thread."""
|
"""Represents the state of a thread."""
|
||||||
|
|
||||||
values: Union[list[dict], dict[str, Any]]
|
values: list[dict] | dict[str, Any]
|
||||||
"""The state values."""
|
"""The state values."""
|
||||||
next: Sequence[str]
|
next: Sequence[str]
|
||||||
"""The next nodes to execute. If empty, the thread is done until new input is
|
"""The next nodes to execute. If empty, the thread is done until new input is
|
||||||
@@ -260,9 +261,9 @@ class ThreadState(TypedDict):
|
|||||||
"""The ID of the checkpoint."""
|
"""The ID of the checkpoint."""
|
||||||
metadata: Json
|
metadata: Json
|
||||||
"""Metadata for this state"""
|
"""Metadata for this state"""
|
||||||
created_at: Optional[str]
|
created_at: str | None
|
||||||
"""Timestamp of state creation"""
|
"""Timestamp of state creation"""
|
||||||
parent_checkpoint: Optional[Checkpoint]
|
parent_checkpoint: Checkpoint | None
|
||||||
"""The ID of the parent checkpoint. If missing, this is the root checkpoint."""
|
"""The ID of the parent checkpoint. If missing, this is the root checkpoint."""
|
||||||
tasks: Sequence[ThreadTask]
|
tasks: Sequence[ThreadTask]
|
||||||
"""Tasks to execute in this step. If already attempted, may contain an error."""
|
"""Tasks to execute in this step. If already attempted, may contain an error."""
|
||||||
@@ -301,9 +302,9 @@ class Cron(TypedDict):
|
|||||||
|
|
||||||
cron_id: str
|
cron_id: str
|
||||||
"""The ID of the cron."""
|
"""The ID of the cron."""
|
||||||
thread_id: Optional[str]
|
thread_id: str | None
|
||||||
"""The ID of the thread."""
|
"""The ID of the thread."""
|
||||||
end_time: Optional[datetime]
|
end_time: datetime | None
|
||||||
"""The end date to stop running the cron."""
|
"""The end date to stop running the cron."""
|
||||||
schedule: str
|
schedule: str
|
||||||
"""The schedule to run, cron format."""
|
"""The schedule to run, cron format."""
|
||||||
@@ -318,25 +319,25 @@ class Cron(TypedDict):
|
|||||||
class RunCreate(TypedDict):
|
class RunCreate(TypedDict):
|
||||||
"""Defines the parameters for initiating a background run."""
|
"""Defines the parameters for initiating a background run."""
|
||||||
|
|
||||||
thread_id: Optional[str]
|
thread_id: str | None
|
||||||
"""The identifier of the thread to run. If not provided, the run is stateless."""
|
"""The identifier of the thread to run. If not provided, the run is stateless."""
|
||||||
assistant_id: str
|
assistant_id: str
|
||||||
"""The identifier of the assistant to use for this run."""
|
"""The identifier of the assistant to use for this run."""
|
||||||
input: Optional[dict]
|
input: dict | None
|
||||||
"""Initial input data for the run."""
|
"""Initial input data for the run."""
|
||||||
metadata: Optional[dict]
|
metadata: dict | None
|
||||||
"""Additional metadata to associate with the run."""
|
"""Additional metadata to associate with the run."""
|
||||||
config: Optional[Config]
|
config: Config | None
|
||||||
"""Configuration options for the run."""
|
"""Configuration options for the run."""
|
||||||
checkpoint_id: Optional[str]
|
checkpoint_id: str | None
|
||||||
"""The identifier of a checkpoint to resume from."""
|
"""The identifier of a checkpoint to resume from."""
|
||||||
interrupt_before: Optional[list[str]]
|
interrupt_before: list[str] | None
|
||||||
"""List of node names to interrupt execution before."""
|
"""List of node names to interrupt execution before."""
|
||||||
interrupt_after: Optional[list[str]]
|
interrupt_after: list[str] | None
|
||||||
"""List of node names to interrupt execution after."""
|
"""List of node names to interrupt execution after."""
|
||||||
webhook: Optional[str]
|
webhook: str | None
|
||||||
"""URL to send webhook notifications about the run's progress."""
|
"""URL to send webhook notifications about the run's progress."""
|
||||||
multitask_strategy: Optional[MultitaskStrategy]
|
multitask_strategy: MultitaskStrategy | None
|
||||||
"""Strategy for handling concurrent runs on the same thread."""
|
"""Strategy for handling concurrent runs on the same thread."""
|
||||||
|
|
||||||
|
|
||||||
@@ -376,7 +377,7 @@ class SearchItem(Item, total=False):
|
|||||||
searching a compatible store with a natural language query.
|
searching a compatible store with a natural language query.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
score: Optional[float]
|
score: float | None
|
||||||
|
|
||||||
|
|
||||||
class SearchItemsResponse(TypedDict):
|
class SearchItemsResponse(TypedDict):
|
||||||
@@ -404,7 +405,7 @@ class Send(TypedDict):
|
|||||||
|
|
||||||
node: str
|
node: str
|
||||||
"""The name of the target node to send the message to."""
|
"""The name of the target node to send the message to."""
|
||||||
input: Optional[dict[str, Any]]
|
input: dict[str, Any] | None
|
||||||
"""Optional dictionary containing the input data to be passed to the node.
|
"""Optional dictionary containing the input data to be passed to the node.
|
||||||
|
|
||||||
If None, the node will be called with no input."""
|
If None, the node will be called with no input."""
|
||||||
@@ -418,14 +419,14 @@ class Command(TypedDict, total=False):
|
|||||||
and resume from interruptions.
|
and resume from interruptions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
goto: Union[Send, str, Sequence[Union[Send, str]]]
|
goto: Send | str | Sequence[Send | str]
|
||||||
"""Specifies where execution should continue. Can be:
|
"""Specifies where execution should continue. Can be:
|
||||||
|
|
||||||
- A string node name to navigate to
|
- A string node name to navigate to
|
||||||
- A Send object to execute a node with specific input
|
- A Send object to execute a node with specific input
|
||||||
- A sequence of node names or Send objects to execute in order
|
- A sequence of node names or Send objects to execute in order
|
||||||
"""
|
"""
|
||||||
update: Union[dict[str, Any], Sequence[tuple[str, Any]]]
|
update: dict[str, Any] | Sequence[tuple[str, Any]]
|
||||||
"""Updates to apply to the graph's state. Can be:
|
"""Updates to apply to the graph's state. Can be:
|
||||||
|
|
||||||
- A dictionary of state updates to merge
|
- A dictionary of state updates to merge
|
||||||
@@ -443,5 +444,5 @@ class RunCreateMetadata(TypedDict):
|
|||||||
run_id: str
|
run_id: str
|
||||||
"""The ID of the run."""
|
"""The ID of the run."""
|
||||||
|
|
||||||
thread_id: Optional[str]
|
thread_id: str | None
|
||||||
"""The ID of the thread."""
|
"""The ID of the thread."""
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"""Adapted from httpx_sse to split lines on \n, \r, \r\n per the SSE spec."""
|
"""Adapted from httpx_sse to split lines on \n, \r, \r\n per the SSE spec."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import AsyncIterator, Iterator
|
from collections.abc import AsyncIterator, Iterator
|
||||||
from typing import Optional, Union
|
from typing import Union
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import orjson
|
import orjson
|
||||||
@@ -77,9 +79,9 @@ class SSEDecoder:
|
|||||||
self._event = ""
|
self._event = ""
|
||||||
self._data = bytearray()
|
self._data = bytearray()
|
||||||
self._last_event_id = ""
|
self._last_event_id = ""
|
||||||
self._retry: Optional[int] = None
|
self._retry: int | None = None
|
||||||
|
|
||||||
def decode(self, line: bytes) -> Optional[StreamPart]:
|
def decode(self, line: bytes) -> StreamPart | None:
|
||||||
# See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501
|
# See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501
|
||||||
|
|
||||||
if not line:
|
if not line:
|
||||||
|
|||||||
@@ -48,4 +48,4 @@ lint.select = [
|
|||||||
"B", # flake8-bugbear
|
"B", # flake8-bugbear
|
||||||
"I", # isort
|
"I", # isort
|
||||||
]
|
]
|
||||||
lint.ignore = ["E501", "B008", "UP007", "UP006"]
|
lint.ignore = ["E501", "B008"]
|
||||||
|
|||||||
Reference in New Issue
Block a user