mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-27 01:52:25 +02:00
Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d1ab66535 | ||
|
|
b4944fa0a0 | ||
|
|
4ddb4ae5a0 | ||
|
|
a99b66ae3f | ||
|
|
4793b3f5e1 | ||
|
|
3c5913aa29 | ||
|
|
43d5dbf174 | ||
|
|
974b6c7bf3 | ||
|
|
d492475c61 | ||
|
|
e5b09b8c20 | ||
|
|
c1d7cbd67c | ||
|
|
8c0b27a85b | ||
|
|
322d6c8dde | ||
|
|
af666a87fb | ||
|
|
6e9f54e5ae | ||
|
|
dd4a15b569 | ||
|
|
1122e5cbbb | ||
|
|
29311f054d | ||
|
|
3b2a237deb | ||
|
|
0cc3799298 | ||
|
|
ddeb9d7314 | ||
|
|
d65d0ec665 | ||
|
|
a937754132 | ||
|
|
8c698fc362 | ||
|
|
3a07fe2efb | ||
|
|
1191dc1b48 | ||
|
|
85e827ae85 | ||
|
|
55263c640f | ||
|
|
51f58279ba | ||
|
|
7d6a302858 | ||
|
|
d225ef074f | ||
|
|
eca419008c | ||
|
|
76994f3c9a | ||
|
|
ca4ebe1031 | ||
|
|
299179bc33 | ||
|
|
487fc8a8ca | ||
|
|
cf4de9a1a5 | ||
|
|
921c366708 | ||
|
|
243881ae90 | ||
|
|
af1f218e00 | ||
|
|
5db13dffbe | ||
|
|
a07ec9d8c8 | ||
|
|
65b64c1331 | ||
|
|
209e4cef7b | ||
|
|
98789acacc | ||
|
|
8e6019a6b8 | ||
|
|
666fb3f31d | ||
|
|
86c047eb31 |
@@ -0,0 +1,200 @@
|
||||
# How to Set Up a LangGraph.js Application for Deployment
|
||||
|
||||
A [LangGraph.js](https://langchain-ai.github.io/langgraphjs/) application must be configured with a [LangGraph API configuration file](../reference/cli.md#configuration-file) in order to be deployed to LangGraph Cloud (or to be self-hosted). This how-to guide discusses the basic steps to setup a LangGraph.js application for deployment using `package.json` to specify project dependencies.
|
||||
|
||||
This walkthrough is based on [this repository](https://github.com/langchain-ai/langgraphjs-studio-starter), which you can play around with to learn more about how to setup your LangGraph application for deployment.
|
||||
|
||||
The final repo structure will look something like this:
|
||||
|
||||
```bash
|
||||
my-app/
|
||||
├── src # all project code lies within here
|
||||
│ ├── utils # optional utilities for your graph
|
||||
│ │ ├── tools.ts # tools for your graph
|
||||
│ │ ├── nodes.ts # node functions for you graph
|
||||
│ │ └── state.ts # state definition of your graph
|
||||
│ └── agent.ts # code for constructing your graph
|
||||
├── package.json # package dependencies
|
||||
├── .env # environment variables
|
||||
└── langgraph.json # configuration file for LangGraph
|
||||
```
|
||||
|
||||
After each step, an example file directory is provided to demonstrate how code can be organized.
|
||||
|
||||
## Specify Dependencies
|
||||
|
||||
Dependencies can be specified in a `package.json`. If none of these files is created, then dependencies can be specified later in the [LangGraph API configuration file](#create-langgraph-api-config).
|
||||
|
||||
Example `package.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "langgraphjs-studio-starter",
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"dependencies": {
|
||||
"@langchain/community": "^0.2.31",
|
||||
"@langchain/core": "^0.2.31",
|
||||
"@langchain/langgraph": "^0.2.0",
|
||||
"@langchain/openai": "^0.2.8"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example file directory:
|
||||
|
||||
```bash
|
||||
my-app/
|
||||
└── package.json # package dependencies
|
||||
```
|
||||
|
||||
## Specify Environment Variables
|
||||
|
||||
Environment variables can optionally be specified in a file (e.g. `.env`). See the [Environment Variables reference](../reference/env_var.md) to configure additional variables for a deployment.
|
||||
|
||||
Example `.env` file:
|
||||
|
||||
```
|
||||
MY_ENV_VAR_1=foo
|
||||
MY_ENV_VAR_2=bar
|
||||
OPENAI_API_KEY=key
|
||||
TAVILY_API_KEY=key_2
|
||||
```
|
||||
|
||||
Example file directory:
|
||||
|
||||
```bash
|
||||
my-app/
|
||||
├── package.json
|
||||
└── .env # environment variables
|
||||
```
|
||||
|
||||
## Define Graphs
|
||||
|
||||
Implement your graphs! Graphs can be defined in a single file or multiple files. Make note of the variable names of each compiled graph to be included in the LangGraph application. The variable names will be used later when creating the [LangGraph API configuration file](../reference/cli.md#configuration-file).
|
||||
|
||||
Here is an example `agent.ts`:
|
||||
|
||||
```ts
|
||||
import type { AIMessage } from "@langchain/core/messages";
|
||||
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
|
||||
import { MessagesAnnotation, StateGraph } from "@langchain/langgraph";
|
||||
import { ToolNode } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
const tools = [
|
||||
new TavilySearchResults({ maxResults: 3, }),
|
||||
];
|
||||
|
||||
// Define the function that calls the model
|
||||
async function callModel(
|
||||
state: typeof MessagesAnnotation.State,
|
||||
) {
|
||||
/**
|
||||
* Call the LLM powering our agent.
|
||||
* Feel free to customize the prompt, model, and other logic!
|
||||
*/
|
||||
const model = new ChatOpenAI({
|
||||
model: "gpt-4o",
|
||||
}).bindTools(tools);
|
||||
|
||||
const response = await model.invoke([
|
||||
{
|
||||
role: "system",
|
||||
content: `You are a helpful assistant. The current date is ${new Date().getTime()}.`
|
||||
},
|
||||
...state.messages
|
||||
]);
|
||||
|
||||
// MessagesAnnotation supports returning a single message or array of messages
|
||||
return { messages: response };
|
||||
}
|
||||
|
||||
// Define the function that determines whether to continue or not
|
||||
function routeModelOutput(state: typeof MessagesAnnotation.State) {
|
||||
const messages = state.messages;
|
||||
const lastMessage: AIMessage = messages[messages.length - 1];
|
||||
// If the LLM is invoking tools, route there.
|
||||
if ((lastMessage?.tool_calls?.length ?? 0) > 0) {
|
||||
return "tools";
|
||||
}
|
||||
// Otherwise end the graph.
|
||||
return "__end__";
|
||||
}
|
||||
|
||||
// Define a new graph.
|
||||
// See https://langchain-ai.github.io/langgraphjs/how-tos/define-state/#getting-started for
|
||||
// more on defining custom graph states.
|
||||
const workflow = new StateGraph(MessagesAnnotation)
|
||||
// Define the two nodes we will cycle between
|
||||
.addNode("callModel", callModel)
|
||||
.addNode("tools", new ToolNode(tools))
|
||||
// Set the entrypoint as `callModel`
|
||||
// This means that this node is the first one called
|
||||
.addEdge("__start__", "callModel")
|
||||
.addConditionalEdges(
|
||||
// First, we define the edges' source node. We use `callModel`.
|
||||
// This means these are the edges taken after the `callModel` node is called.
|
||||
"callModel",
|
||||
// Next, we pass in the function that will determine the sink node(s), which
|
||||
// will be called after the source node is called.
|
||||
routeModelOutput,
|
||||
// List of the possible destinations the conditional edge can route to.
|
||||
// Required for conditional edges to properly render the graph in Studio
|
||||
[
|
||||
"tools",
|
||||
"__end__"
|
||||
],
|
||||
)
|
||||
// This means that after `tools` is called, `callModel` node is called next.
|
||||
.addEdge("tools", "callModel");
|
||||
|
||||
// Finally, we compile it!
|
||||
// This compiles it into a graph you can invoke and deploy.
|
||||
export const graph = workflow.compile();
|
||||
```
|
||||
|
||||
!!! info "Assign `CompiledGraph` to Variable"
|
||||
The build process for LangGraph Cloud requires that the `CompiledGraph` object be assigned to a variable at the top-level of a JavaScript module (alternatively, you can provide [a function that creates a graph](./graph_rebuild.md)).
|
||||
|
||||
Example file directory:
|
||||
|
||||
```bash
|
||||
my-app/
|
||||
├── src # all project code lies within here
|
||||
│ ├── utils # optional utilities for your graph
|
||||
│ │ ├── tools.ts # tools for your graph
|
||||
│ │ ├── nodes.ts # node functions for you graph
|
||||
│ │ └── state.ts # state definition of your graph
|
||||
│ └── agent.ts # code for constructing your graph
|
||||
├── package.json # package dependencies
|
||||
├── .env # environment variables
|
||||
└── langgraph.json # configuration file for LangGraph
|
||||
```
|
||||
|
||||
## Create LangGraph API Config
|
||||
|
||||
Create a [LangGraph API configuration file](../reference/cli.md#configuration-file) called `langgraph.json`. See the [LangGraph CLI reference](../reference/cli.md#configuration-file) for detailed explanations of each key in the JSON object of the configuration file.
|
||||
|
||||
Example `langgraph.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"node_version": "20",
|
||||
"dockerfile_lines": [],
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"agent": "./src/agent.ts:graph"
|
||||
},
|
||||
"env": ".env"
|
||||
}
|
||||
```
|
||||
|
||||
Note that the variable name of the `CompiledGraph` appears at the end of the value of each subkey in the top-level `graphs` key (i.e. `:<variable_name>`).
|
||||
|
||||
!!! info "Configuration Location"
|
||||
The LangGraph API configuration file must be placed in a directory that is at the same level or higher than the TypeScript files that contain compiled graphs and associated dependencies.
|
||||
|
||||
## Next
|
||||
|
||||
After you setup your project and place it in a github repo, it's time to [deploy your app](./cloud.md).
|
||||
@@ -43,15 +43,23 @@ If you don't define your conditional edges carefully, you might notice extra edg
|
||||
|
||||
### Solution 1: Include a path map
|
||||
|
||||
The first way to solve this is to add path maps to your conditional edges. A path map is just a dictionary that maps the possible outputs of your router function with the names of the nodes that each output corresponds to. The path map is passed as the third argument to the `add_conditional_edges` function like so:
|
||||
The first way to solve this is to add path maps to your conditional edges. A path map is just a dictionary or array that maps the possible outputs of your router function with the names of the nodes that each output corresponds to. The path map is passed as the third argument to the `add_conditional_edges` function like so:
|
||||
|
||||
```python
|
||||
graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"})
|
||||
```
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"})
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```ts
|
||||
graph.addConditionalEdges("node_a", routingFunction, ["node_b", "node_c"]);
|
||||
```
|
||||
|
||||
In this case, the routing function returns either True or False, which map to `node_b` and `node_c` respectively.
|
||||
|
||||
### Solution 2: Update the typing of the router
|
||||
### Solution 2: Update the typing of the router (Python only)
|
||||
|
||||
Instead of passing a path map, you can also be explicit about the typing of your routing function by specifying the nodes it can map to using the `Literal` python definition. Here is an example of how to define a routing function in that way:
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ LangGraph Cloud gives you best in class observability, testing, and hosting serv
|
||||
|
||||
- [How to set up app for deployment (requirements.txt)](../deployment/setup.md)
|
||||
- [How to set up app for deployment (pyproject.toml)](../deployment/setup_pyproject.md)
|
||||
- [How to set up app for deployment (JavaScript)](../deployment/setup_javascript.md)
|
||||
- [How to test locally](../deployment/test_locally.md)
|
||||
- [How to deploy to LangGraph cloud](../deployment/cloud.md)
|
||||
- [How to self-host](../deployment/self_hosted.md)
|
||||
|
||||
@@ -44,7 +44,7 @@ Now, let's import our required packages and instantiate our client, assistant, a
|
||||
const thread = await client.threads.create();
|
||||
```
|
||||
|
||||
Now we can start our two runs and join the second on euntil it has completed:
|
||||
Now we can start our two runs and join the second one until it has completed:
|
||||
|
||||
=== "Python"
|
||||
|
||||
|
||||
+168
-31
@@ -14,13 +14,25 @@ This tutorial will use:
|
||||
|
||||
1. Create a new application with the following directory and files:
|
||||
|
||||
=== "Python"
|
||||
|
||||
<my-app>/
|
||||
|-- agent.py # code for your LangGraph agent
|
||||
|-- requirements.txt # Python packages required for your graph
|
||||
|-- langgraph.json # configuration file for LangGraph
|
||||
|-- .env # environment files with API keys
|
||||
|
||||
2. The `agent.py` file should contain Python code for defining your graph. The following code is a simple example, the important thing is that at some point in your file you compile your graph and assign the compiled graph to a variable (in this case the `graph` variable). This example code uses `create_react_agent`, a prebuilt agent, read more about it [here](..//concepts/agentic_concepts.md#react-agent).
|
||||
=== "Javascript"
|
||||
|
||||
<my-app>/
|
||||
|-- agent.ts # code for your LangGraph agent
|
||||
|-- package.json # Javascript packages required for your graph
|
||||
|-- langgraph.json # configuration file for LangGraph
|
||||
|-- .env # environment files with API keys
|
||||
|
||||
2. The `agent.py`/`agent.ts` file should contain code for defining your graph. The following code is a simple example, the important thing is that at some point in your file you compile your graph and assign the compiled graph to a variable (in this case the `graph` variable). This example code uses `create_react_agent`, a prebuilt agent. You can read more about it [here](../concepts/agentic_concepts.md#react-agent).
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
@@ -34,14 +46,53 @@ This tutorial will use:
|
||||
graph = create_react_agent(model, tools)
|
||||
```
|
||||
|
||||
3. The `requirements.txt` file should contain any dependencies for your graph(s). In this case we only require four packages for our graph to run:
|
||||
=== "Javascript"
|
||||
|
||||
langgraph
|
||||
langchain_anthropic
|
||||
tavily-python
|
||||
langchain_community
|
||||
```ts
|
||||
import { ChatAnthropic } from "@langchain/anthropic";
|
||||
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
|
||||
import { createReactAgent } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
4. The [`langgraph.json`][langgraph.json] file is a configuration file that describes what graph(s) you are going to host. In this case we only have one graph to host: the compiled `graph` object from `agent.py`.
|
||||
const model = new ChatAnthropic({
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
});
|
||||
|
||||
const tools = [
|
||||
new TavilySearchResults({ maxResults: 3, }),
|
||||
];
|
||||
|
||||
export const graph = createReactAgent({ llm: model, tools });
|
||||
```
|
||||
|
||||
3. The `requirements.txt`/`package.json` file should contain any dependencies for your graph(s). In this case we only require four packages for our graph to run:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python
|
||||
langgraph
|
||||
langchain_anthropic
|
||||
tavily-python
|
||||
langchain_community
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
{
|
||||
"name": "my-app",
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"dependencies": {
|
||||
"@langchain/community": "^0.2.31",
|
||||
"@langchain/core": "^0.2.31",
|
||||
"@langchain/langgraph": "0.2.0",
|
||||
"@langchain/openai": "^0.2.8"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. The [`langgraph.json`][langgraph.json] file is a configuration file that describes what graph(s) you are going to host. In this case we only have one graph to host: the compiled `graph` object from `agent.py`/`agent.ts`.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -53,7 +104,21 @@ This tutorial will use:
|
||||
}
|
||||
```
|
||||
|
||||
Learn more about the LangGraph CLI configuration file [here](./reference/cli.md#configuration-file).
|
||||
=== "Javascript"
|
||||
|
||||
```json
|
||||
{
|
||||
"node_version": "20",
|
||||
"dockerfile_lines": [],
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"agent": "./src/agent.ts:graph"
|
||||
},
|
||||
"env": ".env"
|
||||
}
|
||||
```
|
||||
|
||||
Learn more about the LangGraph CLI configuration file [here](./reference/cli.md#configuration-file).
|
||||
|
||||
5. The `.env` file should have any environment variables needed to run your graph. This will only be used for local testing, so if you are not testing locally you can skip this step. NOTE: if you do add this, you should NOT check this into git. For this graph, we need two environment variables:
|
||||
|
||||
@@ -206,36 +271,108 @@ export LANGSMITH_API_KEY=...
|
||||
|
||||
The first thing to do when using the SDK is to setup our client, access our assistant, and create a thread to execute a run on:
|
||||
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
=== "Python"
|
||||
|
||||
# Replace this with the URL of your own deployed graph
|
||||
URL = "https://chatbot-23a570f3210f52a7b167f09f6158e3b3-ffoprvkqsa-uc.a.run.app"
|
||||
client = get_client(url=URL)
|
||||
```python
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
# Search all hosted graphs
|
||||
assistants = await client.assistants.search()
|
||||
# In this example we select the first assistant since we are only hosting a single graph
|
||||
assistant = assistants[0]
|
||||
client = get_client(url=<DEPLOYMENT_URL>)
|
||||
# get default assistant
|
||||
assistants = await client.assistants.search()
|
||||
assistant = [a for a in assistants if not a["config"]][0]
|
||||
# create thread
|
||||
thread = await client.threads.create()
|
||||
print(thread)
|
||||
```
|
||||
|
||||
# We create a thread for tracking the state of our run
|
||||
thread = await client.threads.create()
|
||||
```
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
|
||||
const client = new Client({ apiUrl: <DEPLOYMENT_URL> });
|
||||
// get default assistant
|
||||
const assistants = await client.assistants.search();
|
||||
const assistant = assistants.find(a => !a.config);
|
||||
// create thread
|
||||
const thread = await client.threads.create();
|
||||
console.log(thread)
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/assistants/search \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"limit": 10,
|
||||
"offset": 0
|
||||
}' | jq -c 'map(select(.config == null or .config == {})) | .[0]' && \
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{}'
|
||||
```
|
||||
|
||||
We can then execute a run on the thread:
|
||||
|
||||
```python
|
||||
input = {"messages":[{"role": "user", "content": "Hello! My name is Bagatur and I am 26 years old."}]}
|
||||
=== "Python"
|
||||
|
||||
async for chunk in client.runs.stream(
|
||||
thread['thread_id'],
|
||||
assistant["assistant_id"],
|
||||
input=input,
|
||||
stream_mode="updates",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
```
|
||||
```python
|
||||
input = {"messages":[{"role": "user", "content": "Hello! My name is Bagatur and I am 26 years old."}]}
|
||||
|
||||
async for chunk in client.runs.stream(
|
||||
thread['thread_id'],
|
||||
assistant["assistant_id"],
|
||||
input=input,
|
||||
stream_mode="updates",
|
||||
):
|
||||
if chunk.data and chunk.event != "metadata":
|
||||
print(chunk.data)
|
||||
```
|
||||
|
||||
=== "Javascript"
|
||||
|
||||
```js
|
||||
const input = { "messages":[{ "role": "user", "content": "Hello! My name is Bagatur and I am 26 years old." }] };
|
||||
|
||||
const streamResponse = client.runs.stream(
|
||||
thread["thread_id"],
|
||||
assistant["assistant_id"],
|
||||
{
|
||||
input,
|
||||
}
|
||||
);
|
||||
for await (const chunk of streamResponse) {
|
||||
if (chunk.data && chunk.event !== "metadata" ) {
|
||||
console.log(chunk.data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "CURL"
|
||||
|
||||
```bash
|
||||
curl --request POST \
|
||||
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"assistant_id\": <ASSISTANT_ID>,
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"Hello! My name is Bagatur and I am 26 years old.\"}]},
|
||||
}" | sed 's/\r$//' | awk '
|
||||
/^event:/ { event = $2 }
|
||||
/^data:/ {
|
||||
json_data = substr($0, index($0, $2))
|
||||
|
||||
if (event != "metadata") {
|
||||
print json_data
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
|
||||
Output:
|
||||
|
||||
{'agent': {'messages': [{'content': "Hi Bagatur! It's nice to meet you. How can I assist you today?", 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_9cb5d38cf7'}, 'type': 'ai', 'name': None, 'id': 'run-c89118b7-1b1e-42b9-a85d-c43fe99881cd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}
|
||||
|
||||
|
||||
@@ -197,6 +197,7 @@ nav:
|
||||
- Setup:
|
||||
- Setup App: "cloud/deployment/setup.md"
|
||||
- Setup App (pyproject.toml): "cloud/deployment/setup_pyproject.md"
|
||||
- Setup App (JavaScript): "cloud/deployment/setup_javascript.md"
|
||||
- Rebuild Graph at Runtime: "cloud/deployment/graph_rebuild.md"
|
||||
- Test App Locally: "cloud/deployment/test_locally.md"
|
||||
- Deployment:
|
||||
|
||||
@@ -99,46 +99,50 @@
|
||||
" # Backup - we will use this to \"reset\" our DB in each section\n",
|
||||
" shutil.copy(local_file, backup_file)\n",
|
||||
"# Convert the flights to present time for our tutorial\n",
|
||||
"conn = sqlite3.connect(local_file)\n",
|
||||
"cursor = conn.cursor()\n",
|
||||
"def update_dates(file):\n",
|
||||
" shutil.copy(backup_file, file)\n",
|
||||
" conn = sqlite3.connect(file)\n",
|
||||
" cursor = conn.cursor()\n",
|
||||
"\n",
|
||||
"tables = pd.read_sql(\n",
|
||||
" \"SELECT name FROM sqlite_master WHERE type='table';\", conn\n",
|
||||
").name.tolist()\n",
|
||||
"tdf = {}\n",
|
||||
"for t in tables:\n",
|
||||
" tdf[t] = pd.read_sql(f\"SELECT * from {t}\", conn)\n",
|
||||
" tables = pd.read_sql(\n",
|
||||
" \"SELECT name FROM sqlite_master WHERE type='table';\", conn\n",
|
||||
" ).name.tolist()\n",
|
||||
" tdf = {}\n",
|
||||
" for t in tables:\n",
|
||||
" tdf[t] = pd.read_sql(f\"SELECT * from {t}\", conn)\n",
|
||||
"\n",
|
||||
"example_time = pd.to_datetime(\n",
|
||||
" tdf[\"flights\"][\"actual_departure\"].replace(\"\\\\N\", pd.NaT)\n",
|
||||
").max()\n",
|
||||
"current_time = pd.to_datetime(\"now\").tz_localize(example_time.tz)\n",
|
||||
"time_diff = current_time - example_time\n",
|
||||
" example_time = pd.to_datetime(\n",
|
||||
" tdf[\"flights\"][\"actual_departure\"].replace(\"\\\\N\", pd.NaT)\n",
|
||||
" ).max()\n",
|
||||
" current_time = pd.to_datetime(\"now\").tz_localize(example_time.tz)\n",
|
||||
" time_diff = current_time - example_time\n",
|
||||
"\n",
|
||||
"tdf[\"bookings\"][\"book_date\"] = (\n",
|
||||
" pd.to_datetime(tdf[\"bookings\"][\"book_date\"].replace(\"\\\\N\", pd.NaT), utc=True)\n",
|
||||
" + time_diff\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"datetime_columns = [\n",
|
||||
" \"scheduled_departure\",\n",
|
||||
" \"scheduled_arrival\",\n",
|
||||
" \"actual_departure\",\n",
|
||||
" \"actual_arrival\",\n",
|
||||
"]\n",
|
||||
"for column in datetime_columns:\n",
|
||||
" tdf[\"flights\"][column] = (\n",
|
||||
" pd.to_datetime(tdf[\"flights\"][column].replace(\"\\\\N\", pd.NaT)) + time_diff\n",
|
||||
" tdf[\"bookings\"][\"book_date\"] = (\n",
|
||||
" pd.to_datetime(tdf[\"bookings\"][\"book_date\"].replace(\"\\\\N\", pd.NaT), utc=True)\n",
|
||||
" + time_diff\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"for table_name, df in tdf.items():\n",
|
||||
" df.to_sql(table_name, conn, if_exists=\"replace\", index=False)\n",
|
||||
"del df\n",
|
||||
"del tdf\n",
|
||||
"conn.commit()\n",
|
||||
"conn.close()\n",
|
||||
" datetime_columns = [\n",
|
||||
" \"scheduled_departure\",\n",
|
||||
" \"scheduled_arrival\",\n",
|
||||
" \"actual_departure\",\n",
|
||||
" \"actual_arrival\",\n",
|
||||
" ]\n",
|
||||
" for column in datetime_columns:\n",
|
||||
" tdf[\"flights\"][column] = (\n",
|
||||
" pd.to_datetime(tdf[\"flights\"][column].replace(\"\\\\N\", pd.NaT)) + time_diff\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
"db = local_file # We'll be using this local file as our DB in this tutorial"
|
||||
" for table_name, df in tdf.items():\n",
|
||||
" df.to_sql(table_name, conn, if_exists=\"replace\", index=False)\n",
|
||||
" del df\n",
|
||||
" del tdf\n",
|
||||
" conn.commit()\n",
|
||||
" conn.close()\n",
|
||||
"\n",
|
||||
" return file\n",
|
||||
"\n",
|
||||
"db = update_dates(local_file)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1750,7 +1754,7 @@
|
||||
"]\n",
|
||||
"\n",
|
||||
"# Update with the backup file so we can restart from the original place in each section\n",
|
||||
"shutil.copy(backup_file, db)\n",
|
||||
"db = update_dates(db)\n",
|
||||
"thread_id = str(uuid.uuid4())\n",
|
||||
"\n",
|
||||
"config = {\n",
|
||||
@@ -2304,7 +2308,7 @@
|
||||
"import uuid\n",
|
||||
"\n",
|
||||
"# Update with the backup file so we can restart from the original place in each section\n",
|
||||
"shutil.copy(backup_file, db)\n",
|
||||
"db = update_dates(db)\n",
|
||||
"thread_id = str(uuid.uuid4())\n",
|
||||
"\n",
|
||||
"config = {\n",
|
||||
@@ -2908,7 +2912,7 @@
|
||||
"import uuid\n",
|
||||
"\n",
|
||||
"# Update with the backup file so we can restart from the original place in each section\n",
|
||||
"shutil.copy(backup_file, db)\n",
|
||||
"db = update_dates(db)\n",
|
||||
"thread_id = str(uuid.uuid4())\n",
|
||||
"\n",
|
||||
"config = {\n",
|
||||
@@ -4330,7 +4334,7 @@
|
||||
"import uuid\n",
|
||||
"\n",
|
||||
"# Update with the backup file so we can restart from the original place in each section\n",
|
||||
"shutil.copy(backup_file, db)\n",
|
||||
"db = update_dates(db)\n",
|
||||
"thread_id = str(uuid.uuid4())\n",
|
||||
"\n",
|
||||
"config = {\n",
|
||||
|
||||
@@ -1504,7 +1504,7 @@
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from langgraph.graph import StateGraph\n",
|
||||
"from langgraph.graph.message import add_messages\n",
|
||||
"from langgraph.prebuilt import ToolNode\n",
|
||||
"from langgraph.prebuilt import ToolNode, tools_condition\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class State(TypedDict):\n",
|
||||
|
||||
@@ -646,11 +646,12 @@
|
||||
"def _parse_joiner_output(decision: JoinOutputs) -> List[BaseMessage]:\n",
|
||||
" response = [AIMessage(content=f\"Thought: {decision.thought}\")]\n",
|
||||
" if isinstance(decision.action, Replan):\n",
|
||||
" return response + [\n",
|
||||
" return {\"messages\": response + [\n",
|
||||
" SystemMessage(\n",
|
||||
" content=f\"Context from last attempt: {decision.action.feedback}\"\n",
|
||||
" )\n",
|
||||
" ]\n",
|
||||
" }\n",
|
||||
" else:\n",
|
||||
" return {\"messages\": response + [AIMessage(content=decision.action.response)]}\n",
|
||||
"\n",
|
||||
@@ -933,6 +934,46 @@
|
||||
"print(step['join']['messages'][-1].content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "f9487866",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"#### Complex Replanning Example\n",
|
||||
"\n",
|
||||
"This question is likely to prompt the Replan functionality, but it may need to be run multiple times to see this in action."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "391d6931",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'plan_and_schedule': {'messages': [FunctionMessage(content=\"[{'url': 'https://www.timeanddate.com/weather/japan/tokyo', 'content': '88 / 84 °F. 13. 87 / 82 °F. 14. 84 / 80 °F. Detailed forecast for 14 days. Need some help? Current weather in Tokyo and forecast for today, tomorrow, and next 14 days.'}]\", additional_kwargs={'idx': 1, 'args': {'query': 'current temperature in Tokyo'}}, name='tavily_search_results_json', tool_call_id=1), FunctionMessage(content='join', additional_kwargs={'idx': 2, 'args': ()}, name='join', tool_call_id=2)]}}\n",
|
||||
"{'join': {'messages': [AIMessage(content=\"Thought: The search result provides the current temperature in Tokyo but does not explicitly state which temperature (88 / 84 °F) corresponds to the current condition. It seems to be a range, possibly the day's high and low. Without a clear indication of the exact current temperature, it's challenging to provide a precise flashcard summary.\", id='8ef2a131-69db-4180-a76e-fd9d6f4037c1'), SystemMessage(content='Context from last attempt: The information provided does not explicitly state the current temperature in Tokyo; it provides a temperature range without specifying which is the current temperature. Need to find a source that gives the exact current temperature in Tokyo for a precise flashcard summary.', id='f5bd752c-b068-459a-8d9e-bd1f1b5fa4fe')]}}\n",
|
||||
"{'plan_and_schedule': {'messages': [FunctionMessage(content='join', additional_kwargs={'idx': 3, 'args': ()}, name='join', tool_call_id=3)]}}\n",
|
||||
"{'join': {'messages': [AIMessage(content=\"Thought: The search result provides a temperature range for Tokyo but does not specify the current temperature. This makes it challenging to create a precise flashcard without an exact current temperature. The user's request cannot be fully satisfied without this detail.\", id='3cc41891-4f47-4453-8edf-b989926ab25e'), SystemMessage(content='Context from last attempt: The search did not provide an exact current temperature for Tokyo, making it impossible to create a precise flashcard. A source that explicitly states the current temperature is needed for an accurate response.', id='96290b41-a4c4-4ab5-829a-89cc31dfe6c8')]}}\n",
|
||||
"{'plan_and_schedule': {'messages': [FunctionMessage(content='join', additional_kwargs={'idx': 4, 'args': ()}, name='join', tool_call_id=4)]}}\n",
|
||||
"{'join': {'messages': [AIMessage(content=\"Thought: The search result provides a temperature range for Tokyo but does not specify the current temperature. This makes it challenging to create a precise flashcard without an exact current temperature. The user's request cannot be fully satisfied without this detail.\", id='4724b242-ddb8-47e6-b235-de25de54fe45'), AIMessage(content='I was unable to find the exact current temperature in Tokyo. However, the temperature range for today in Tokyo is between 88°F and 84°F. For the most accurate and up-to-date temperature, I recommend checking a reliable weather forecasting website or app.', id='40e29a47-a001-4f65-a18f-65c2931d1ae5')]}}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for step in chain.stream({\"messages\":\n",
|
||||
" [\n",
|
||||
" HumanMessage(\n",
|
||||
" content=\"Find the current temperature in Tokyo, then, respond with a flashcard summarizing this information\"\n",
|
||||
" )\n",
|
||||
" ]}\n",
|
||||
"):\n",
|
||||
" print(step)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c647d5f3-5e00-4449-9cec-5a9f438c9cff",
|
||||
|
||||
@@ -219,7 +219,7 @@
|
||||
"workflow = StateGraph(AgentState)\n",
|
||||
"workflow.add_node(\"Researcher\", research_node)\n",
|
||||
"workflow.add_node(\"Coder\", code_node)\n",
|
||||
"workflow.add_node(\"supervisor\", supervisor_chain)"
|
||||
"workflow.add_node(\"supervisor\", supervisor_agent)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -132,8 +132,6 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from psycopg.rows import dict_row\n",
|
||||
"\n",
|
||||
"connection_kwargs = {\n",
|
||||
" \"autocommit\": True,\n",
|
||||
" \"prepare_threshold\": 0,\n",
|
||||
@@ -161,15 +159,13 @@
|
||||
"source": [
|
||||
"from psycopg_pool import ConnectionPool\n",
|
||||
"\n",
|
||||
"pool = ConnectionPool(\n",
|
||||
"with ConnectionPool(\n",
|
||||
" # Example configuration\n",
|
||||
" conninfo=DB_URI,\n",
|
||||
" max_size=20,\n",
|
||||
" kwargs=connection_kwargs,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"with pool.connection() as conn:\n",
|
||||
" checkpointer = PostgresSaver(conn)\n",
|
||||
") as pool:\n",
|
||||
" checkpointer = PostgresSaver(pool)\n",
|
||||
"\n",
|
||||
" # NOTE: you need to call .setup() the first time you're using your checkpointer\n",
|
||||
" checkpointer.setup()\n",
|
||||
@@ -394,8 +390,8 @@
|
||||
" conninfo=DB_URI,\n",
|
||||
" max_size=20,\n",
|
||||
" kwargs=connection_kwargs,\n",
|
||||
") as pool, pool.connection() as conn:\n",
|
||||
" checkpointer = AsyncPostgresSaver(conn)\n",
|
||||
") as pool:\n",
|
||||
" checkpointer = AsyncPostgresSaver(pool)\n",
|
||||
"\n",
|
||||
" # NOTE: you need to call .setup() the first time you're using your checkpointer\n",
|
||||
" # await checkpointer.setup()\n",
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
"source": [
|
||||
"## Agent state\n",
|
||||
" \n",
|
||||
"We will defined a graph.\n",
|
||||
"We will define a graph.\n",
|
||||
"\n",
|
||||
"A `state` object that it passes around to each node.\n",
|
||||
"\n",
|
||||
|
||||
@@ -429,6 +429,14 @@
|
||||
"]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def find_tool_calls_react(messages):\n",
|
||||
" \"\"\"\n",
|
||||
" Find all tool calls in the messages returned\n",
|
||||
" \"\"\"\n",
|
||||
" tool_calls = [tc['name'] for m in messages['messages'] for tc in getattr(m, 'tool_calls', [])]\n",
|
||||
" return tool_calls\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def check_trajectory_react(root_run: Run, example: Example) -> dict:\n",
|
||||
" \"\"\"\n",
|
||||
" Check if all expected tools are called in exact order and without any additional tool calls.\n",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any, AsyncIterator, Optional, Union
|
||||
from typing import Any, AsyncIterator, Iterator, List, Optional, Union
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline
|
||||
@@ -51,6 +51,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
self.conn = conn
|
||||
self.pipe = pipe
|
||||
self.lock = asyncio.Lock()
|
||||
self.loop = asyncio.get_running_loop()
|
||||
|
||||
@classmethod
|
||||
@asynccontextmanager
|
||||
@@ -329,3 +330,96 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
binary=True, row_factory=dict_row
|
||||
) as cur:
|
||||
yield cur
|
||||
|
||||
def list(
|
||||
self,
|
||||
config: Optional[RunnableConfig],
|
||||
*,
|
||||
filter: Optional[dict[str, Any]] = None,
|
||||
before: Optional[RunnableConfig] = None,
|
||||
limit: Optional[int] = None,
|
||||
) -> Iterator[CheckpointTuple]:
|
||||
"""List checkpoints from the database.
|
||||
|
||||
This method retrieves a list of checkpoint tuples from the Postgres database based
|
||||
on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
|
||||
|
||||
Args:
|
||||
config (Optional[RunnableConfig]): Base configuration for filtering checkpoints.
|
||||
filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata.
|
||||
before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
||||
limit (Optional[int]): Maximum number of checkpoints to return.
|
||||
|
||||
Yields:
|
||||
Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
|
||||
"""
|
||||
aiter_ = self.alist(config, filter=filter, before=before, limit=limit)
|
||||
while True:
|
||||
try:
|
||||
yield asyncio.run_coroutine_threadsafe(
|
||||
anext(aiter_), self.loop
|
||||
).result()
|
||||
except StopAsyncIteration:
|
||||
break
|
||||
|
||||
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||
"""Get a checkpoint tuple from the database.
|
||||
|
||||
This method retrieves a checkpoint tuple from the Postgres database based on the
|
||||
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||
the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint
|
||||
for the given thread ID is retrieved.
|
||||
|
||||
Args:
|
||||
config (RunnableConfig): The config to use for retrieving the checkpoint.
|
||||
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
"""
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
self.aget_tuple(config), self.loop
|
||||
).result()
|
||||
|
||||
def put(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
checkpoint: Checkpoint,
|
||||
metadata: CheckpointMetadata,
|
||||
new_versions: ChannelVersions,
|
||||
) -> RunnableConfig:
|
||||
"""Save a checkpoint to the database.
|
||||
|
||||
This method saves a checkpoint to the Postgres database. The checkpoint is associated
|
||||
with the provided config and its parent config (if any).
|
||||
|
||||
Args:
|
||||
config (RunnableConfig): The config to associate with the checkpoint.
|
||||
checkpoint (Checkpoint): The checkpoint to save.
|
||||
metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.
|
||||
new_versions (ChannelVersions): New channel versions as of this write.
|
||||
|
||||
Returns:
|
||||
RunnableConfig: Updated configuration after storing the checkpoint.
|
||||
"""
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
self.aput(config, checkpoint, metadata, new_versions), self.loop
|
||||
).result()
|
||||
|
||||
def put_writes(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
writes: List[tuple[str, Any]],
|
||||
task_id: str,
|
||||
) -> None:
|
||||
"""Store intermediate writes linked to a checkpoint.
|
||||
|
||||
This method saves intermediate writes associated with a checkpoint to the database.
|
||||
|
||||
Args:
|
||||
config (RunnableConfig): Configuration of the related checkpoint.
|
||||
writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
|
||||
task_id (str): Identifier for the task creating the writes.
|
||||
"""
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
self.aput_writes(config, writes, task_id), self.loop
|
||||
).result()
|
||||
|
||||
Generated
+3
-4
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
@@ -266,7 +266,7 @@ tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "1.0.6"
|
||||
version = "1.0.8"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -766,7 +766,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||
@@ -972,4 +971,4 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
content-hash = "b139531e8c6f4e24cea4bdfc29d111d1ea000a6a8a604ba81be4bff0977a2466"
|
||||
content-hash = "e294b6996aa6c8f671e6aaf65be8b4aba94c18e2f237dd3ee0e1b777849ce8a8"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "1.0.5"
|
||||
version = "1.0.6"
|
||||
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0,<4.0"
|
||||
langgraph-checkpoint = "^1.0.1"
|
||||
langgraph-checkpoint = "^1.0.8"
|
||||
orjson = ">=3.10.1"
|
||||
psycopg = "^3.0.0"
|
||||
psycopg-pool = "^3.0.0"
|
||||
|
||||
@@ -104,10 +104,12 @@ class SqliteSaver(BaseCheckpointSaver):
|
||||
with SqliteSaver.from_conn_string("checkpoints.sqlite") as memory:
|
||||
...
|
||||
"""
|
||||
with sqlite3.connect(
|
||||
conn_string,
|
||||
# https://ricardoanderegg.com/posts/python-sqlite-thread-safety/
|
||||
check_same_thread=False,
|
||||
with closing(
|
||||
sqlite3.connect(
|
||||
conn_string,
|
||||
# https://ricardoanderegg.com/posts/python-sqlite-thread-safety/
|
||||
check_same_thread=False,
|
||||
)
|
||||
) as conn:
|
||||
yield SqliteSaver(conn)
|
||||
|
||||
@@ -163,14 +165,15 @@ class SqliteSaver(BaseCheckpointSaver):
|
||||
Yields:
|
||||
sqlite3.Cursor: A cursor for the SQLite database.
|
||||
"""
|
||||
self.setup()
|
||||
cur = self.conn.cursor()
|
||||
try:
|
||||
yield cur
|
||||
finally:
|
||||
if transaction:
|
||||
self.conn.commit()
|
||||
cur.close()
|
||||
with self.lock:
|
||||
self.setup()
|
||||
cur = self.conn.cursor()
|
||||
try:
|
||||
yield cur
|
||||
finally:
|
||||
if transaction:
|
||||
self.conn.commit()
|
||||
cur.close()
|
||||
|
||||
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||
"""Get a checkpoint tuple from the database.
|
||||
@@ -396,7 +399,7 @@ class SqliteSaver(BaseCheckpointSaver):
|
||||
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
||||
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
|
||||
serialized_metadata = self.jsonplus_serde.dumps(metadata)
|
||||
with self.lock, self.cursor() as cur:
|
||||
with self.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
@@ -432,7 +435,7 @@ class SqliteSaver(BaseCheckpointSaver):
|
||||
writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
|
||||
task_id (str): Identifier for the task creating the writes.
|
||||
"""
|
||||
with self.lock, self.cursor() as cur:
|
||||
with self.cursor() as cur:
|
||||
cur.executemany(
|
||||
"INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import asyncio
|
||||
import functools
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import (
|
||||
Any,
|
||||
AsyncIterator,
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
@@ -31,20 +31,6 @@ from langgraph.checkpoint.sqlite.utils import search_where
|
||||
T = TypeVar("T", bound=callable)
|
||||
|
||||
|
||||
def not_implemented_sync_method(func: T) -> T:
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
raise NotImplementedError(
|
||||
"The AsyncSqliteSaver does not support synchronous methods. "
|
||||
"Consider using the SqliteSaver instead.\n"
|
||||
"from langgraph.checkpoint.sqlite import SqliteSaver\n"
|
||||
"See https://langchain-ai.github.io/langgraph/reference/checkpoints/langgraph.checkpoint.sqlite.SqliteSaver "
|
||||
"for more information."
|
||||
)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
"""An asynchronous checkpoint saver that stores checkpoints in a SQLite database.
|
||||
|
||||
@@ -132,6 +118,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
self.jsonplus_serde = JsonPlusSerializer()
|
||||
self.conn = conn
|
||||
self.lock = asyncio.Lock()
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.is_setup = False
|
||||
|
||||
@classmethod
|
||||
@@ -150,16 +137,24 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
async with aiosqlite.connect(conn_string) as conn:
|
||||
yield AsyncSqliteSaver(conn)
|
||||
|
||||
@not_implemented_sync_method
|
||||
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||
"""Get a checkpoint tuple from the database.
|
||||
|
||||
Note:
|
||||
This method is not implemented for the AsyncSqliteSaver. Use `aget` instead.
|
||||
Or consider using the [SqliteSaver][sqlitesaver] checkpointer.
|
||||
"""
|
||||
This method retrieves a checkpoint tuple from the SQLite database based on the
|
||||
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
|
||||
the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
|
||||
for the given thread ID is retrieved.
|
||||
|
||||
Args:
|
||||
config (RunnableConfig): The config to use for retrieving the checkpoint.
|
||||
|
||||
Returns:
|
||||
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
|
||||
"""
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
self.aget_tuple(config), self.loop
|
||||
).result()
|
||||
|
||||
@not_implemented_sync_method
|
||||
def list(
|
||||
self,
|
||||
config: Optional[RunnableConfig],
|
||||
@@ -168,21 +163,60 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
before: Optional[RunnableConfig] = None,
|
||||
limit: Optional[int] = None,
|
||||
) -> Iterator[CheckpointTuple]:
|
||||
"""List checkpoints from the database.
|
||||
"""List checkpoints from the database asynchronously.
|
||||
|
||||
Note:
|
||||
This method is not implemented for the AsyncSqliteSaver. Use `alist` instead.
|
||||
Or consider using the [SqliteSaver][sqlitesaver] checkpointer.
|
||||
This method retrieves a list of checkpoint tuples from the SQLite database based
|
||||
on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first).
|
||||
|
||||
Args:
|
||||
config (Optional[RunnableConfig]): Base configuration for filtering checkpoints.
|
||||
filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata.
|
||||
before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
|
||||
limit (Optional[int]): Maximum number of checkpoints to return.
|
||||
|
||||
Yields:
|
||||
Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
|
||||
"""
|
||||
aiter_ = self.alist(config, filter=filter, before=before, limit=limit)
|
||||
while True:
|
||||
try:
|
||||
yield asyncio.run_coroutine_threadsafe(
|
||||
anext(aiter_), self.loop
|
||||
).result()
|
||||
except StopAsyncIteration:
|
||||
break
|
||||
|
||||
@not_implemented_sync_method
|
||||
def put(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
checkpoint: Checkpoint,
|
||||
metadata: CheckpointMetadata,
|
||||
new_versions: ChannelVersions,
|
||||
) -> RunnableConfig:
|
||||
"""Save a checkpoint to the database. FOO"""
|
||||
"""Save a checkpoint to the database.
|
||||
|
||||
This method saves a checkpoint to the SQLite database. The checkpoint is associated
|
||||
with the provided config and its parent config (if any).
|
||||
|
||||
Args:
|
||||
config (RunnableConfig): The config to associate with the checkpoint.
|
||||
checkpoint (Checkpoint): The checkpoint to save.
|
||||
metadata (CheckpointMetadata): Additional metadata to save with the checkpoint.
|
||||
new_versions (ChannelVersions): New channel versions as of this write.
|
||||
|
||||
Returns:
|
||||
RunnableConfig: Updated configuration after storing the checkpoint.
|
||||
"""
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
self.aput(config, checkpoint, metadata, new_versions), self.loop
|
||||
).result()
|
||||
|
||||
def put_writes(
|
||||
self, config: RunnableConfig, writes: List[Tuple[str, Any]], task_id: str
|
||||
) -> None:
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
self.aput_writes(config, writes, task_id), self.loop
|
||||
).result()
|
||||
|
||||
async def setup(self) -> None:
|
||||
"""Set up the checkpoint database asynchronously.
|
||||
@@ -242,7 +276,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
"""
|
||||
await self.setup()
|
||||
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
||||
async with self.conn.cursor() as cur:
|
||||
async with self.lock, self.conn.cursor() as cur:
|
||||
# find the latest checkpoint for the thread_id
|
||||
if checkpoint_id := get_checkpoint_id(config):
|
||||
await cur.execute(
|
||||
@@ -337,7 +371,9 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
ORDER BY checkpoint_id DESC"""
|
||||
if limit:
|
||||
query += f" LIMIT {limit}"
|
||||
async with self.conn.execute(query, params) as cur, self.conn.cursor() as wcur:
|
||||
async with self.lock, self.conn.execute(
|
||||
query, params
|
||||
) as cur, self.conn.cursor() as wcur:
|
||||
async for (
|
||||
thread_id,
|
||||
checkpoint_ns,
|
||||
@@ -404,7 +440,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
||||
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
|
||||
serialized_metadata = self.jsonplus_serde.dumps(metadata)
|
||||
async with self.conn.execute(
|
||||
async with self.lock, self.conn.execute(
|
||||
"INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
str(config["configurable"]["thread_id"]),
|
||||
@@ -441,7 +477,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
|
||||
task_id (str): Identifier for the task creating the writes.
|
||||
"""
|
||||
await self.setup()
|
||||
async with self.conn.cursor() as cur:
|
||||
async with self.lock, self.conn.cursor() as cur:
|
||||
await cur.executemany(
|
||||
"INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[
|
||||
|
||||
Generated
+3
-4
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiosqlite"
|
||||
@@ -252,7 +252,7 @@ tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "1.0.1"
|
||||
version = "1.0.8"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -651,7 +651,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||
@@ -835,4 +834,4 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.9.0"
|
||||
content-hash = "d50ec7c6b55075d19193e080cc95d3b0998b9efa1a0ed407bd6055b5b5e867e0"
|
||||
content-hash = "752a22dc2b57a0818a3a4d9bf5f62226ba7f8e0c458892551bf8b4c93723dbc1"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "1.0.1"
|
||||
version = "1.0.3"
|
||||
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0"
|
||||
langgraph-checkpoint = "^1.0.1"
|
||||
langgraph-checkpoint = "^1.0.8"
|
||||
aiosqlite = "^0.20.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
||||
@@ -52,11 +52,11 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
return obj.to_json()
|
||||
elif hasattr(obj, "model_dump") and callable(obj.model_dump):
|
||||
return self._encode_constructor_args(
|
||||
obj.__class__, method="model_construct", kwargs=obj.model_dump()
|
||||
obj.__class__, method=[None, "model_construct"], kwargs=obj.model_dump()
|
||||
)
|
||||
elif hasattr(obj, "dict") and callable(obj.dict):
|
||||
return self._encode_constructor_args(
|
||||
obj.__class__, method="construct", kwargs=obj.dict()
|
||||
obj.__class__, method=[None, "construct"], kwargs=obj.dict()
|
||||
)
|
||||
elif isinstance(obj, pathlib.Path):
|
||||
return self._encode_constructor_args(pathlib.Path, args=obj.parts)
|
||||
@@ -136,21 +136,30 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
# Import class
|
||||
cls = getattr(mod, name)
|
||||
# Instantiate class
|
||||
if value["method"] is not None:
|
||||
method = getattr(cls, value["method"])
|
||||
if isinstance(value["method"], str):
|
||||
methods = [getattr(cls, value["method"])]
|
||||
elif isinstance(value["method"], list):
|
||||
methods = [
|
||||
cls if method is None else getattr(cls, method)
|
||||
for method in value["method"]
|
||||
]
|
||||
else:
|
||||
method = cls
|
||||
if isclass(method) and issubclass(method, BaseException):
|
||||
return None
|
||||
if value["args"] and value["kwargs"]:
|
||||
return method(*value["args"], **value["kwargs"])
|
||||
elif value["args"]:
|
||||
return method(*value["args"])
|
||||
elif value["kwargs"]:
|
||||
return method(**value["kwargs"])
|
||||
else:
|
||||
return method()
|
||||
except (ImportError, AttributeError, TypeError):
|
||||
methods = [cls]
|
||||
for method in methods:
|
||||
try:
|
||||
if isclass(method) and issubclass(method, BaseException):
|
||||
return None
|
||||
if value["args"] and value["kwargs"]:
|
||||
return method(*value["args"], **value["kwargs"])
|
||||
elif value["args"]:
|
||||
return method(*value["args"])
|
||||
elif value["kwargs"]:
|
||||
return method(**value["kwargs"])
|
||||
else:
|
||||
return method()
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
return LC_REVIVER(value)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "1.0.7"
|
||||
version = "1.0.8"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -122,7 +122,7 @@ def test_serde_jsonplus() -> None:
|
||||
|
||||
assert dumped == (
|
||||
"json",
|
||||
b"""{"path": {"lc": 2, "type": "constructor", "id": ["pathlib", "Path"], "method": null, "args": ["foo", "bar"], "kwargs": {}}, "re": {"lc": 2, "type": "constructor", "id": ["re", "compile"], "method": null, "args": ["foo", 48], "kwargs": {}}, "decimal": {"lc": 2, "type": "constructor", "id": ["decimal", "Decimal"], "method": null, "args": ["1.10101"], "kwargs": {}}, "ip4": {"lc": 2, "type": "constructor", "id": ["ipaddress", "IPv4Address"], "method": null, "args": ["192.168.0.1"], "kwargs": {}}, "deque": {"lc": 2, "type": "constructor", "id": ["collections", "deque"], "method": null, "args": [[1, 2, 3]], "kwargs": {}}, "tzn": {"lc": 2, "type": "constructor", "id": ["zoneinfo", "ZoneInfo"], "method": null, "args": ["America/New_York"], "kwargs": {}}, "date": {"lc": 2, "type": "constructor", "id": ["datetime", "date"], "method": null, "args": [2024, 4, 19], "kwargs": {}}, "time": {"lc": 2, "type": "constructor", "id": ["datetime", "time"], "method": null, "args": [23, 4, 57, 51022, {"lc": 2, "type": "constructor", "id": ["datetime", "timezone"], "method": null, "args": [{"lc": 2, "type": "constructor", "id": ["datetime", "timedelta"], "method": null, "args": [0, 86340, 0], "kwargs": {}}], "kwargs": {}}], "kwargs": {"fold": 0}}, "uid": {"lc": 2, "type": "constructor", "id": ["uuid", "UUID"], "method": null, "args": ["00000000000000000000000000000001"], "kwargs": {}}, "timestamp": {"lc": 2, "type": "constructor", "id": ["datetime", "datetime"], "method": "fromisoformat", "args": ["2024-04-19T23:04:57.051022+23:59"], "kwargs": {}}, "my_slotted_class": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclassWSlots"], "method": null, "args": [], "kwargs": {"foo": "bar", "bar": 2}}, "my_dataclass": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclass"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_enum": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyEnum"], "method": null, "args": ["foo"], "kwargs": {}}, "my_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyPydantic"], "method": "model_construct", "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_funny_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyFunnyPydantic"], "method": "construct", "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "person": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "Person"], "method": null, "args": [], "kwargs": {"name": "foo"}}, "a_bool": true, "a_none": null, "a_str": "foo", "a_str_nuc": "foo\\u0000", "a_str_uc": "foo \xe2\x9b\xb0\xef\xb8\x8f", "a_str_ucuc": "foo \xe2\x9b\xb0\xef\xb8\x8f\\u0000", "a_str_ucucuc": "foo \\\\u26f0\\\\ufe0f", "text": ["Hello", "Python", "Surrogate", "Example", "String", "With", "Surrogates", "Embedded", "In", "The", "Text", "\xe6\x94\xb6\xe8\x8a\xb1\xf0\x9f\x99\x84\xc2\xb7\xe5\x88\xb0"], "an_int": 1, "a_float": 1.1, "runnable_map": {"lc": 1, "type": "constructor", "id": ["langchain", "schema", "runnable", "RunnableParallel"], "kwargs": {"steps__": {}}, "name": "RunnableParallel<>", "graph": {"nodes": [{"id": 0, "type": "schema", "data": "Parallel<>Input"}, {"id": 1, "type": "schema", "data": "Parallel<>Output"}], "edges": []}}}""",
|
||||
b"""{"path": {"lc": 2, "type": "constructor", "id": ["pathlib", "Path"], "method": null, "args": ["foo", "bar"], "kwargs": {}}, "re": {"lc": 2, "type": "constructor", "id": ["re", "compile"], "method": null, "args": ["foo", 48], "kwargs": {}}, "decimal": {"lc": 2, "type": "constructor", "id": ["decimal", "Decimal"], "method": null, "args": ["1.10101"], "kwargs": {}}, "ip4": {"lc": 2, "type": "constructor", "id": ["ipaddress", "IPv4Address"], "method": null, "args": ["192.168.0.1"], "kwargs": {}}, "deque": {"lc": 2, "type": "constructor", "id": ["collections", "deque"], "method": null, "args": [[1, 2, 3]], "kwargs": {}}, "tzn": {"lc": 2, "type": "constructor", "id": ["zoneinfo", "ZoneInfo"], "method": null, "args": ["America/New_York"], "kwargs": {}}, "date": {"lc": 2, "type": "constructor", "id": ["datetime", "date"], "method": null, "args": [2024, 4, 19], "kwargs": {}}, "time": {"lc": 2, "type": "constructor", "id": ["datetime", "time"], "method": null, "args": [23, 4, 57, 51022, {"lc": 2, "type": "constructor", "id": ["datetime", "timezone"], "method": null, "args": [{"lc": 2, "type": "constructor", "id": ["datetime", "timedelta"], "method": null, "args": [0, 86340, 0], "kwargs": {}}], "kwargs": {}}], "kwargs": {"fold": 0}}, "uid": {"lc": 2, "type": "constructor", "id": ["uuid", "UUID"], "method": null, "args": ["00000000000000000000000000000001"], "kwargs": {}}, "timestamp": {"lc": 2, "type": "constructor", "id": ["datetime", "datetime"], "method": "fromisoformat", "args": ["2024-04-19T23:04:57.051022+23:59"], "kwargs": {}}, "my_slotted_class": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclassWSlots"], "method": null, "args": [], "kwargs": {"foo": "bar", "bar": 2}}, "my_dataclass": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclass"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_enum": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyEnum"], "method": null, "args": ["foo"], "kwargs": {}}, "my_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyPydantic"], "method": [null, "model_construct"], "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_funny_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyFunnyPydantic"], "method": [null, "construct"], "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "person": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "Person"], "method": null, "args": [], "kwargs": {"name": "foo"}}, "a_bool": true, "a_none": null, "a_str": "foo", "a_str_nuc": "foo\\u0000", "a_str_uc": "foo \xe2\x9b\xb0\xef\xb8\x8f", "a_str_ucuc": "foo \xe2\x9b\xb0\xef\xb8\x8f\\u0000", "a_str_ucucuc": "foo \\\\u26f0\\\\ufe0f", "text": ["Hello", "Python", "Surrogate", "Example", "String", "With", "Surrogates", "Embedded", "In", "The", "Text", "\xe6\x94\xb6\xe8\x8a\xb1\xf0\x9f\x99\x84\xc2\xb7\xe5\x88\xb0"], "an_int": 1, "a_float": 1.1, "runnable_map": {"lc": 1, "type": "constructor", "id": ["langchain", "schema", "runnable", "RunnableParallel"], "kwargs": {"steps__": {}}, "name": "RunnableParallel<>", "graph": {"nodes": [{"id": 0, "type": "schema", "data": "Parallel<>Input"}, {"id": 1, "type": "schema", "data": "Parallel<>Output"}], "edges": []}}}""",
|
||||
)
|
||||
|
||||
assert serde.loads_typed(dumped) == {
|
||||
|
||||
Generated
+4
-2080
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,6 @@ package-mode = false
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0,<4.0"
|
||||
jupyter = "^1.0.0"
|
||||
langgraph-cli = {path = "../../cli", develop = true}
|
||||
langgraph-sdk = {path = "../../sdk-py", develop = true}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ coverage:
|
||||
--cov-report term-missing:skip-covered
|
||||
|
||||
start-postgres:
|
||||
docker compose -f tests/compose-postgres.yml up -V --force-recreate --wait
|
||||
docker compose -f tests/compose-postgres.yml up -V --force-recreate --wait --remove-orphans
|
||||
|
||||
stop-postgres:
|
||||
docker compose -f tests/compose-postgres.yml down -v
|
||||
|
||||
@@ -424,6 +424,10 @@ class Graph:
|
||||
class CompiledGraph(Pregel):
|
||||
builder: Graph
|
||||
|
||||
def __init__(self, *, builder: Graph, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.builder = builder
|
||||
|
||||
def attach_node(self, key: str, node: NodeSpec) -> None:
|
||||
self.channels[key] = EphemeralValue(Any)
|
||||
self.nodes[key] = (
|
||||
@@ -484,6 +488,10 @@ class CompiledGraph(Pregel):
|
||||
START: graph.add_node(self.get_input_schema(config), START)
|
||||
}
|
||||
end_nodes: dict[str, DrawableNode] = {}
|
||||
if xray:
|
||||
subgraphs = dict(self.get_subgraphs())
|
||||
else:
|
||||
subgraphs = {}
|
||||
|
||||
def add_edge(
|
||||
start: str, end: str, label: Optional[str] = None, conditional: bool = False
|
||||
@@ -503,11 +511,11 @@ class CompiledGraph(Pregel):
|
||||
metadata["__interrupt"] = "after"
|
||||
if xray:
|
||||
subgraph = (
|
||||
node.get_graph(
|
||||
subgraphs[key].get_graph(
|
||||
config=config,
|
||||
xray=xray - 1 if isinstance(xray, int) and xray > 0 else xray,
|
||||
)
|
||||
if isinstance(node, CompiledGraph)
|
||||
if key in subgraphs
|
||||
else node.get_graph(config=config)
|
||||
)
|
||||
subgraph.trim_first_node()
|
||||
|
||||
@@ -17,12 +17,10 @@ from typing import (
|
||||
overload,
|
||||
)
|
||||
|
||||
from langchain_core.pydantic_v1 import BaseModel
|
||||
from langchain_core.runnables import Runnable, RunnableConfig
|
||||
from langchain_core.runnables.base import RunnableLike
|
||||
from langchain_core.runnables.utils import (
|
||||
create_model,
|
||||
)
|
||||
from langchain_core.runnables.utils import create_model
|
||||
from pydantic import BaseModel
|
||||
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.channels.binop import BinaryOperatorAggregate
|
||||
@@ -33,14 +31,7 @@ from langgraph.channels.named_barrier_value import NamedBarrierValue
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.constants import NS_END, NS_SEP, TAG_HIDDEN
|
||||
from langgraph.errors import InvalidUpdateError
|
||||
from langgraph.graph.graph import (
|
||||
END,
|
||||
START,
|
||||
Branch,
|
||||
CompiledGraph,
|
||||
Graph,
|
||||
Send,
|
||||
)
|
||||
from langgraph.graph.graph import END, START, Branch, CompiledGraph, Graph, Send
|
||||
from langgraph.managed.base import (
|
||||
ChannelKeyPlaceholder,
|
||||
ChannelTypePlaceholder,
|
||||
@@ -53,7 +44,7 @@ from langgraph.pregel.read import ChannelRead, PregelNode
|
||||
from langgraph.pregel.types import All, RetryPolicy
|
||||
from langgraph.pregel.write import SKIP_WRITE, ChannelWrite, ChannelWriteEntry
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.utils import RunnableCallable, coerce_to_runnable
|
||||
from langgraph.utils import RunnableCallable, coerce_to_runnable, get_field_default
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -498,7 +489,16 @@ class CompiledStateGraph(CompiledGraph):
|
||||
return create_model( # type: ignore[call-overload]
|
||||
self.get_name("Input"),
|
||||
**{
|
||||
k: (self.channels[k].UpdateType, None)
|
||||
k: (
|
||||
self.channels[k].UpdateType,
|
||||
(
|
||||
get_field_default(
|
||||
k,
|
||||
self.channels[k].UpdateType,
|
||||
self.builder.input,
|
||||
)
|
||||
),
|
||||
)
|
||||
for k in self.builder.schemas[self.builder.input]
|
||||
if isinstance(self.channels[k], BaseChannel)
|
||||
},
|
||||
|
||||
@@ -25,11 +25,10 @@ from uuid import UUID, uuid5
|
||||
|
||||
from langchain_core.globals import get_debug
|
||||
from langchain_core.load.dump import dumpd
|
||||
from langchain_core.pydantic_v1 import BaseModel, Field, root_validator
|
||||
from langchain_core.runnables import (
|
||||
Runnable,
|
||||
RunnableLambda,
|
||||
RunnableSequence,
|
||||
RunnableSerializable,
|
||||
)
|
||||
from langchain_core.runnables.base import Input, Output, coerce_to_runnable
|
||||
from langchain_core.runnables.config import (
|
||||
@@ -43,9 +42,11 @@ from langchain_core.runnables.config import (
|
||||
from langchain_core.runnables.utils import (
|
||||
ConfigurableFieldSpec,
|
||||
create_model,
|
||||
get_function_nonlocals,
|
||||
get_unique_config_specs,
|
||||
)
|
||||
from langchain_core.tracers._streaming import _StreamingCallbackHandler
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.channels.base import (
|
||||
@@ -102,6 +103,7 @@ from langgraph.pregel.utils import (
|
||||
from langgraph.pregel.validate import validate_graph, validate_keys
|
||||
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.utils import RunnableCallable
|
||||
|
||||
WriteValue = Union[
|
||||
Runnable[Input, Output],
|
||||
@@ -183,16 +185,10 @@ class Channel:
|
||||
)
|
||||
|
||||
|
||||
class Pregel(
|
||||
RunnableSerializable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]
|
||||
):
|
||||
class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]):
|
||||
nodes: Mapping[str, PregelNode]
|
||||
|
||||
channels: Mapping[str, Union[BaseChannel, ManagedValueSpec]] = Field(
|
||||
default_factory=dict
|
||||
)
|
||||
|
||||
auto_validate: bool = True
|
||||
channels: Mapping[str, Union[BaseChannel, ManagedValueSpec]]
|
||||
|
||||
stream_mode: StreamMode = "values"
|
||||
"""Mode to stream output, defaults to 'values'."""
|
||||
@@ -202,16 +198,16 @@ class Pregel(
|
||||
stream_channels: Optional[Union[str, Sequence[str]]] = None
|
||||
"""Channels to stream, defaults to all channels not in reserved channels"""
|
||||
|
||||
interrupt_after_nodes: Union[All, Sequence[str]] = Field(default_factory=list)
|
||||
interrupt_after_nodes: Union[All, Sequence[str]]
|
||||
|
||||
interrupt_before_nodes: Union[All, Sequence[str]] = Field(default_factory=list)
|
||||
interrupt_before_nodes: Union[All, Sequence[str]]
|
||||
|
||||
input_channels: Union[str, Sequence[str]]
|
||||
|
||||
step_timeout: Optional[float] = None
|
||||
"""Maximum time to wait for a step to complete, in seconds. Defaults to None."""
|
||||
|
||||
debug: bool = Field(default_factory=get_debug)
|
||||
debug: bool
|
||||
"""Whether to print debug information during execution. Defaults to False."""
|
||||
|
||||
checkpointer: Optional[BaseCheckpointSaver] = None
|
||||
@@ -229,36 +225,50 @@ class Pregel(
|
||||
|
||||
name: str = "LangGraph"
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
nodes: Mapping[str, PregelNode],
|
||||
channels: Mapping[str, Union[BaseChannel, ManagedValueSpec]] = None,
|
||||
auto_validate: bool = True,
|
||||
stream_mode: StreamMode = "values",
|
||||
output_channels: Union[str, Sequence[str]],
|
||||
stream_channels: Optional[Union[str, Sequence[str]]] = None,
|
||||
interrupt_after_nodes: Union[All, Sequence[str]] = (),
|
||||
interrupt_before_nodes: Union[All, Sequence[str]] = (),
|
||||
input_channels: Union[str, Sequence[str]],
|
||||
step_timeout: Optional[float] = None,
|
||||
debug: Optional[bool] = None,
|
||||
checkpointer: Optional[BaseCheckpointSaver] = None,
|
||||
store: Optional[BaseStore] = None,
|
||||
retry_policy: Optional[RetryPolicy] = None,
|
||||
config_type: Optional[Type[Any]] = None,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
name: str = "LangGraph",
|
||||
) -> None:
|
||||
self.nodes = nodes
|
||||
self.channels = channels or {}
|
||||
self.stream_mode = stream_mode
|
||||
self.output_channels = output_channels
|
||||
self.stream_channels = stream_channels
|
||||
self.interrupt_after_nodes = interrupt_after_nodes
|
||||
self.interrupt_before_nodes = interrupt_before_nodes
|
||||
self.input_channels = input_channels
|
||||
self.step_timeout = step_timeout
|
||||
self.debug = debug if debug is not None else get_debug()
|
||||
self.checkpointer = checkpointer
|
||||
self.store = store
|
||||
self.retry_policy = retry_policy
|
||||
self.config_type = config_type
|
||||
self.config = config
|
||||
self.name = name
|
||||
if auto_validate:
|
||||
self.validate()
|
||||
|
||||
def with_config(self, config: RunnableConfig | None = None, **kwargs: Any) -> Self:
|
||||
return self.copy(
|
||||
update={"config": cast(RunnableConfig, {**(config or {}), **kwargs})}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
"""Return whether the graph can be serialized by Langchain."""
|
||||
return True
|
||||
|
||||
@root_validator(skip_on_failure=True)
|
||||
def validate_on_init(cls, values: dict[str, Any]) -> dict[str, Any]:
|
||||
if not values["auto_validate"]:
|
||||
return values
|
||||
validate_graph(
|
||||
values["nodes"],
|
||||
values["channels"],
|
||||
values["input_channels"],
|
||||
values["output_channels"],
|
||||
values["stream_channels"],
|
||||
values["interrupt_after_nodes"],
|
||||
values["interrupt_before_nodes"],
|
||||
)
|
||||
if values["interrupt_after_nodes"] or values["interrupt_before_nodes"]:
|
||||
if not values["checkpointer"]:
|
||||
raise ValueError("Interrupts require a checkpointer")
|
||||
return values
|
||||
attrs = {**self.__dict__}
|
||||
attrs["config"] = merge_configs(self.config, config, kwargs)
|
||||
return self.__class__(**attrs)
|
||||
|
||||
def validate(self) -> Self:
|
||||
validate_graph(
|
||||
@@ -356,13 +366,26 @@ class Pregel(
|
||||
for name, node in self.nodes.items():
|
||||
# find the subgraph, if any
|
||||
graph: Optional[Pregel] = None
|
||||
if isinstance(node.bound, Pregel):
|
||||
graph = node.bound
|
||||
elif isinstance(node.bound, RunnableSequence):
|
||||
for runnable in node.bound.steps:
|
||||
if isinstance(runnable, Pregel):
|
||||
graph = runnable
|
||||
break
|
||||
candidates = [node.bound]
|
||||
for candidate in candidates:
|
||||
if isinstance(candidate, Pregel):
|
||||
graph = candidate
|
||||
break
|
||||
elif isinstance(candidate, RunnableSequence):
|
||||
candidates.extend(candidate.steps)
|
||||
elif isinstance(candidate, RunnableLambda):
|
||||
candidates.extend(candidate.deps)
|
||||
elif isinstance(candidate, RunnableCallable):
|
||||
if candidate.func is not None:
|
||||
candidates.extend(
|
||||
nl.__self__ if hasattr(nl, "__self__") else nl
|
||||
for nl in get_function_nonlocals(candidate.func)
|
||||
)
|
||||
if candidate.afunc is not None:
|
||||
candidates.extend(
|
||||
nl.__self__ if hasattr(nl, "__self__") else nl
|
||||
for nl in get_function_nonlocals(candidate.afunc)
|
||||
)
|
||||
# if found, yield recursively
|
||||
if graph:
|
||||
yield name, graph
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, Mapping, Optional, Sequence, Union
|
||||
from typing import (
|
||||
Any,
|
||||
AsyncIterator,
|
||||
Callable,
|
||||
Iterator,
|
||||
Mapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
Union,
|
||||
)
|
||||
|
||||
from langchain_core.pydantic_v1 import Field
|
||||
from langchain_core.runnables import (
|
||||
Runnable,
|
||||
RunnableConfig,
|
||||
@@ -10,7 +18,7 @@ from langchain_core.runnables import (
|
||||
RunnableSequence,
|
||||
RunnableSerializable,
|
||||
)
|
||||
from langchain_core.runnables.base import Other, RunnableBindingBase, coerce_to_runnable
|
||||
from langchain_core.runnables.base import Input, Other, Output, coerce_to_runnable
|
||||
from langchain_core.runnables.config import merge_configs
|
||||
from langchain_core.runnables.utils import ConfigurableFieldSpec
|
||||
|
||||
@@ -99,20 +107,47 @@ class ChannelRead(RunnableCallable):
|
||||
DEFAULT_BOUND: RunnablePassthrough = RunnablePassthrough()
|
||||
|
||||
|
||||
class PregelNode(RunnableBindingBase):
|
||||
class PregelNode(Runnable):
|
||||
channels: Union[list[str], Mapping[str, str]]
|
||||
|
||||
triggers: list[str] = Field(default_factory=list)
|
||||
triggers: list[str]
|
||||
|
||||
mapper: Optional[Callable[[Any], Any]] = None
|
||||
mapper: Optional[Callable[[Any], Any]]
|
||||
|
||||
writers: list[Runnable] = Field(default_factory=list)
|
||||
writers: list[Runnable]
|
||||
|
||||
bound: Runnable[Any, Any] = Field(default=DEFAULT_BOUND)
|
||||
bound: Runnable[Any, Any]
|
||||
|
||||
kwargs: Mapping[str, Any] = Field(default_factory=dict)
|
||||
retry_policy: Optional[RetryPolicy]
|
||||
|
||||
retry_policy: Optional[RetryPolicy] = None
|
||||
config: RunnableConfig
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
channels: Union[list[str], Mapping[str, str]],
|
||||
triggers: Sequence[str],
|
||||
mapper: Optional[Callable[[Any], Any]] = None,
|
||||
writers: Optional[list[Runnable]] = None,
|
||||
tags: Optional[list[str]] = None,
|
||||
metadata: Optional[Mapping[str, Any]] = None,
|
||||
bound: Optional[Runnable[Any, Any]] = None,
|
||||
retry_policy: Optional[RetryPolicy] = None,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
) -> None:
|
||||
self.channels = channels
|
||||
self.triggers = list(triggers)
|
||||
self.mapper = mapper
|
||||
self.writers = writers or []
|
||||
self.bound = bound if bound is not None else DEFAULT_BOUND
|
||||
self.retry_policy = retry_policy
|
||||
self.config = merge_configs(
|
||||
config, {"tags": tags or [], "metadata": metadata or {}}
|
||||
)
|
||||
|
||||
def copy(self, update: dict[str, Any]) -> PregelNode:
|
||||
attrs = {**self.__dict__, **update}
|
||||
return PregelNode(**attrs)
|
||||
|
||||
def get_writers(self) -> list[Runnable]:
|
||||
"""Get writers with optimizations applied."""
|
||||
@@ -145,38 +180,6 @@ class PregelNode(RunnableBindingBase):
|
||||
else:
|
||||
return self.bound
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
channels: Union[list[str], Mapping[str, str]],
|
||||
triggers: Sequence[str],
|
||||
mapper: Optional[Callable[[Any], Any]] = None,
|
||||
writers: Optional[list[Runnable]] = None,
|
||||
tags: Optional[list[str]] = None,
|
||||
metadata: Optional[Mapping[str, Any]] = None,
|
||||
bound: Optional[Runnable[Any, Any]] = None,
|
||||
kwargs: Optional[Mapping[str, Any]] = None,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
retry_policy: Optional[RetryPolicy] = None,
|
||||
**other_kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
channels=channels,
|
||||
triggers=triggers,
|
||||
mapper=mapper,
|
||||
writers=writers or [],
|
||||
bound=bound or DEFAULT_BOUND,
|
||||
kwargs=kwargs or {},
|
||||
retry_policy=retry_policy,
|
||||
config=merge_configs(
|
||||
config, {"tags": tags or [], "metadata": metadata or {}}
|
||||
),
|
||||
**other_kwargs,
|
||||
)
|
||||
|
||||
def __repr_args__(self) -> Any:
|
||||
return [(k, v) for k, v in super().__repr_args__() if k != "bound"]
|
||||
|
||||
def join(self, channels: Sequence[str]) -> PregelNode:
|
||||
assert isinstance(channels, list) or isinstance(
|
||||
channels, tuple
|
||||
@@ -226,3 +229,42 @@ class PregelNode(RunnableBindingBase):
|
||||
],
|
||||
) -> RunnableSerializable:
|
||||
raise NotImplementedError()
|
||||
|
||||
def invoke(
|
||||
self,
|
||||
input: Input,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
**kwargs: Optional[Any],
|
||||
) -> Output:
|
||||
return self.bound.invoke(input, merge_configs(self.config, config), **kwargs)
|
||||
|
||||
async def ainvoke(
|
||||
self,
|
||||
input: Input,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
**kwargs: Optional[Any],
|
||||
) -> Output:
|
||||
return await self.bound.ainvoke(
|
||||
input, merge_configs(self.config, config), **kwargs
|
||||
)
|
||||
|
||||
def stream(
|
||||
self,
|
||||
input: Input,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
**kwargs: Optional[Any],
|
||||
) -> Iterator[Output]:
|
||||
yield from self.bound.stream(
|
||||
input, merge_configs(self.config, config), **kwargs
|
||||
)
|
||||
|
||||
async def astream(
|
||||
self,
|
||||
input: Input,
|
||||
config: Optional[RunnableConfig] = None,
|
||||
**kwargs: Optional[Any],
|
||||
) -> AsyncIterator[Output]:
|
||||
async for item in self.bound.astream(
|
||||
input, merge_configs(self.config, config), **kwargs
|
||||
):
|
||||
yield item
|
||||
|
||||
@@ -4,7 +4,7 @@ import inspect
|
||||
import sys
|
||||
from contextvars import copy_context
|
||||
from functools import partial, wraps
|
||||
from typing import Any, AsyncIterator, Awaitable, Callable, Optional
|
||||
from typing import Any, AsyncIterator, Awaitable, Callable, Optional, Type, Union
|
||||
|
||||
from langchain_core.runnables.base import (
|
||||
Runnable,
|
||||
@@ -19,7 +19,14 @@ from langchain_core.runnables.config import (
|
||||
var_child_runnable_config,
|
||||
)
|
||||
from langchain_core.runnables.utils import accepts_config
|
||||
from typing_extensions import TypeGuard
|
||||
from typing_extensions import (
|
||||
Annotated,
|
||||
NotRequired,
|
||||
ReadOnly,
|
||||
Required,
|
||||
TypeGuard,
|
||||
get_origin,
|
||||
)
|
||||
|
||||
try:
|
||||
from langchain_core.runnables.config import _set_config_context
|
||||
@@ -34,8 +41,6 @@ except ImportError:
|
||||
class StrEnum(str, enum.Enum):
|
||||
"""A string enum."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RunnableCallable(Runnable):
|
||||
"""A much simpler version of RunnableLambda that requires sync and async functions."""
|
||||
@@ -183,3 +188,95 @@ def coerce_to_runnable(thing: RunnableLike, *, name: str, trace: bool) -> Runnab
|
||||
f"Expected a Runnable, callable or dict."
|
||||
f"Instead got an unsupported type: {type(thing)}"
|
||||
)
|
||||
|
||||
|
||||
def _is_optional_type(type_: Any) -> bool:
|
||||
"""Check if a type is Optional."""
|
||||
|
||||
if hasattr(type_, "__origin__") and hasattr(type_, "__args__"):
|
||||
origin = get_origin(type_)
|
||||
if origin is Optional:
|
||||
return True
|
||||
if origin is Union:
|
||||
return any(
|
||||
arg is type(None) or _is_optional_type(arg) for arg in type_.__args__
|
||||
)
|
||||
if origin is Annotated:
|
||||
return _is_optional_type(type_.__args__[0])
|
||||
return origin is None
|
||||
if hasattr(type_, "__bound__") and type_.__bound__ is not None:
|
||||
return _is_optional_type(type_.__bound__)
|
||||
return type_ is None
|
||||
|
||||
|
||||
def _is_required_type(type_: Any) -> Optional[bool]:
|
||||
"""Check if an annotation is marked as Required/NotRequired.
|
||||
|
||||
Returns:
|
||||
- True if required
|
||||
- False if not required
|
||||
- None if not annotated with either
|
||||
"""
|
||||
origin = get_origin(type_)
|
||||
if origin is Required:
|
||||
return True
|
||||
if origin is NotRequired:
|
||||
return False
|
||||
if origin is Annotated or getattr(origin, "__args__", None):
|
||||
# See https://typing.readthedocs.io/en/latest/spec/typeddict.html#interaction-with-annotated
|
||||
return _is_required_type(type_.__args__[0])
|
||||
return None
|
||||
|
||||
|
||||
def _is_readonly_type(type_: Any) -> bool:
|
||||
"""Check if an annotation is marked as ReadOnly.
|
||||
|
||||
Returns:
|
||||
- True if is read only
|
||||
- False if not read only
|
||||
"""
|
||||
|
||||
# See: https://typing.readthedocs.io/en/latest/spec/typeddict.html#typing-readonly-type-qualifier
|
||||
origin = get_origin(type_)
|
||||
if origin is Annotated:
|
||||
return _is_readonly_type(type_.__args__[0])
|
||||
if origin is ReadOnly:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
_DEFAULT_KEYS = frozenset()
|
||||
|
||||
|
||||
def get_field_default(name: str, type_: Any, schema: Type[Any]) -> Any:
|
||||
"""Determine the default value for a field in a state schema.
|
||||
|
||||
This is based on:
|
||||
If TypedDict:
|
||||
- Required/NotRequired
|
||||
- total=False -> everything optional
|
||||
- Type annotation (Optional/Union[None])
|
||||
"""
|
||||
optional_keys = getattr(schema, "__optional_keys__", _DEFAULT_KEYS)
|
||||
irq = _is_required_type(type_)
|
||||
if name in optional_keys:
|
||||
# Either total=False or explicit NotRequired.
|
||||
# No type annotation trumps this.
|
||||
if irq:
|
||||
# Unless it's earlier versions of python & explicit Required
|
||||
return ...
|
||||
return None
|
||||
if irq is not None:
|
||||
if irq:
|
||||
# Handle Required[<type>]
|
||||
# (we already handled NotRequired and total=False)
|
||||
return ...
|
||||
# Handle NotRequired[<type>] for earlier versions of python
|
||||
return None
|
||||
# Note, we ignore ReadOnly attributes,
|
||||
# as they don't make much sense. (we don't care if you mutate the state in your node)
|
||||
# and mutating state in your node has no effect on our graph state.
|
||||
# Base case is the annotation
|
||||
if _is_optional_type(type_):
|
||||
return None
|
||||
return ...
|
||||
|
||||
Generated
+16
-35
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
@@ -1634,13 +1634,13 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (>
|
||||
|
||||
[[package]]
|
||||
name = "jupyterlab"
|
||||
version = "4.2.2"
|
||||
version = "4.2.5"
|
||||
description = "JupyterLab computational environment"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "jupyterlab-4.2.2-py3-none-any.whl", hash = "sha256:59ee9b839f43308c3dfd55d72d1f1a299ed42a7f91f2d1afe9c12a783f9e525f"},
|
||||
{file = "jupyterlab-4.2.2.tar.gz", hash = "sha256:a534b6a25719a92a40d514fb133a9fe8f0d9981b0bbce5d8a5fcaa33344a3038"},
|
||||
{file = "jupyterlab-4.2.5-py3-none-any.whl", hash = "sha256:73b6e0775d41a9fee7ee756c80f58a6bed4040869ccc21411dc559818874d321"},
|
||||
{file = "jupyterlab-4.2.5.tar.gz", hash = "sha256:ae7f3a1b8cb88b4f55009ce79fa7c06f99d70cd63601ee4aa91815d054f46f75"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1665,7 +1665,7 @@ dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov",
|
||||
docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<7.3.0)", "sphinx-copybutton"]
|
||||
docs-screenshots = ["altair (==5.3.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.2)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.1.post2)", "matplotlib (==3.8.3)", "nbconvert (>=7.0.0)", "pandas (==2.2.1)", "scipy (==1.12.0)", "vega-datasets (==0.9.0)"]
|
||||
test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"]
|
||||
upgrade-extension = ["copier (>=8,<10)", "jinja2-time (<0.3)", "pydantic (<2.0)", "pyyaml-include (<2.0)", "tomli-w (<2.0)"]
|
||||
upgrade-extension = ["copier (>=9,<10)", "jinja2-time (<0.3)", "pydantic (<3.0)", "pyyaml-include (<3.0)", "tomli-w (<2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "jupyterlab-pygments"
|
||||
@@ -2243,13 +2243,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "notebook"
|
||||
version = "7.2.1"
|
||||
version = "7.2.2"
|
||||
description = "Jupyter Notebook - A web-based notebook environment for interactive computing"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "notebook-7.2.1-py3-none-any.whl", hash = "sha256:f45489a3995746f2195a137e0773e2130960b51c9ac3ce257dbc2705aab3a6ca"},
|
||||
{file = "notebook-7.2.1.tar.gz", hash = "sha256:4287b6da59740b32173d01d641f763d292f49c30e7a51b89c46ba8473126341e"},
|
||||
{file = "notebook-7.2.2-py3-none-any.whl", hash = "sha256:c89264081f671bc02eec0ed470a627ed791b9156cad9285226b31611d3e9fe1c"},
|
||||
{file = "notebook-7.2.2.tar.gz", hash = "sha256:2ef07d4220421623ad3fe88118d687bc0450055570cdd160814a59cf3a1c516e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2829,13 +2829,13 @@ diagrams = ["jinja2", "railroad-diagrams"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.4.4"
|
||||
version = "8.3.2"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
|
||||
{file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
|
||||
{file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"},
|
||||
{file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2843,29 +2843,11 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=0.12,<2.0"
|
||||
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
||||
pluggy = ">=1.5,<2"
|
||||
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-asyncio"
|
||||
version = "0.20.3"
|
||||
description = "Pytest support for asyncio"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pytest-asyncio-0.20.3.tar.gz", hash = "sha256:83cbf01169ce3e8eb71c6c278ccb0574d1a7a3bb8eaaf5e50e0ad342afb33b36"},
|
||||
{file = "pytest_asyncio-0.20.3-py3-none-any.whl", hash = "sha256:f129998b209d04fcc65c96fc85c11e5316738358909a8399e93be553d7656442"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pytest = ">=6.1.0"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
|
||||
testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
|
||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
@@ -3069,7 +3051,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||
@@ -4310,4 +4291,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools",
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.9.0,<4.0"
|
||||
content-hash = "7e0d6fd967987fcf46b038c47b5ca81d86cb8fff89704eac2074f2ce55336a2b"
|
||||
content-hash = "e0787721e6cb80996a39284c74f1ddee4789c6060d6c2272cb1957f4686bd478"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph"
|
||||
version = "0.2.15"
|
||||
version = "0.2.16"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -14,10 +14,9 @@ langgraph-checkpoint = "^1.0.2"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^7.3.0"
|
||||
pytest = "^8.3.2"
|
||||
pytest-cov = "^4.0.0"
|
||||
pytest-dotenv = "^0.5.2"
|
||||
pytest-asyncio = "^0.20.3"
|
||||
pytest-mock = "^3.10.0"
|
||||
syrupy = "^4.0.2"
|
||||
httpx = "^0.26.0"
|
||||
@@ -73,7 +72,6 @@ requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
# --strict-markers will raise errors on unknown marks.
|
||||
# https://docs.pytest.org/en/7.1.x/how-to/mark.html#raising-errors-on-unknown-marks
|
||||
#
|
||||
|
||||
@@ -366,7 +366,7 @@
|
||||
'''
|
||||
# ---
|
||||
# name: test_conditional_entrypoint_to_multiple_state_graph
|
||||
'{"title": "LangGraphInput", "type": "object", "properties": {"locations": {"title": "Locations", "type": "array", "items": {"type": "string"}}, "results": {"title": "Results", "type": "array", "items": {"type": "string"}}}}'
|
||||
'{"title": "LangGraphInput", "type": "object", "properties": {"locations": {"title": "Locations", "type": "array", "items": {"type": "string"}}, "results": {"title": "Results", "type": "array", "items": {"type": "string"}}}, "required": ["locations", "results"]}'
|
||||
# ---
|
||||
# name: test_conditional_entrypoint_to_multiple_state_graph.1
|
||||
'{"title": "LangGraphOutput", "type": "object", "properties": {"locations": {"title": "Locations", "type": "array", "items": {"type": "string"}}, "results": {"title": "Results", "type": "array", "items": {"type": "string"}}}}'
|
||||
@@ -3304,7 +3304,7 @@
|
||||
'query',
|
||||
'inner',
|
||||
]),
|
||||
'title': 'Input',
|
||||
'title': 'LangGraphInput',
|
||||
'type': 'object',
|
||||
})
|
||||
# ---
|
||||
@@ -3323,11 +3323,7 @@
|
||||
'type': 'array',
|
||||
}),
|
||||
}),
|
||||
'required': list([
|
||||
'answer',
|
||||
'docs',
|
||||
]),
|
||||
'title': 'Output',
|
||||
'title': 'LangGraphOutput',
|
||||
'type': 'object',
|
||||
})
|
||||
# ---
|
||||
@@ -3374,7 +3370,7 @@
|
||||
'query',
|
||||
'inner',
|
||||
]),
|
||||
'title': 'Input',
|
||||
'title': 'LangGraphInput',
|
||||
'type': 'object',
|
||||
})
|
||||
# ---
|
||||
@@ -3393,11 +3389,7 @@
|
||||
'type': 'array',
|
||||
}),
|
||||
}),
|
||||
'required': list([
|
||||
'answer',
|
||||
'docs',
|
||||
]),
|
||||
'title': 'Output',
|
||||
'title': 'LangGraphOutput',
|
||||
'type': 'object',
|
||||
})
|
||||
# ---
|
||||
@@ -3444,7 +3436,7 @@
|
||||
'query',
|
||||
'inner',
|
||||
]),
|
||||
'title': 'Input',
|
||||
'title': 'LangGraphInput',
|
||||
'type': 'object',
|
||||
})
|
||||
# ---
|
||||
@@ -3463,11 +3455,7 @@
|
||||
'type': 'array',
|
||||
}),
|
||||
}),
|
||||
'required': list([
|
||||
'answer',
|
||||
'docs',
|
||||
]),
|
||||
'title': 'Output',
|
||||
'title': 'LangGraphOutput',
|
||||
'type': 'object',
|
||||
})
|
||||
# ---
|
||||
@@ -3514,7 +3502,7 @@
|
||||
'query',
|
||||
'inner',
|
||||
]),
|
||||
'title': 'Input',
|
||||
'title': 'LangGraphInput',
|
||||
'type': 'object',
|
||||
})
|
||||
# ---
|
||||
@@ -3533,11 +3521,7 @@
|
||||
'type': 'array',
|
||||
}),
|
||||
}),
|
||||
'required': list([
|
||||
'answer',
|
||||
'docs',
|
||||
]),
|
||||
'title': 'Output',
|
||||
'title': 'LangGraphOutput',
|
||||
'type': 'object',
|
||||
})
|
||||
# ---
|
||||
@@ -3584,7 +3568,7 @@
|
||||
'query',
|
||||
'inner',
|
||||
]),
|
||||
'title': 'Input',
|
||||
'title': 'LangGraphInput',
|
||||
'type': 'object',
|
||||
})
|
||||
# ---
|
||||
@@ -3603,11 +3587,7 @@
|
||||
'type': 'array',
|
||||
}),
|
||||
}),
|
||||
'required': list([
|
||||
'answer',
|
||||
'docs',
|
||||
]),
|
||||
'title': 'Output',
|
||||
'title': 'LangGraphOutput',
|
||||
'type': 'object',
|
||||
})
|
||||
# ---
|
||||
@@ -4855,7 +4835,7 @@
|
||||
'''
|
||||
# ---
|
||||
# name: test_prebuilt_tool_chat
|
||||
'{"title": "LangGraphInput", "type": "object", "properties": {"messages": {"title": "Messages", "type": "array", "items": {"$ref": "#/definitions/BaseMessage"}}}, "definitions": {"BaseMessage": {"title": "BaseMessage", "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "type"]}}}'
|
||||
'{"title": "LangGraphInput", "type": "object", "properties": {"messages": {"title": "Messages", "type": "array", "items": {"$ref": "#/definitions/BaseMessage"}}}, "required": ["messages"], "definitions": {"BaseMessage": {"title": "BaseMessage", "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "type"]}}}'
|
||||
# ---
|
||||
# name: test_prebuilt_tool_chat.1
|
||||
'{"title": "LangGraphOutput", "type": "object", "properties": {"messages": {"title": "Messages", "type": "array", "items": {"$ref": "#/definitions/BaseMessage"}}}, "definitions": {"BaseMessage": {"title": "BaseMessage", "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "type"]}}}'
|
||||
@@ -5094,7 +5074,7 @@
|
||||
'{"title": "LangGraphConfig", "type": "object", "properties": {"configurable": {"$ref": "#/definitions/Configurable"}}, "definitions": {"Configurable": {"title": "Configurable", "type": "object", "properties": {"tools": {"title": "Tools", "type": "array", "items": {"type": "string"}}}}}}'
|
||||
# ---
|
||||
# name: test_state_graph_w_config_inherited_state_keys.1
|
||||
'{"title": "LangGraphInput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}'
|
||||
'{"title": "LangGraphInput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "required": ["input"], "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}'
|
||||
# ---
|
||||
# name: test_state_graph_w_config_inherited_state_keys.2
|
||||
'{"title": "LangGraphOutput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,6 @@
|
||||
import asyncio
|
||||
import sys
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from typing import AsyncIterator, Iterator, TypeVar
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncIterator, Optional
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
@@ -10,6 +8,7 @@ from psycopg import AsyncConnection, Connection
|
||||
from psycopg_pool import AsyncConnectionPool, ConnectionPool
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.checkpoint.postgres import PostgresSaver
|
||||
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
||||
from langgraph.checkpoint.sqlite import SqliteSaver
|
||||
@@ -19,6 +18,11 @@ from tests.memory_assert import MemorySaverAssertImmutable
|
||||
DEFAULT_POSTGRES_URI = "postgres://postgres:postgres@localhost:5442/"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def anyio_backend():
|
||||
return "asyncio"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def deterministic_uuids(mocker: MockerFixture) -> MockerFixture:
|
||||
side_effect = (
|
||||
@@ -27,35 +31,6 @@ def deterministic_uuids(mocker: MockerFixture) -> MockerFixture:
|
||||
return mocker.patch("uuid.uuid4", side_effect=side_effect)
|
||||
|
||||
|
||||
"""
|
||||
pytest-asyncio doesn't support calling async fixtures with getfixturevalue
|
||||
so we need to use ThreadPoolExecutor to run the async fixture in a thread
|
||||
https://github.com/pytest-dev/pytest-asyncio/issues/112#issuecomment-462062890
|
||||
"""
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def close_loop(loop: asyncio.AbstractEventLoop) -> None:
|
||||
loop.run_until_complete(loop.shutdown_asyncgens())
|
||||
loop.run_until_complete(loop.shutdown_default_executor())
|
||||
asyncio.set_event_loop(None)
|
||||
loop.close()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def agen_to_gen(agen: AsyncIterator[T]) -> Iterator[T]:
|
||||
with ThreadPoolExecutor(1) as bg:
|
||||
loop = asyncio.new_event_loop()
|
||||
bg.submit(asyncio.set_event_loop, loop).result()
|
||||
try:
|
||||
yield bg.submit(loop.run_until_complete, agen.__aenter__()).result()
|
||||
finally:
|
||||
bg.submit(
|
||||
loop.run_until_complete, agen.__aexit__(None, None, None)
|
||||
).result()
|
||||
bg.submit(close_loop, loop).result()
|
||||
|
||||
|
||||
# checkpointer fixtures
|
||||
|
||||
|
||||
@@ -70,12 +45,6 @@ def checkpointer_sqlite():
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_sqlite_aio():
|
||||
with agen_to_gen(_checkpointer_sqlite_aio()) as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_sqlite_aio():
|
||||
async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
@@ -143,16 +112,10 @@ def checkpointer_postgres_pool():
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_postgres_aio():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
with agen_to_gen(_checkpointer_postgres_aio()) as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
@@ -174,16 +137,10 @@ async def _checkpointer_postgres_aio():
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_postgres_aio_pipe():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
with agen_to_gen(_checkpointer_postgres_aio_pipe()) as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio_pipe():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
@@ -208,16 +165,10 @@ async def _checkpointer_postgres_aio_pipe():
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_postgres_aio_pool():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
with agen_to_gen(_checkpointer_postgres_aio_pool()) as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio_pool():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
@@ -240,6 +191,30 @@ async def _checkpointer_postgres_aio_pool():
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def awith_checkpointer(
|
||||
checkpointer_name: Optional[str],
|
||||
) -> AsyncIterator[BaseCheckpointSaver]:
|
||||
if checkpointer_name is None:
|
||||
yield None
|
||||
elif checkpointer_name == "memory":
|
||||
yield MemorySaverAssertImmutable()
|
||||
elif checkpointer_name == "sqlite_aio":
|
||||
async with _checkpointer_sqlite_aio() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_aio":
|
||||
async with _checkpointer_postgres_aio() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_aio_pipe":
|
||||
async with _checkpointer_postgres_aio_pipe() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_aio_pool":
|
||||
async with _checkpointer_postgres_aio_pool() as checkpointer:
|
||||
yield checkpointer
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown checkpointer: {checkpointer_name}")
|
||||
|
||||
|
||||
ALL_CHECKPOINTERS_SYNC = [
|
||||
"memory",
|
||||
"sqlite",
|
||||
@@ -254,3 +229,7 @@ ALL_CHECKPOINTERS_ASYNC = [
|
||||
"postgres_aio_pipe",
|
||||
"postgres_aio_pool",
|
||||
]
|
||||
ALL_CHECKPOINTERS_ASYNC_PLUS_NONE = [
|
||||
*ALL_CHECKPOINTERS_ASYNC,
|
||||
None,
|
||||
]
|
||||
|
||||
@@ -8,6 +8,8 @@ from langgraph.channels.last_value import LastValue
|
||||
from langgraph.channels.topic import Topic
|
||||
from langgraph.errors import EmptyChannelError, InvalidUpdateError
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
def test_last_value() -> None:
|
||||
with LastValue(int).from_checkpoint(None, {}) as channel:
|
||||
|
||||
@@ -4,12 +4,16 @@ import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from langgraph.graph import END, START, StateGraph
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"checkpointer_name",
|
||||
["memory", "sqlite", "postgres", "postgres_pipe"],
|
||||
from tests.conftest import (
|
||||
ALL_CHECKPOINTERS_ASYNC,
|
||||
ALL_CHECKPOINTERS_SYNC,
|
||||
awith_checkpointer,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_interruption_without_state_updates(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture
|
||||
) -> None:
|
||||
@@ -47,12 +51,9 @@ def test_interruption_without_state_updates(
|
||||
assert graph.get_state(thread).next == ()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"checkpointer_name",
|
||||
["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"],
|
||||
)
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_interruption_without_state_updates_async(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture
|
||||
checkpointer_name: str, mocker: MockerFixture
|
||||
):
|
||||
"""Test interruption without state updates. This test confirms that
|
||||
interrupting doesn't require a state key having been updated in the prev step"""
|
||||
@@ -72,17 +73,17 @@ async def test_interruption_without_state_updates_async(
|
||||
builder.add_edge("step_2", "step_3")
|
||||
builder.add_edge("step_3", END)
|
||||
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
graph = builder.compile(checkpointer=checkpointer, interrupt_after="*")
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
graph = builder.compile(checkpointer=checkpointer, interrupt_after="*")
|
||||
|
||||
initial_input = {"input": "hello world"}
|
||||
thread = {"configurable": {"thread_id": "1"}}
|
||||
initial_input = {"input": "hello world"}
|
||||
thread = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
await graph.ainvoke(initial_input, thread, debug=True)
|
||||
assert (await graph.aget_state(thread)).next == ("step_2",)
|
||||
await graph.ainvoke(initial_input, thread, debug=True)
|
||||
assert (await graph.aget_state(thread)).next == ("step_2",)
|
||||
|
||||
await graph.ainvoke(None, thread, debug=True)
|
||||
assert (await graph.aget_state(thread)).next == ("step_3",)
|
||||
await graph.ainvoke(None, thread, debug=True)
|
||||
assert (await graph.aget_state(thread)).next == ("step_3",)
|
||||
|
||||
await graph.ainvoke(None, thread, debug=True)
|
||||
assert (await graph.aget_state(thread)).next == ()
|
||||
await graph.ainvoke(None, thread, debug=True)
|
||||
assert (await graph.aget_state(thread)).next == ()
|
||||
|
||||
@@ -23,8 +23,15 @@ from pydantic import BaseModel as BaseModelV2
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.prebuilt import ToolNode, ValidationNode, create_react_agent
|
||||
from langgraph.prebuilt.tool_node import InjectedState
|
||||
from tests.conftest import (
|
||||
ALL_CHECKPOINTERS_ASYNC,
|
||||
ALL_CHECKPOINTERS_SYNC,
|
||||
awith_checkpointer,
|
||||
)
|
||||
from tests.messages import _AnyIdHumanMessage
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
class FakeToolCallingModel(BaseChatModel):
|
||||
def _generate(
|
||||
@@ -53,10 +60,7 @@ class FakeToolCallingModel(BaseChatModel):
|
||||
return self
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"checkpointer_name",
|
||||
["memory", "sqlite", "postgres", "postgres_pipe"],
|
||||
)
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_no_modifier(request: pytest.FixtureRequest, checkpointer_name: str) -> None:
|
||||
checkpointer: BaseCheckpointSaver = request.getfixturevalue(
|
||||
"checkpointer_" + checkpointer_name
|
||||
@@ -89,43 +93,35 @@ def test_no_modifier(request: pytest.FixtureRequest, checkpointer_name: str) ->
|
||||
assert saved.pending_writes == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"checkpointer_name",
|
||||
["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"],
|
||||
)
|
||||
async def test_no_modifier_async(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
) -> None:
|
||||
checkpointer: BaseCheckpointSaver = request.getfixturevalue(
|
||||
f"checkpointer_{checkpointer_name}"
|
||||
)
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_no_modifier_async(checkpointer_name: str) -> None:
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
model = FakeToolCallingModel()
|
||||
|
||||
model = FakeToolCallingModel()
|
||||
agent = create_react_agent(model, [], checkpointer=checkpointer)
|
||||
inputs = [HumanMessage("hi?")]
|
||||
thread = {"configurable": {"thread_id": "123"}}
|
||||
response = await agent.ainvoke({"messages": inputs}, thread, debug=True)
|
||||
expected_response = {"messages": inputs + [AIMessage(content="hi?", id="0")]}
|
||||
assert response == expected_response
|
||||
|
||||
agent = create_react_agent(model, [], checkpointer=checkpointer)
|
||||
inputs = [HumanMessage("hi?")]
|
||||
thread = {"configurable": {"thread_id": "123"}}
|
||||
response = await agent.ainvoke({"messages": inputs}, thread, debug=True)
|
||||
expected_response = {"messages": inputs + [AIMessage(content="hi?", id="0")]}
|
||||
assert response == expected_response
|
||||
|
||||
if checkpointer:
|
||||
saved = await checkpointer.aget_tuple(thread)
|
||||
assert saved is not None
|
||||
assert saved.checkpoint["channel_values"] == {
|
||||
"messages": [
|
||||
_AnyIdHumanMessage(content="hi?"),
|
||||
AIMessage(content="hi?", id="0"),
|
||||
],
|
||||
"agent": "agent",
|
||||
}
|
||||
assert saved.metadata == {
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}},
|
||||
"step": 1,
|
||||
}
|
||||
assert saved.pending_writes == []
|
||||
if checkpointer:
|
||||
saved = await checkpointer.aget_tuple(thread)
|
||||
assert saved is not None
|
||||
assert saved.checkpoint["channel_values"] == {
|
||||
"messages": [
|
||||
_AnyIdHumanMessage(content="hi?"),
|
||||
AIMessage(content="hi?", id="0"),
|
||||
],
|
||||
"agent": "agent",
|
||||
}
|
||||
assert saved.metadata == {
|
||||
"parents": {},
|
||||
"source": "loop",
|
||||
"writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}},
|
||||
"step": 1,
|
||||
}
|
||||
assert saved.pending_writes == []
|
||||
|
||||
|
||||
def test_passing_two_modifiers():
|
||||
|
||||
@@ -74,10 +74,7 @@ from langgraph.store.memory import MemoryStore
|
||||
from tests.any_str import AnyDict, AnyStr, AnyVersion, UnsortedSequence
|
||||
from tests.conftest import ALL_CHECKPOINTERS_SYNC
|
||||
from tests.fake_tracer import FakeTracer
|
||||
from tests.memory_assert import (
|
||||
MemorySaverAssertCheckpointMetadata,
|
||||
MemorySaverNoPending,
|
||||
)
|
||||
from tests.memory_assert import MemorySaverAssertCheckpointMetadata
|
||||
from tests.messages import _AnyIdAIMessage, _AnyIdHumanMessage
|
||||
|
||||
|
||||
@@ -1626,29 +1623,6 @@ def test_cond_edge_after_send() -> None:
|
||||
assert graph.invoke(["0"]) == ["0", "1", "2", "2", "3"]
|
||||
|
||||
|
||||
async def test_checkpointer_null_pending_writes() -> None:
|
||||
class Node:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
setattr(self, "__name__", name)
|
||||
|
||||
def __call__(self, state):
|
||||
return [self.name]
|
||||
|
||||
builder = StateGraph(Annotated[list, operator.add])
|
||||
builder.add_node(Node("1"))
|
||||
builder.add_edge(START, "1")
|
||||
graph = builder.compile(checkpointer=MemorySaverNoPending())
|
||||
assert graph.invoke([], {"configurable": {"thread_id": "foo"}}) == ["1"]
|
||||
assert graph.invoke([], {"configurable": {"thread_id": "foo"}}) == ["1"] * 2
|
||||
assert (await graph.ainvoke([], {"configurable": {"thread_id": "foo"}})) == [
|
||||
"1"
|
||||
] * 3
|
||||
assert (await graph.ainvoke([], {"configurable": {"thread_id": "foo"}})) == [
|
||||
"1"
|
||||
] * 4
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_invoke_checkpoint_three(
|
||||
mocker: MockerFixture, request: pytest.FixtureRequest, checkpointer_name: str
|
||||
@@ -1929,7 +1903,7 @@ def test_invoke_two_processes_no_in(mocker: MockerFixture) -> None:
|
||||
one = Channel.subscribe_to("between") | add_one | Channel.write_to("output")
|
||||
two = Channel.subscribe_to("between") | add_one
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(TypeError):
|
||||
Pregel(nodes={"one": one, "two": two})
|
||||
|
||||
|
||||
@@ -9969,10 +9943,12 @@ def test_send_to_nested_graphs(
|
||||
graph.update_state(outer_state.tasks[1].state, {"subject": "turtles - hohoho"})
|
||||
|
||||
# continue past interrupt
|
||||
assert graph.invoke(None, config=config) == {
|
||||
"subjects": ["cats", "dogs"],
|
||||
"jokes": ["Joke about cats - hohoho", "Joke about turtles - hohoho"],
|
||||
}
|
||||
assert sorted(
|
||||
graph.stream(None, config=config), key=lambda d: d["generate_joke"]["jokes"][0]
|
||||
) == [
|
||||
{"generate_joke": {"jokes": ["Joke about cats - hohoho"]}},
|
||||
{"generate_joke": {"jokes": ["Joke about turtles - hohoho"]}},
|
||||
]
|
||||
|
||||
actual_snapshot = graph.get_state(config)
|
||||
expected_snapshot = StateSnapshot(
|
||||
@@ -10209,10 +10185,13 @@ def test_weather_subgraph(
|
||||
else:
|
||||
return "normal_llm_node"
|
||||
|
||||
def weather_graph(state: RouterState):
|
||||
return subgraph.invoke(state)
|
||||
|
||||
graph = StateGraph(RouterState)
|
||||
graph.add_node(router_node)
|
||||
graph.add_node(normal_llm_node)
|
||||
graph.add_node("weather_graph", subgraph)
|
||||
graph.add_node("weather_graph", weather_graph)
|
||||
graph.add_edge(START, "router_node")
|
||||
graph.add_conditional_edges("router_node", route_after_prediction)
|
||||
graph.add_edge("normal_llm_node", END)
|
||||
|
||||
+4768
-4233
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,11 @@
|
||||
import warnings
|
||||
from typing import Annotated as Annotated2
|
||||
from typing import Any
|
||||
from typing import Any, Optional
|
||||
|
||||
import pytest
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from pydantic.v1 import BaseModel
|
||||
from typing_extensions import Annotated, TypedDict
|
||||
from typing_extensions import Annotated, NotRequired, Required, TypedDict
|
||||
|
||||
from langgraph.graph.state import StateGraph, _warn_invalid_state_schema
|
||||
|
||||
@@ -45,7 +46,8 @@ def test_warns_invalid_schema(schema: Any):
|
||||
)
|
||||
def test_doesnt_warn_valid_schema(schema: Any):
|
||||
# Assert the function does not raise a warning
|
||||
with pytest.warns(None):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
_warn_invalid_state_schema(schema)
|
||||
|
||||
|
||||
@@ -86,3 +88,45 @@ def test_state_schema_with_type_hint():
|
||||
for i, c in enumerate(graph.stream(input_state, stream_mode="updates")):
|
||||
node_name = actions[i].__name__
|
||||
assert c[node_name] == output_state
|
||||
|
||||
|
||||
@pytest.mark.parametrize("total_", [True, False])
|
||||
def test_state_schema_optional_values(total_: bool):
|
||||
class SomeParentState(TypedDict):
|
||||
val0a: str
|
||||
val0b: Optional[str]
|
||||
|
||||
class InputState(SomeParentState, total=total_): # type: ignore
|
||||
val1: str
|
||||
val2: Optional[str]
|
||||
val3: Required[str]
|
||||
val4: NotRequired[dict]
|
||||
val5: Annotated[Required[str], "foo"]
|
||||
val6: Annotated[NotRequired[str], "bar"]
|
||||
|
||||
class State(InputState): # this would be ignored
|
||||
val4: dict
|
||||
|
||||
builder = StateGraph(State, input=InputState)
|
||||
builder.add_node("n", lambda x: x)
|
||||
builder.add_edge("__start__", "n")
|
||||
graph = builder.compile()
|
||||
model = graph.input_schema
|
||||
json_schema = model.schema()
|
||||
|
||||
if total_ is False:
|
||||
expected_required = set()
|
||||
expected_optional = {"val2", "val1"}
|
||||
else:
|
||||
expected_required = {"val1"}
|
||||
|
||||
expected_optional = {"val2"}
|
||||
|
||||
# The others should always have precedence based on the required annotation
|
||||
expected_required |= {"val0a", "val3", "val5"}
|
||||
expected_optional |= {"val0b", "val4", "val6"}
|
||||
|
||||
assert set(json_schema.get("required", set())) == expected_required
|
||||
assert (
|
||||
set(json_schema["properties"].keys()) == expected_required | expected_optional
|
||||
)
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import asyncio
|
||||
from typing import Any, Optional
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.store.batch import AsyncBatchedStore
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
async def test_async_batch_store(mocker: MockerFixture) -> None:
|
||||
aget = mocker.stub()
|
||||
|
||||
@@ -1,15 +1,34 @@
|
||||
import functools
|
||||
import sys
|
||||
import uuid
|
||||
from typing import TypedDict
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
ForwardRef,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
TypedDict,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
from unittest.mock import patch
|
||||
|
||||
import langsmith
|
||||
import pytest
|
||||
from typing_extensions import Annotated, NotRequired, Required
|
||||
|
||||
from langgraph.graph import END, StateGraph
|
||||
from langgraph.graph.graph import CompiledGraph
|
||||
from langgraph.utils import is_async_callable, is_async_generator
|
||||
from langgraph.utils import (
|
||||
_is_optional_type,
|
||||
get_field_default,
|
||||
is_async_callable,
|
||||
is_async_generator,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
def test_is_async() -> None:
|
||||
@@ -119,3 +138,96 @@ async def test_runnable_callable_tracing_nested_async(rt_graph: CompiledGraph) -
|
||||
with langsmith.tracing_context(enabled=True):
|
||||
res = await rt_graph.ainvoke({"foo": 1})
|
||||
assert isinstance(res["node_run_id"], uuid.UUID)
|
||||
|
||||
|
||||
def test_is_optional_type():
|
||||
assert _is_optional_type(None)
|
||||
assert not _is_optional_type(type(None))
|
||||
assert _is_optional_type(Optional[list])
|
||||
assert not _is_optional_type(int)
|
||||
assert _is_optional_type(Optional[Literal[1, 2, 3]])
|
||||
assert not _is_optional_type(Literal[1, 2, 3])
|
||||
assert _is_optional_type(Optional[List[int]])
|
||||
assert _is_optional_type(Optional[Dict[str, int]])
|
||||
assert not _is_optional_type(List[Optional[int]])
|
||||
assert _is_optional_type(Union[Optional[str], Optional[int]])
|
||||
assert _is_optional_type(
|
||||
Union[
|
||||
Union[Optional[str], Optional[int]], Union[Optional[float], Optional[dict]]
|
||||
]
|
||||
)
|
||||
assert not _is_optional_type(Union[Union[str, int], Union[float, dict]])
|
||||
|
||||
assert _is_optional_type(Union[int, None])
|
||||
assert _is_optional_type(Union[str, None, int])
|
||||
assert _is_optional_type(Union[None, str, int])
|
||||
assert not _is_optional_type(Union[int, str])
|
||||
|
||||
assert not _is_optional_type(Any) # Do we actually want this?
|
||||
assert _is_optional_type(Optional[Any])
|
||||
|
||||
class MyClass:
|
||||
pass
|
||||
|
||||
assert _is_optional_type(Optional[MyClass])
|
||||
assert not _is_optional_type(MyClass)
|
||||
assert _is_optional_type(Optional[ForwardRef("MyClass")])
|
||||
assert not _is_optional_type(ForwardRef("MyClass"))
|
||||
|
||||
assert _is_optional_type(Optional[Union[List[int], Dict[str, Optional[int]]]])
|
||||
assert not _is_optional_type(Union[List[int], Dict[str, Optional[int]]])
|
||||
|
||||
assert _is_optional_type(Optional[Callable[[int], str]])
|
||||
assert not _is_optional_type(Callable[[int], Optional[str]])
|
||||
|
||||
T = TypeVar("T")
|
||||
assert _is_optional_type(Optional[T])
|
||||
assert not _is_optional_type(T)
|
||||
|
||||
U = TypeVar("U", bound=Optional[T]) # type: ignore
|
||||
assert _is_optional_type(U)
|
||||
|
||||
|
||||
def test_is_required():
|
||||
class MyBaseTypedDict(TypedDict):
|
||||
val_1: Required[Optional[str]]
|
||||
val_2: Required[str]
|
||||
val_3: NotRequired[str]
|
||||
val_4: NotRequired[Optional[str]]
|
||||
val_5: Annotated[NotRequired[int], "foo"]
|
||||
val_6: NotRequired[Annotated[int, "foo"]]
|
||||
val_7: Annotated[Required[int], "foo"]
|
||||
val_8: Required[Annotated[int, "foo"]]
|
||||
val_9: Optional[str]
|
||||
val_10: str
|
||||
|
||||
annos = MyBaseTypedDict.__annotations__
|
||||
assert get_field_default("val_1", annos["val_1"], MyBaseTypedDict) == ...
|
||||
assert get_field_default("val_2", annos["val_2"], MyBaseTypedDict) == ...
|
||||
assert get_field_default("val_3", annos["val_3"], MyBaseTypedDict) is None
|
||||
assert get_field_default("val_4", annos["val_4"], MyBaseTypedDict) is None
|
||||
# See https://peps.python.org/pep-0655/#interaction-with-annotated
|
||||
assert get_field_default("val_5", annos["val_5"], MyBaseTypedDict) is None
|
||||
assert get_field_default("val_6", annos["val_6"], MyBaseTypedDict) is None
|
||||
assert get_field_default("val_7", annos["val_7"], MyBaseTypedDict) == ...
|
||||
assert get_field_default("val_8", annos["val_8"], MyBaseTypedDict) == ...
|
||||
assert get_field_default("val_9", annos["val_9"], MyBaseTypedDict) is None
|
||||
assert get_field_default("val_10", annos["val_10"], MyBaseTypedDict) == ...
|
||||
|
||||
class MyChildDict(MyBaseTypedDict):
|
||||
val_11: int
|
||||
val_11b: Optional[int]
|
||||
val_11c: Union[int, None, str]
|
||||
|
||||
class MyGrandChildDict(MyChildDict, total=False):
|
||||
val_12: int
|
||||
val_13: Required[str]
|
||||
|
||||
cannos = MyChildDict.__annotations__
|
||||
gcannos = MyGrandChildDict.__annotations__
|
||||
assert get_field_default("val_11", cannos["val_11"], MyChildDict) == ...
|
||||
assert get_field_default("val_11b", cannos["val_11b"], MyChildDict) is None
|
||||
assert get_field_default("val_11c", cannos["val_11c"], MyChildDict) is None
|
||||
assert get_field_default("val_12", gcannos["val_12"], MyGrandChildDict) is None
|
||||
assert get_field_default("val_9", gcannos["val_9"], MyGrandChildDict) is None
|
||||
assert get_field_default("val_13", gcannos["val_13"], MyGrandChildDict) == ...
|
||||
|
||||
Reference in New Issue
Block a user