mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-18 05:35:43 +02:00
Compare commits
160
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
016a9c1936 | ||
|
|
a2d6837fba | ||
|
|
f5bb2a3b04 | ||
|
|
f807b73092 | ||
|
|
167405daf2 | ||
|
|
c6360e5408 | ||
|
|
f0505155a2 | ||
|
|
886df0fa86 | ||
|
|
3f1792d6ba | ||
|
|
9208052a94 | ||
|
|
7c11325e23 | ||
|
|
d99dc7d81b | ||
|
|
973ad76a58 | ||
|
|
36e49eb190 | ||
|
|
66b9a7dee7 | ||
|
|
5494855ffa | ||
|
|
38d93a324c | ||
|
|
07c65321c1 | ||
|
|
1dbdd7df2e | ||
|
|
dab29ce094 | ||
|
|
0388534b9f | ||
|
|
81077e7c3a | ||
|
|
29a0042149 | ||
|
|
e9162e2516 | ||
|
|
0f6c001c25 | ||
|
|
7f26325c87 | ||
|
|
3c4ce3f945 | ||
|
|
84ef939bf4 | ||
|
|
bdc22ea127 | ||
|
|
5abbb79e1b | ||
|
|
0a5220aa07 | ||
|
|
970e68edcc | ||
|
|
da1a80e86d | ||
|
|
c4b240e0c2 | ||
|
|
c2052d11c2 | ||
|
|
dc0281b99c | ||
|
|
3a860ad537 | ||
|
|
7051bccc30 | ||
|
|
199e41b228 | ||
|
|
229a9e19a8 | ||
|
|
3c3a1a1f35 | ||
|
|
f11127648e | ||
|
|
29f833b1a7 | ||
|
|
7a3ea42743 | ||
|
|
a94902db8a | ||
|
|
9fd152ef3a | ||
|
|
03bc9ba6e6 | ||
|
|
7fe6f88876 | ||
|
|
1d88affd29 | ||
|
|
6906e12edb | ||
|
|
7b4c29a20d | ||
|
|
16bfa80b58 | ||
|
|
cb10437c3f | ||
|
|
2bcf1c0a20 | ||
|
|
00964b18f6 | ||
|
|
0d5c6201d3 | ||
|
|
86d2847dab | ||
|
|
b3a4eaa967 | ||
|
|
87fc519ce7 | ||
|
|
ef3a1ee997 | ||
|
|
311e16dffd | ||
|
|
c83b8f6d04 | ||
|
|
62d3a85b07 | ||
|
|
810ae0ef51 | ||
|
|
2ff49d2200 | ||
|
|
d0567dc7be | ||
|
|
ea64ac5c07 | ||
|
|
090b53ccc1 | ||
|
|
0e872e7482 | ||
|
|
3ad966e057 | ||
|
|
a73f9affab | ||
|
|
89a0859928 | ||
|
|
75c502cd93 | ||
|
|
0cc45f7a35 | ||
|
|
efb1dd6a10 | ||
|
|
6edb213fe7 | ||
|
|
c0513076a2 | ||
|
|
c043f148a6 | ||
|
|
366b5e04c7 | ||
|
|
72239d2228 | ||
|
|
b7f238975a | ||
|
|
a3c45141ee | ||
|
|
f7b899a54d | ||
|
|
95477277a2 | ||
|
|
64b446f671 | ||
|
|
c2d2e44794 | ||
|
|
f60b9f3b43 | ||
|
|
e3acfa3435 | ||
|
|
0f92c89529 | ||
|
|
7cba75ec35 | ||
|
|
8408ba3a3e | ||
|
|
c8c58b30d5 | ||
|
|
b4bed3329c | ||
|
|
4eec3aa69e | ||
|
|
35e3276e34 | ||
|
|
b346f4ead8 | ||
|
|
fef748e6cc | ||
|
|
19cc95f7b6 | ||
|
|
eef65d94ac | ||
|
|
72d497e052 | ||
|
|
32f58258aa | ||
|
|
87d57b434a | ||
|
|
4bbbb7d246 | ||
|
|
c1ce3c6b5f | ||
|
|
511de6f5f6 | ||
|
|
4dc08cd724 | ||
|
|
d7b9b3b01d | ||
|
|
a82ded65c6 | ||
|
|
1ca49fa568 | ||
|
|
4eb3cd32fa | ||
|
|
80d2a315aa | ||
|
|
82c9aa8485 | ||
|
|
8ecfafefbf | ||
|
|
010564cbb3 | ||
|
|
18a3fa4a00 | ||
|
|
de8e487ff1 | ||
|
|
e5ebdff4d8 | ||
|
|
f283dac325 | ||
|
|
639501809c | ||
|
|
36e6b89081 | ||
|
|
90639e6cd7 | ||
|
|
dec0b7f439 | ||
|
|
04657408f8 | ||
|
|
971d746061 | ||
|
|
9a2ba8c8cd | ||
|
|
b50d41bbf3 | ||
|
|
b71fd5092b | ||
|
|
f9b151b67f | ||
|
|
7ba9a66301 | ||
|
|
1b85764bf6 | ||
|
|
5e4c928948 | ||
|
|
ae282e3ae1 | ||
|
|
de3b654735 | ||
|
|
d28734f287 | ||
|
|
f9409022ed | ||
|
|
8138c88b41 | ||
|
|
38332fd3c6 | ||
|
|
5606bef3dd | ||
|
|
895079bbdc | ||
|
|
6aaf80f2fd | ||
|
|
2e656d9145 | ||
|
|
1fb8e013f7 | ||
|
|
f8fe2041d9 | ||
|
|
014f8485a6 | ||
|
|
58d7eb9b17 | ||
|
|
44ff5f1a5a | ||
|
|
46f25cf926 | ||
|
|
e1467c27cb | ||
|
|
5deedfe548 | ||
|
|
30937d0406 | ||
|
|
6eb8130419 | ||
|
|
8455771eb9 | ||
|
|
39dcafcdea | ||
|
|
7854a19dac | ||
|
|
18cea39090 | ||
|
|
bf3098a075 | ||
|
|
1e3953d1e0 | ||
|
|
18e71469e1 | ||
|
|
2624fc43dd | ||
|
|
0c5c2e6370 |
@@ -19,14 +19,19 @@ jobs:
|
||||
- "3.13"
|
||||
core-version:
|
||||
- "latest"
|
||||
ff-send-v2:
|
||||
- "false"
|
||||
include:
|
||||
- python-version: "3.11"
|
||||
core-version: ">=0.2.42,<0.3.0"
|
||||
- python-version: "3.11"
|
||||
core-version: "latest"
|
||||
ff-send-v2: "true"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: libs/langgraph
|
||||
name: "test #${{ matrix.python-version }} (langchain-core: ${{ matrix.core-version }})"
|
||||
name: "test #${{ matrix.python-version }} (langchain-core: ${{ matrix.core-version }}, ff-send-v2: ${{ matrix.ff-send-v2 }})"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }} + Poetry ${{ env.POETRY_VERSION }}
|
||||
@@ -52,6 +57,8 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
shell: bash
|
||||
env:
|
||||
LANGGRAPH_FF_SEND_V2: ${{ matrix.ff-send-v2 }}
|
||||
run: |
|
||||
make test
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@ jobs:
|
||||
deploy:
|
||||
# needs: run-changed-notebooks
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.MKDOCS_GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -58,8 +60,14 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install --with test
|
||||
poetry run pip install -U pytest pytest-check-links langsmith langchain GitPython
|
||||
poetry install --with test --no-root
|
||||
poetry run pip install -U \
|
||||
pytest \
|
||||
pytest-check-links \
|
||||
langsmith \
|
||||
langchain \
|
||||
GitPython \
|
||||
"git+https://${GITHUB_TOKEN}@github.com/langchain-ai/mkdocs-material-insiders.git"
|
||||
|
||||
- name: Lint Docs
|
||||
# This step lints the docs using the existing linting set up.
|
||||
@@ -82,7 +90,8 @@ jobs:
|
||||
--check-links-ignore "https://github\.com/.*" \
|
||||
--check-links-ignore "/.*\.(ipynb|html)$" \
|
||||
--check-links-ignore "https://python\.langchain\.com/.*" \
|
||||
--check-links-ignore "https://openai.com/index/memory-and-new-controls-for-chatgpt/" \
|
||||
--check-links-ignore "https://openai\.com/.*" \
|
||||
--check-links-ignore "https://pepy\.tech/.*" \
|
||||
--check-links $(find docs/site -name "index.html" | grep -v 'storm/index.html')
|
||||
|
||||
else
|
||||
|
||||
@@ -86,19 +86,19 @@ To create an assistant using the studio do the following steps:
|
||||
|
||||
1. Click on the "Create New Assistant" button:
|
||||
|
||||

|
||||

|
||||
|
||||
2. Use the create assistant pane to enter info for the assistant you wish to create, and then click create:
|
||||
1. Use the create assistant pane to enter info for the assistant you wish to create, and then click create:
|
||||
|
||||

|
||||

|
||||
|
||||
3. See that your assistant was created and is displayed in the Studio
|
||||
1. See that your assistant was created and is displayed in the Studio
|
||||
|
||||

|
||||

|
||||
|
||||
4. Click on the edit button next to the selected assistant to manage your created assistant:
|
||||
1. Click on the edit button next to the selected assistant to manage your created assistant:
|
||||
|
||||

|
||||

|
||||
|
||||
## Create a new version for your assistant
|
||||
|
||||
@@ -131,15 +131,15 @@ Let's now say we wanted to add a system prompt to our assistant. We can do this
|
||||
|
||||
1. First, click on the edit button next to the `openai_assistant`. Then, add a system prompt and click "Save New Version":
|
||||
|
||||

|
||||

|
||||
|
||||
2. Then you can see it is selected in the assistant dropdown:
|
||||
1. Then you can see it is selected in the assistant dropdown:
|
||||
|
||||

|
||||

|
||||
|
||||
3. And you can see all the version history in the edit pane for the assistant:
|
||||
1. And you can see all the version history in the edit pane for the assistant:
|
||||
|
||||

|
||||

|
||||
|
||||
## Point your assistant to a different version
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ Now we can start our two runs and join the second on euntil it has completed:
|
||||
assistant_id,
|
||||
input={"messages": [{"role": "user", "content": "what's the weather in sf?"}]},
|
||||
)
|
||||
# sleep a bit to get partial outputs from the first run
|
||||
await asyncio.sleep(2)
|
||||
run = await client.runs.create(
|
||||
thread["thread_id"],
|
||||
@@ -114,6 +115,7 @@ Now we can start our two runs and join the second on euntil it has completed:
|
||||
assistantId,
|
||||
{ input: { messages: [{ role: "human", content: "what's the weather in sf?" }] } }
|
||||
);
|
||||
// sleep a bit to get partial outputs from the first run
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
let run = await client.runs.create(
|
||||
|
||||
@@ -95,7 +95,6 @@ Now let's run a thread with the multitask parameter set to "rollback":
|
||||
assistant_id,
|
||||
input={"messages": [{"role": "user", "content": "what's the weather in sf?"}]},
|
||||
)
|
||||
await asyncio.sleep(2)
|
||||
run = await client.runs.create(
|
||||
thread["thread_id"],
|
||||
assistant_id,
|
||||
@@ -115,7 +114,6 @@ Now let's run a thread with the multitask parameter set to "rollback":
|
||||
assistantId,
|
||||
{ input: { messages: [{ role: "human", content: "what's the weather in sf?" }] } }
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
let run = await client.runs.create(
|
||||
thread["thread_id"],
|
||||
@@ -139,7 +137,7 @@ Now let's run a thread with the multitask parameter set to "rollback":
|
||||
--data "{
|
||||
\"assistant_id\": \"agent\",
|
||||
\"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in sf?\"}]},
|
||||
}" && sleep 2 && curl --request POST \
|
||||
}" && curl --request POST \
|
||||
--url <DEPLOY<ENT_URL>>/threads/<THREAD_ID>/runs \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Quick Start
|
||||
# LangGraph Cloud Quick Start
|
||||
|
||||
In this tutorial you will build and deploy a simple chatbot agent that can look things up on the internet. You will be using [LangGraph Cloud](../concepts/langgraph_cloud.md), [LangGraph Studio](../concepts/langgraph_studio.md) to visualize and test it out, and [LangGraph SDK](./reference/sdk/python_sdk_ref.md) to interact with the deployed agent.
|
||||
|
||||
|
||||
@@ -60,6 +60,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict",
|
||||
"content": {
|
||||
@@ -116,6 +126,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
@@ -2848,9 +2868,18 @@
|
||||
"description": "The cron schedule to execute this job on."
|
||||
},
|
||||
"assistant_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Assistant Id"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Assistant Id"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"title": "Graph Id"
|
||||
}
|
||||
],
|
||||
"description": "The assistant ID or graph name to run. If using graph name, will default to the assistant automatically created from that graph by the server."
|
||||
},
|
||||
"input": {
|
||||
"anyOf": [
|
||||
@@ -3151,6 +3180,66 @@
|
||||
],
|
||||
"title": "Run"
|
||||
},
|
||||
"Send": {
|
||||
"type": "object",
|
||||
"title": "Send",
|
||||
"description": "A message to send to a node.",
|
||||
"properties": {
|
||||
"node": {
|
||||
"type": "string",
|
||||
"title": "Node",
|
||||
"description": "The node to send the message to."
|
||||
},
|
||||
"input": {
|
||||
"type": "object",
|
||||
"title": "Message",
|
||||
"description": "The message to send."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"node",
|
||||
"input"
|
||||
]
|
||||
},
|
||||
"Command": {
|
||||
"type": "object",
|
||||
"title": "Command",
|
||||
"description": "The command to run.",
|
||||
"properties": {
|
||||
"update": {
|
||||
"type": "object",
|
||||
"title": "Update",
|
||||
"description": "An update to the state."
|
||||
},
|
||||
"resume": {
|
||||
"type": [
|
||||
"object",
|
||||
"array",
|
||||
"number",
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"title": "Resume",
|
||||
"description": "A value to pass to an interrupted node."
|
||||
},
|
||||
"send": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Send"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Send"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"RunCreateStateful": {
|
||||
"properties": {
|
||||
"assistant_id": {
|
||||
@@ -3176,13 +3265,19 @@
|
||||
"input": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "object"
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Input",
|
||||
"description": "The input to the graph."
|
||||
},
|
||||
"command": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Command"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
@@ -3385,13 +3480,19 @@
|
||||
"input": {
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "object"
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Input",
|
||||
"description": "The input to the graph."
|
||||
},
|
||||
"command": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Command"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
|
||||
@@ -53,7 +53,7 @@ You’ll build a Docker image using the [LangGraph CLI](./langgraph_cli.md), whi
|
||||
For more information, please see:
|
||||
|
||||
* [Self-Hosted conceptual guide](./self_hosted.md)
|
||||
* [Self-Hosted Deployment how-to guide](../how-tos/deploy-self-hosted.md)
|
||||
* [Self-Hosted deployment how-to guide](../how-tos/deploy-self-hosted.md)
|
||||
|
||||
## Cloud SaaS
|
||||
|
||||
@@ -89,8 +89,8 @@ For more information please see:
|
||||
|
||||
## Related
|
||||
|
||||
For more information please see:
|
||||
For more information, please see:
|
||||
|
||||
* [LangGraph Platform Plans](./plans.md)
|
||||
* [LangGraph Platform Pricing](https://www.langchain.com/langgraph-platform-pricing)
|
||||
* [LangGraph Platform plans](./plans.md)
|
||||
* [LangGraph Platform pricing](https://www.langchain.com/langgraph-platform-pricing)
|
||||
* [Deployment how-to guides](../how-tos/index.md#deployment)
|
||||
|
||||
@@ -15,10 +15,12 @@ If you do not want to use LangGraph Platform, we describe the options we have im
|
||||

|
||||
|
||||
## Reject
|
||||
|
||||
This is the simplest option, this just rejects any follow up runs and does not allow double texting.
|
||||
See the [how-to guide](../cloud/how-tos/reject_concurrent.md) for configuring the reject double text option.
|
||||
|
||||
## Enqueue
|
||||
|
||||
This is a relatively simple option which continues the first run until it completes the whole run, then sends the new input as a separate run.
|
||||
See the [how-to guide](../cloud/how-tos/enqueue_concurrent.md) for configuring the enqueue double text option.
|
||||
|
||||
@@ -35,10 +37,6 @@ See the [how-to guide](../cloud/how-tos/interrupt_concurrent.md) for configuring
|
||||
|
||||
## Rollback
|
||||
|
||||
This option rolls back all work done up until that point.
|
||||
It then sends the user input in, basically as if it just followed the original run input.
|
||||
|
||||
This may create some weird states - for example, you may have two `User` messages in a row, with no `Asssitant` message in between them.
|
||||
You will need to make sure the LLM you are calling can handle that, or combine those into a single `User` message.
|
||||
This option interrupts the current execution AND rolls back all work done up until that point, including the original run input. It then sends the new user input in, basically as if it was the original input.
|
||||
|
||||
See the [how-to guide](../cloud/how-tos/rollback_concurrent.md) for configuring the rollback double text option.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 141 KiB |
@@ -42,14 +42,14 @@ The LangGraph Platform offers a few different deployment options described in th
|
||||
* LangGraph is an MIT-licensed open-source library, which we are committed to maintaining and growing for the community.
|
||||
* You can always deploy LangGraph applications on your own infrastructure using the open-source LangGraph project without using LangGraph Platform.
|
||||
|
||||
**High Level**
|
||||
### High Level
|
||||
|
||||
- [Why LangGraph Platform?](./langgraph_platform.md): The LangGraph platform is an opinionated way to deploy and manage LangGraph applications. This guide provides an overview of the key features and concepts behind LangGraph Platform.
|
||||
- [Deployment Options](./deployment_options.md): LangGraph Platform offers four deployment options: [Self-Hosted Lite](./self_hosted.md#self-hosted-lite), [Self-Hosted Enterprise](./self_hosted.md#self-hosted-enterprise), [bring your own cloud (BYOC)](./bring_your_own_cloud.md), and [Cloud SaaS](./langgraph_cloud.md). This guide explains the differences between these options, and which Plans they are available on.
|
||||
- [Plans](./plans.md): LangGraph Platforms offer three different plans: Developer, Plus, Enterprise. This guide explains the differences between these options, what deployment options are available for each, and how to sign up for each one.
|
||||
- [Template Applications](./template_applications.md): Reference applications designed to help you get started quickly when building with LangGraph.
|
||||
|
||||
**Components**
|
||||
### Components
|
||||
|
||||
The LangGraph Platform comprises several components that work together to support the deployment and management of LangGraph applications:
|
||||
|
||||
@@ -59,7 +59,7 @@ The LangGraph Platform comprises several components that work together to suppor
|
||||
- [Python/JS SDK](./sdk.md): The Python/JS SDK provides a programmatic way to interact with deployed LangGraph Applications.
|
||||
- [Remote Graph](../how-tos/use-remote-graph.md): A RemoteGraph allows you to interact with any deployed LangGraph application as though it were running locally.
|
||||
|
||||
**LangGraph Server**
|
||||
### LangGraph Server
|
||||
|
||||
- [Application Structure](./application_structure.md): A LangGraph application consists of one or more graphs, a LangGraph API Configuration file (`langgraph.json`), a file that specifies dependencies, and environment variables.
|
||||
- [Assistants](./assistants.md): Assistants are a way to save and manage different configurations of your LangGraph applications.
|
||||
@@ -67,7 +67,7 @@ The LangGraph Platform comprises several components that work together to suppor
|
||||
- [Cron Jobs](./langgraph_server.md#cron-jobs): Cron jobs are a way to schedule tasks to run at specific times in your LangGraph application.
|
||||
- [Double Texting](./double_texting.md): Double texting is a common issue in LLM applications where users may send multiple messages before the graph has finished running. This guide explains how to handle double texting with LangGraph Deploy.
|
||||
|
||||
**Deployment Options**
|
||||
### Deployment Options
|
||||
|
||||
|
||||
- [Self-Hosted Lite](./self_hosted.md): A free (up to 1 million nodes executed), limited version of LangGraph Platform that you can run locally or in a self-hosted manner
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Cloud SaaS
|
||||
|
||||
!!! info "Prerequisites"
|
||||
- [LangGraph Platform](./langgraph_platform.md)
|
||||
!!! info "Prerequisites"
|
||||
- [LangGraph Platform](./langgraph_platform.md)
|
||||
- [LangGraph Server](./langgraph_server.md)
|
||||
|
||||
## Overview
|
||||
|
||||
LangGraph's Cloud SaaS is a managed service that provides a scalable and secure environment for deploying LangGraph APIs. It is designed to work seamlessly with your LangGraph API regardless of how it is defined, what tools it uses, or any dependencies. Cloud SaaS provides a simple way to deploy and manage your LangGraph API in the cloud.
|
||||
LangGraph's Cloud SaaS is a managed service for deploying LangGraph APIs, regardless of its definition or dependencies. The service offers managed implementations of checkpointers and stores, allowing you to focus on building the right cognitive architecture for your use case. By handling scalable & secure infrastructure, LangGraph Cloud offers the fastest path to getting your LangGraph API deployed to production.
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -35,4 +35,4 @@ A high-level diagram of a Cloud SaaS deployment.
|
||||
|
||||
## Related
|
||||
|
||||
- [Deployment Options](./deployment_options.md)
|
||||
- [Deployment Options](./deployment_options.md)
|
||||
|
||||
@@ -391,7 +391,7 @@ Read [this how-to](https://langchain-ai.github.io/langgraph/how-tos/recursion-li
|
||||
|
||||
It can often be useful to set breakpoints before or after certain nodes execute. This can be used to wait for human approval before continuing. These can be set when you ["compile" a graph](#compiling-your-graph). You can set breakpoints either _before_ a node executes (using `interrupt_before`) or after a node executes (using `interrupt_after`.)
|
||||
|
||||
You **MUST** use a [checkpoiner](./persistence.md) when using breakpoints. This is because your graph needs to be able to resume execution.
|
||||
You **MUST** use a [checkpointer](./persistence.md) when using breakpoints. This is because your graph needs to be able to resume execution.
|
||||
|
||||
In order to resume execution, you can just invoke your graph with `None` as the input.
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ You must pass these when invoking the graph as part of the `configurable` portio
|
||||
# {"configurable": {"thread_id": "1", "checkpoint_id": "0c62ca34-ac19-445d-bbb0-5b4984975b2a"}} # also valid config
|
||||
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
graph.invoke(inputs, config=config)
|
||||
graph.invoke(None, config=config)
|
||||
```
|
||||
|
||||
Importantly, LangGraph knows whether a particular checkpoint has been executed previously. If it has, LangGraph simply *re-plays* that particular step in the graph and does not re-execute the step. See this [how to guide on time-travel to learn more about replaying](../how-tos/human_in_the_loop/time-travel.ipynb).
|
||||
|
||||
@@ -29,7 +29,11 @@ To use the Self-Hosted Enterprise version, you must acquire a license key that y
|
||||
## How it works
|
||||
|
||||
- Deploy Redis and Postgres instances on your own infrastructure.
|
||||
- Build the docker image for [LangGraph Server](./langgraph_server.md) using the [LangGraph CLI](./langgraph_cli.md)
|
||||
- Build the docker image for [LangGraph Server](./langgraph_server.md) using the [LangGraph CLI](./langgraph_cli.md).
|
||||
- Deploy a web server that will run the docker image and pass in the necessary environment variables.
|
||||
|
||||
See the [how-to guide](../how-tos/deploy-self-hosted.md)
|
||||
For step-by-step instructions, see [How to set up a self-hosted deployment of LangGraph](../how-tos/deploy-self-hosted.md).
|
||||
|
||||
## Related
|
||||
|
||||
- [How to set up a self-hosted deployment of LangGraph](../how-tos/deploy-self-hosted.md).
|
||||
|
||||
@@ -40,7 +40,7 @@ LangGraph makes it easy to manage conversation [memory](../concepts/memory.md) i
|
||||
- [How to delete messages](memory/delete-messages.ipynb)
|
||||
- [How to add summary conversation memory](memory/add-summary-conversation-history.ipynb)
|
||||
|
||||
### Human in the Loop
|
||||
### Human-in-the-loop
|
||||
|
||||
[Human-in-the-loop](../concepts/human_in_the_loop.md) functionality allows
|
||||
you to involve humans in the decision-making process of your graph. These how-to guides show how to implement human-in-the-loop workflows in your graph.
|
||||
@@ -99,7 +99,6 @@ These how-to guides show common patterns for tool calling with LangGraph:
|
||||
- [How to run graph asynchronously](async.ipynb)
|
||||
- [How to visualize your graph](visualization.ipynb)
|
||||
- [How to add runtime configuration to your graph](configuration.ipynb)
|
||||
- [How to use a Pydantic model as your state](state-model.ipynb)
|
||||
- [How to add node retries](node-retries.ipynb)
|
||||
- [How to force function calling agent to structure output](react-agent-structured-output.ipynb)
|
||||
- [How to pass custom LangSmith run ID for graph runs](run-id-langsmith.ipynb)
|
||||
|
||||
@@ -3,6 +3,13 @@ title: Reference
|
||||
description: API reference for LangGraph
|
||||
---
|
||||
|
||||
<style>
|
||||
.md-sidebar {
|
||||
display: block !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
# Reference
|
||||
|
||||
Welcome to the LangGraph API reference! This reference provides detailed information about the LangGraph API, including classes, methods, and other components.
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
"from langchain_community.document_loaders.recursive_url_loader import RecursiveUrlLoader\n",
|
||||
"\n",
|
||||
"# LCEL docs\n",
|
||||
"url = \"https://python.langchain.com/docs/concepts/#langchain-expression-language-lcel\"\n",
|
||||
"url = \"https://python.langchain.com/docs/concepts/lcel/\"\n",
|
||||
"loader = RecursiveUrlLoader(\n",
|
||||
" url=url, max_depth=20, extractor=lambda x: Soup(x, \"html.parser\").text\n",
|
||||
")\n",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"% pip install -U langgraph langchain-community langchain-anthropic tavily-python pandas"
|
||||
"%pip install -U langgraph langchain-community langchain-anthropic tavily-python pandas openai"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -55,6 +55,7 @@
|
||||
"\n",
|
||||
"\n",
|
||||
"_set_env(\"ANTHROPIC_API_KEY\")\n",
|
||||
"_set_env(\"OPENAI_API_KEY\")\n",
|
||||
"_set_env(\"TAVILY_API_KEY\")"
|
||||
]
|
||||
},
|
||||
@@ -85,7 +86,9 @@
|
||||
"cell_type": "code",
|
||||
"execution_count": 21,
|
||||
"id": "71638c2a-5038-439e-907a-de2bb548db34",
|
||||
"metadata": {"hide_from_vcr": true},
|
||||
"metadata": {
|
||||
"hide_from_vcr": true
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import os\n",
|
||||
@@ -176,7 +179,9 @@
|
||||
"cell_type": "code",
|
||||
"execution_count": 22,
|
||||
"id": "654e2f81",
|
||||
"metadata": {"hide_from_vcr": true},
|
||||
"metadata": {
|
||||
"hide_from_vcr": true
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import re\n",
|
||||
@@ -1077,7 +1082,7 @@
|
||||
" ),\n",
|
||||
" (\"placeholder\", \"{messages}\"),\n",
|
||||
" ]\n",
|
||||
").partial(time=datetime.now())\n",
|
||||
").partial(time=datetime.now)\n",
|
||||
"\n",
|
||||
"part_1_tools = [\n",
|
||||
" TavilySearchResults(max_results=1),\n",
|
||||
@@ -1893,7 +1898,7 @@
|
||||
" ),\n",
|
||||
" (\"placeholder\", \"{messages}\"),\n",
|
||||
" ]\n",
|
||||
").partial(time=datetime.now())\n",
|
||||
").partial(time=datetime.now)\n",
|
||||
"\n",
|
||||
"part_2_tools = [\n",
|
||||
" TavilySearchResults(max_results=1),\n",
|
||||
@@ -2472,7 +2477,7 @@
|
||||
" ),\n",
|
||||
" (\"placeholder\", \"{messages}\"),\n",
|
||||
" ]\n",
|
||||
").partial(time=datetime.now())\n",
|
||||
").partial(time=datetime.now)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# \"Read\"-only tools (such as retrievers) don't need a user confirmation to use\n",
|
||||
@@ -3183,7 +3188,7 @@
|
||||
" ),\n",
|
||||
" (\"placeholder\", \"{messages}\"),\n",
|
||||
" ]\n",
|
||||
").partial(time=datetime.now())\n",
|
||||
").partial(time=datetime.now)\n",
|
||||
"\n",
|
||||
"update_flight_safe_tools = [search_flights]\n",
|
||||
"update_flight_sensitive_tools = [update_ticket_to_new_flight, cancel_ticket]\n",
|
||||
@@ -3215,7 +3220,7 @@
|
||||
" ),\n",
|
||||
" (\"placeholder\", \"{messages}\"),\n",
|
||||
" ]\n",
|
||||
").partial(time=datetime.now())\n",
|
||||
").partial(time=datetime.now)\n",
|
||||
"\n",
|
||||
"book_hotel_safe_tools = [search_hotels]\n",
|
||||
"book_hotel_sensitive_tools = [book_hotel, update_hotel, cancel_hotel]\n",
|
||||
@@ -3247,7 +3252,7 @@
|
||||
" ),\n",
|
||||
" (\"placeholder\", \"{messages}\"),\n",
|
||||
" ]\n",
|
||||
").partial(time=datetime.now())\n",
|
||||
").partial(time=datetime.now)\n",
|
||||
"\n",
|
||||
"book_car_rental_safe_tools = [search_car_rentals]\n",
|
||||
"book_car_rental_sensitive_tools = [\n",
|
||||
@@ -3282,7 +3287,7 @@
|
||||
" ),\n",
|
||||
" (\"placeholder\", \"{messages}\"),\n",
|
||||
" ]\n",
|
||||
").partial(time=datetime.now())\n",
|
||||
").partial(time=datetime.now)\n",
|
||||
"\n",
|
||||
"book_excursion_safe_tools = [search_trip_recommendations]\n",
|
||||
"book_excursion_sensitive_tools = [book_excursion, update_excursion, cancel_excursion]\n",
|
||||
@@ -3389,7 +3394,7 @@
|
||||
" ),\n",
|
||||
" (\"placeholder\", \"{messages}\"),\n",
|
||||
" ]\n",
|
||||
").partial(time=datetime.now())\n",
|
||||
").partial(time=datetime.now)\n",
|
||||
"primary_assistant_tools = [\n",
|
||||
" TavilySearchResults(max_results=1),\n",
|
||||
" search_flights,\n",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"id": "4a1aae78-88a6-4133-b905-7e46c8e3772f",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Quick Start\n",
|
||||
"# LangGraph Quick Start\n",
|
||||
"\n",
|
||||
"In this comprehensive quick start, we will build a support chatbot in LangGraph that can:\n",
|
||||
"\n",
|
||||
|
||||
+239
-6
@@ -25,6 +25,7 @@ theme:
|
||||
- navigation.sections
|
||||
- navigation.instant.prefetch
|
||||
- navigation.instant.progress
|
||||
- navigation.path
|
||||
- navigation.prune
|
||||
- navigation.tabs
|
||||
- navigation.tabs.sticky
|
||||
@@ -87,12 +88,242 @@ plugins:
|
||||
filters:
|
||||
- "!^_"
|
||||
nav:
|
||||
# Setting the names of the nav items explicitly due to mkdocs
|
||||
# how-reload being a bit buggy with the names of the tabs.
|
||||
- Home: "index.md"
|
||||
- Tutorials: "tutorials/index.md"
|
||||
- Concepts: "concepts/index.md"
|
||||
- "How-to Guides": "how-tos/index.md"
|
||||
- Home: index.md
|
||||
- Tutorials:
|
||||
- tutorials/index.md
|
||||
- Quick Start:
|
||||
- Quick Start: tutorials#quick-start
|
||||
- tutorials/introduction.ipynb
|
||||
- cloud/quick_start.md
|
||||
- Chatbots:
|
||||
- Chatbots: tutorials#chatbots
|
||||
- tutorials/customer-support/customer-support.ipynb
|
||||
- tutorials/chatbots/information-gather-prompting.ipynb
|
||||
- tutorials/code_assistant/langgraph_code_assistant.ipynb
|
||||
- RAG:
|
||||
- RAG: tutorials#rag
|
||||
- tutorials/rag/langgraph_adaptive_rag.ipynb
|
||||
- tutorials/rag/langgraph_adaptive_rag_local.ipynb
|
||||
- tutorials/rag/langgraph_agentic_rag.ipynb
|
||||
- tutorials/rag/langgraph_crag.ipynb
|
||||
- tutorials/rag/langgraph_crag_local.ipynb
|
||||
- tutorials/rag/langgraph_self_rag.ipynb
|
||||
- tutorials/rag/langgraph_self_rag_local.ipynb
|
||||
- tutorials/sql-agent.ipynb
|
||||
- Agent Architectures:
|
||||
- Agent Architectures: tutorials#agent-architectures
|
||||
- Multi-Agent Systems:
|
||||
- Multi-Agent Systems: tutorials#multi-agent-systems
|
||||
- tutorials/multi_agent/multi-agent-collaboration.ipynb
|
||||
- tutorials/multi_agent/agent_supervisor.ipynb
|
||||
- tutorials/multi_agent/hierarchical_agent_teams.ipynb
|
||||
- Planning Agents:
|
||||
- Planning Agents: tutorials#planning-agents
|
||||
- tutorials/plan-and-execute/plan-and-execute.ipynb
|
||||
- tutorials/rewoo/rewoo.ipynb
|
||||
- tutorials/llm-compiler/LLMCompiler.ipynb
|
||||
- Reflection & Critique:
|
||||
- Reflection & Critique: tutorials#reflection-critique
|
||||
- tutorials/reflection/reflection.ipynb
|
||||
- tutorials/reflexion/reflexion.ipynb
|
||||
- tutorials/tot/tot.ipynb
|
||||
- tutorials/lats/lats.ipynb
|
||||
- tutorials/self-discover/self-discover.ipynb
|
||||
- Evaluation & Analysis:
|
||||
- Evaluation & Analysis: tutorials#evaluation
|
||||
- tutorials/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb
|
||||
- tutorials/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb
|
||||
- Experimental:
|
||||
- Experimental: tutorials#experimental
|
||||
- tutorials/storm/storm.ipynb
|
||||
- tutorials/tnt-llm/tnt-llm.ipynb
|
||||
- tutorials/web-navigation/web_voyager.ipynb
|
||||
- tutorials/usaco/usaco.ipynb
|
||||
- tutorials/extraction/retries.ipynb
|
||||
|
||||
- How-to Guides:
|
||||
- how-tos/index.md
|
||||
- LangGraph:
|
||||
- LangGraph: how-tos#langgraph
|
||||
- Controllability:
|
||||
- Controllability: how-tos#controllability
|
||||
- how-tos/branching.ipynb
|
||||
- how-tos/map-reduce.ipynb
|
||||
- how-tos/recursion-limit.ipynb
|
||||
- Persistence:
|
||||
- Persistence: how-tos#persistence
|
||||
- how-tos/persistence.ipynb
|
||||
- how-tos/subgraph-persistence.ipynb
|
||||
- how-tos/cross-thread-persistence.ipynb
|
||||
- how-tos/persistence_postgres.ipynb
|
||||
- how-tos/persistence_mongodb.ipynb
|
||||
- how-tos/persistence_redis.ipynb
|
||||
- Memory:
|
||||
- Memory: how-tos#memory
|
||||
- how-tos/memory/manage-conversation-history.ipynb
|
||||
- how-tos/memory/delete-messages.ipynb
|
||||
- how-tos/memory/add-summary-conversation-history.ipynb
|
||||
- Human-in-the-loop:
|
||||
- Human-in-the-loop: how-tos#human-in-the-loop
|
||||
- how-tos/human_in_the_loop/breakpoints.ipynb
|
||||
- how-tos/human_in_the_loop/dynamic_breakpoints.ipynb
|
||||
- how-tos/human_in_the_loop/edit-graph-state.ipynb
|
||||
- how-tos/human_in_the_loop/wait-user-input.ipynb
|
||||
- how-tos/human_in_the_loop/time-travel.ipynb
|
||||
- how-tos/human_in_the_loop/review-tool-calls.ipynb
|
||||
- Streaming:
|
||||
- Streaming: how-tos#streaming
|
||||
- how-tos/stream-values.ipynb
|
||||
- how-tos/stream-updates.ipynb
|
||||
- how-tos/streaming-tokens.ipynb
|
||||
- how-tos/streaming-tokens-without-langchain.ipynb
|
||||
- how-tos/streaming-content.ipynb
|
||||
- how-tos/stream-multiple.ipynb
|
||||
- how-tos/streaming-events-from-within-tools.ipynb
|
||||
- how-tos/streaming-events-from-within-tools-without-langchain.ipynb
|
||||
- how-tos/streaming-from-final-node.ipynb
|
||||
- how-tos/streaming-subgraphs.ipynb
|
||||
- how-tos/disable-streaming.ipynb
|
||||
- Tool calling:
|
||||
- Tool calling: how-tos#tool-calling
|
||||
- how-tos/tool-calling.ipynb
|
||||
- how-tos/tool-calling-errors.ipynb
|
||||
- how-tos/pass-run-time-values-to-tools.ipynb
|
||||
- how-tos/pass-config-to-tools.ipynb
|
||||
- how-tos/many-tools.ipynb
|
||||
- Subgraphs:
|
||||
- Subgraphs: how-tos#subgraphs
|
||||
- how-tos/subgraph.ipynb
|
||||
- how-tos/subgraphs-manage-state.ipynb
|
||||
- how-tos/subgraph-transform-state.ipynb
|
||||
- State Management:
|
||||
- State Management: how-tos#state-management
|
||||
- how-tos/state-model.ipynb
|
||||
- how-tos/input_output_schema.ipynb
|
||||
- how-tos/pass_private_state.ipynb
|
||||
- Other:
|
||||
- Other: how-tos#other
|
||||
- how-tos/async.ipynb
|
||||
- how-tos/visualization.ipynb
|
||||
- how-tos/configuration.ipynb
|
||||
- how-tos/node-retries.ipynb
|
||||
- how-tos/react-agent-structured-output.ipynb
|
||||
- how-tos/run-id-langsmith.ipynb
|
||||
- how-tos/return-when-recursion-limit-hits.ipynb
|
||||
- Prebuilt ReAct Agent:
|
||||
- Prebuilt ReAct Agent: how-tos#prebuilt-react-agent
|
||||
- how-tos/create-react-agent.ipynb
|
||||
- how-tos/create-react-agent-memory.ipynb
|
||||
- how-tos/create-react-agent-system-prompt.ipynb
|
||||
- how-tos/create-react-agent-hitl.ipynb
|
||||
- how-tos/react-agent-from-scratch.ipynb
|
||||
- LangGraph Platform:
|
||||
- LangGraph Platform: how-tos#langgraph-platform
|
||||
- Application Structure:
|
||||
- Application Structure: how-tos#application-structure
|
||||
- cloud/deployment/setup.md
|
||||
- cloud/deployment/setup_pyproject.md
|
||||
- cloud/deployment/setup_javascript.md
|
||||
- cloud/deployment/custom_docker.md
|
||||
- cloud/deployment/test_locally.md
|
||||
- cloud/deployment/graph_rebuild.md
|
||||
- Deployment:
|
||||
- Deployment: how-tos#deployment
|
||||
- cloud/deployment/cloud.md
|
||||
- how-tos/deploy-self-hosted.md
|
||||
- how-tos/use-remote-graph.md
|
||||
- Assistants:
|
||||
- Assistants: how-tos#assistants
|
||||
- cloud/how-tos/configuration_cloud.md
|
||||
- cloud/how-tos/assistant_versioning.md
|
||||
- Threads:
|
||||
- Threads: how-tos#threads
|
||||
- cloud/how-tos/copy_threads.md
|
||||
- cloud/how-tos/check_thread_status.md
|
||||
- Runs:
|
||||
- Runs: how-tos#runs
|
||||
- cloud/how-tos/background_run.md
|
||||
- cloud/how-tos/same-thread.md
|
||||
- cloud/how-tos/cron_jobs.md
|
||||
- cloud/how-tos/stateless_runs.md
|
||||
- Streaming:
|
||||
- Streaming: how-tos#streaming_1
|
||||
- cloud/how-tos/stream_values.md
|
||||
- cloud/how-tos/stream_updates.md
|
||||
- cloud/how-tos/stream_messages.md
|
||||
- cloud/how-tos/stream_events.md
|
||||
- cloud/how-tos/stream_debug.md
|
||||
- cloud/how-tos/stream_multiple.md
|
||||
- Human-in-the-loop:
|
||||
- Human-in-the-loop: how-tos#human-in-the-loop_1
|
||||
- cloud/how-tos/human_in_the_loop_breakpoint.md
|
||||
- cloud/how-tos/human_in_the_loop_user_input.md
|
||||
- cloud/how-tos/human_in_the_loop_edit_state.md
|
||||
- cloud/how-tos/human_in_the_loop_time_travel.md
|
||||
- cloud/how-tos/human_in_the_loop_review_tool_calls.md
|
||||
- Double-texting:
|
||||
- Double-texting: how-tos#double-texting
|
||||
- cloud/how-tos/interrupt_concurrent.md
|
||||
- cloud/how-tos/rollback_concurrent.md
|
||||
- cloud/how-tos/reject_concurrent.md
|
||||
- cloud/how-tos/enqueue_concurrent.md
|
||||
- Webhooks:
|
||||
- cloud/how-tos/webhooks.md
|
||||
- Cron Jobs:
|
||||
- cloud/how-tos/cron_jobs.md
|
||||
- LangGraph Studio:
|
||||
- LangGraph Studio: how-tos#langgraph-studio
|
||||
- cloud/how-tos/test_deployment.md
|
||||
- cloud/how-tos/test_local_deployment.md
|
||||
- cloud/how-tos/invoke_studio.md
|
||||
- cloud/how-tos/threads_studio.md
|
||||
- Troubleshooting:
|
||||
- Troubleshooting: how-tos#troubleshooting
|
||||
- troubleshooting/errors/index.md
|
||||
- troubleshooting/errors/GRAPH_RECURSION_LIMIT.md
|
||||
- troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md
|
||||
- troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md
|
||||
- troubleshooting/errors/MULTIPLE_SUBGRAPHS.md
|
||||
|
||||
- Conceptual Guides:
|
||||
- concepts/index.md
|
||||
- LangGraph:
|
||||
- LangGraph: concepts#langgraph
|
||||
- concepts/high_level.md
|
||||
- concepts/low_level.md
|
||||
- concepts/agentic_concepts.md
|
||||
- concepts/multi_agent.md
|
||||
- concepts/human_in_the_loop.md
|
||||
- concepts/persistence.md
|
||||
- concepts/memory.md
|
||||
- concepts/streaming.md
|
||||
- concepts/faq.md
|
||||
- LangGraph Platform:
|
||||
- LangGraph Platform: concepts#langgraph-platform
|
||||
- High Level:
|
||||
- High Level: concepts#high-level
|
||||
- concepts/langgraph_platform.md
|
||||
- concepts/deployment_options.md
|
||||
- concepts/plans.md
|
||||
- concepts/template_applications.md
|
||||
- Components:
|
||||
- Components: concepts#components
|
||||
- concepts/langgraph_server.md
|
||||
- concepts/langgraph_studio.md
|
||||
- concepts/langgraph_cli.md
|
||||
- concepts/sdk.md
|
||||
- how-tos/use-remote-graph.md
|
||||
- LangGraph Server:
|
||||
- LangGraph Server: concepts#langgraph-server
|
||||
- concepts/application_structure.md
|
||||
- concepts/assistants.md
|
||||
- concepts/double_texting.md
|
||||
- Deployment Options:
|
||||
- Deployment Options: concepts#deployment-options
|
||||
- concepts/self_hosted.md
|
||||
- concepts/langgraph_cloud.md
|
||||
- concepts/bring_your_own_cloud.md
|
||||
|
||||
- Reference:
|
||||
- "reference/index.md"
|
||||
- Library:
|
||||
@@ -206,3 +437,5 @@ validation:
|
||||
# because it creates anchors in the generated report
|
||||
# and those anchors are not available in the actual doc
|
||||
anchors: info
|
||||
# this is needed to handle headers with anchors for nav
|
||||
not_found: info
|
||||
@@ -34,6 +34,17 @@
|
||||
color: #1E88E5;
|
||||
}
|
||||
|
||||
.md-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Show sidebar on mobile */
|
||||
@media screen and (max-width: 1220px) {
|
||||
.md-sidebar--primary {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.md-typeset a:hover {
|
||||
color: #1565C0;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ from contextlib import contextmanager
|
||||
from typing import Any, Iterator, Optional, Sequence, Union
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from psycopg import Connection, Cursor, Pipeline
|
||||
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
||||
from psycopg.errors import UndefinedTable
|
||||
from psycopg.rows import DictRow, dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
@@ -52,6 +52,7 @@ class PostgresSaver(BasePostgresSaver):
|
||||
self.conn = conn
|
||||
self.pipe = pipe
|
||||
self.lock = threading.Lock()
|
||||
self.supports_pipeline = Capabilities().has_pipeline()
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
@@ -365,6 +366,13 @@ class PostgresSaver(BasePostgresSaver):
|
||||
|
||||
@contextmanager
|
||||
def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor[DictRow]]:
|
||||
"""Create a database cursor as a context manager.
|
||||
|
||||
Args:
|
||||
pipeline (bool): whether to use pipeline for the DB operations inside the context manager.
|
||||
Will be applied regardless of whether the PostgresSaver instance was initialized with a pipeline.
|
||||
If pipeline mode is not supported, will fall back to using transaction context manager.
|
||||
"""
|
||||
with _get_connection(self.conn) as conn:
|
||||
if self.pipe:
|
||||
# a connection in pipeline mode can be used concurrently
|
||||
@@ -379,10 +387,17 @@ class PostgresSaver(BasePostgresSaver):
|
||||
elif pipeline:
|
||||
# a connection not in pipeline mode can only be used by one
|
||||
# thread/coroutine at a time, so we acquire a lock
|
||||
with self.lock, conn.pipeline(), conn.cursor(
|
||||
binary=True, row_factory=dict_row
|
||||
) as cur:
|
||||
yield cur
|
||||
if self.supports_pipeline:
|
||||
with self.lock, conn.pipeline(), conn.cursor(
|
||||
binary=True, row_factory=dict_row
|
||||
) as cur:
|
||||
yield cur
|
||||
else:
|
||||
# Use connection's transaction context manager when pipeline mode not supported
|
||||
with self.lock, conn.transaction(), conn.cursor(
|
||||
binary=True, row_factory=dict_row
|
||||
) as cur:
|
||||
yield cur
|
||||
else:
|
||||
with self.lock, conn.cursor(binary=True, row_factory=dict_row) as cur:
|
||||
yield cur
|
||||
|
||||
@@ -3,7 +3,7 @@ from contextlib import asynccontextmanager
|
||||
from typing import Any, AsyncIterator, Iterator, Optional, Sequence, Union
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline
|
||||
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
||||
from psycopg.errors import UndefinedTable
|
||||
from psycopg.rows import DictRow, dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
@@ -55,6 +55,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
self.pipe = pipe
|
||||
self.lock = asyncio.Lock()
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.supports_pipeline = Capabilities().has_pipeline()
|
||||
|
||||
@classmethod
|
||||
@asynccontextmanager
|
||||
@@ -323,6 +324,13 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
async def _cursor(
|
||||
self, *, pipeline: bool = False
|
||||
) -> AsyncIterator[AsyncCursor[DictRow]]:
|
||||
"""Create a database cursor as a context manager.
|
||||
|
||||
Args:
|
||||
pipeline (bool): whether to use pipeline for the DB operations inside the context manager.
|
||||
Will be applied regardless of whether the AsyncPostgresSaver instance was initialized with a pipeline.
|
||||
If pipeline mode is not supported, will fall back to using transaction context manager.
|
||||
"""
|
||||
async with _get_connection(self.conn) as conn:
|
||||
if self.pipe:
|
||||
# a connection in pipeline mode can be used concurrently
|
||||
@@ -337,10 +345,17 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
elif pipeline:
|
||||
# a connection not in pipeline mode can only be used by one
|
||||
# thread/coroutine at a time, so we acquire a lock
|
||||
async with self.lock, conn.pipeline(), conn.cursor(
|
||||
binary=True, row_factory=dict_row
|
||||
) as cur:
|
||||
yield cur
|
||||
if self.supports_pipeline:
|
||||
async with self.lock, conn.pipeline(), conn.cursor(
|
||||
binary=True, row_factory=dict_row
|
||||
) as cur:
|
||||
yield cur
|
||||
else:
|
||||
# Use connection's transaction context manager when pipeline mode not supported
|
||||
async with self.lock, conn.transaction(), conn.cursor(
|
||||
binary=True, row_factory=dict_row
|
||||
) as cur:
|
||||
yield cur
|
||||
else:
|
||||
async with self.lock, conn.cursor(
|
||||
binary=True, row_factory=dict_row
|
||||
|
||||
@@ -84,7 +84,7 @@ select
|
||||
and cw.checkpoint_id = checkpoints.checkpoint_id
|
||||
) as pending_writes,
|
||||
(
|
||||
select array_agg(array[cw.type::bytea, cw.blob] order by cw.idx)
|
||||
select array_agg(array[cw.type::bytea, cw.blob] order by cw.task_id, cw.idx)
|
||||
from checkpoint_writes cw
|
||||
where cw.thread_id = checkpoints.thread_id
|
||||
and cw.checkpoint_ns = checkpoints.checkpoint_ns
|
||||
@@ -133,6 +133,7 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
|
||||
INSERT_CHECKPOINT_WRITES_SQL = INSERT_CHECKPOINT_WRITES_SQL
|
||||
|
||||
jsonplus_serde = JsonPlusSerializer()
|
||||
supports_pipeline: bool
|
||||
|
||||
def _load_checkpoint(
|
||||
self,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "2.0.2"
|
||||
version = "2.0.3"
|
||||
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -24,6 +24,8 @@ from langgraph.checkpoint.serde.base import SerializerProtocol, maybe_add_typed_
|
||||
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
|
||||
from langgraph.checkpoint.serde.types import (
|
||||
ERROR,
|
||||
INTERRUPT,
|
||||
RESUME,
|
||||
SCHEDULED,
|
||||
ChannelProtocol,
|
||||
SendProtocol,
|
||||
@@ -37,12 +39,13 @@ PendingWrite = Tuple[str, str, Any]
|
||||
class CheckpointMetadata(TypedDict, total=False):
|
||||
"""Metadata associated with a checkpoint."""
|
||||
|
||||
source: Literal["input", "loop", "update"]
|
||||
source: Literal["input", "loop", "update", "fork"]
|
||||
"""The source of the checkpoint.
|
||||
|
||||
- "input": The checkpoint was created from an input to invoke/stream/batch.
|
||||
- "loop": The checkpoint was created from inside the pregel loop.
|
||||
- "update": The checkpoint was created from a manual state update.
|
||||
- "fork": The checkpoint was created as a copy of another checkpoint.
|
||||
"""
|
||||
step: int
|
||||
"""The step number of the checkpoint.
|
||||
@@ -449,4 +452,4 @@ Special writes (e.g. errors) map to negative indices, to avoid those writes from
|
||||
conflicting with regular writes.
|
||||
Each Checkpointer implementation should use this mapping in put_writes.
|
||||
"""
|
||||
WRITES_IDX_MAP = {ERROR: -1, SCHEDULED: -2}
|
||||
WRITES_IDX_MAP = {ERROR: -1, SCHEDULED: -2, INTERRUPT: -3, RESUME: -4}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import random
|
||||
import shutil
|
||||
from collections import defaultdict
|
||||
from contextlib import AbstractAsyncContextManager, AbstractContextManager
|
||||
from contextlib import AbstractAsyncContextManager, AbstractContextManager, ExitStack
|
||||
from functools import partial
|
||||
from types import TracebackType
|
||||
from typing import Any, AsyncIterator, Dict, Iterator, Optional, Sequence, Tuple
|
||||
from typing import Any, AsyncIterator, Dict, Iterator, Optional, Sequence, Tuple, Type
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
@@ -20,6 +24,8 @@ from langgraph.checkpoint.base import (
|
||||
)
|
||||
from langgraph.checkpoint.serde.types import TASKS, ChannelProtocol
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MemorySaver(
|
||||
BaseCheckpointSaver[str], AbstractContextManager, AbstractAsyncContextManager
|
||||
@@ -68,13 +74,18 @@ class MemorySaver(
|
||||
self,
|
||||
*,
|
||||
serde: Optional[SerializerProtocol] = None,
|
||||
factory: Type[defaultdict] = defaultdict,
|
||||
) -> None:
|
||||
super().__init__(serde=serde)
|
||||
self.storage = defaultdict(lambda: defaultdict(dict))
|
||||
self.writes = defaultdict(dict)
|
||||
self.storage = factory(lambda: defaultdict(dict))
|
||||
self.writes = factory(dict)
|
||||
self.stack = ExitStack()
|
||||
if factory is not defaultdict:
|
||||
self.stack.enter_context(self.storage) # type: ignore[arg-type]
|
||||
self.stack.enter_context(self.writes) # type: ignore[arg-type]
|
||||
|
||||
def __enter__(self) -> "MemorySaver":
|
||||
return self
|
||||
return self.stack.__enter__()
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
@@ -82,10 +93,10 @@ class MemorySaver(
|
||||
exc_value: Optional[BaseException],
|
||||
traceback: Optional[TracebackType],
|
||||
) -> Optional[bool]:
|
||||
return
|
||||
return self.stack.__exit__(exc_type, exc_value, traceback)
|
||||
|
||||
async def __aenter__(self) -> "MemorySaver":
|
||||
return self
|
||||
return self.stack.__enter__()
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
@@ -93,7 +104,7 @@ class MemorySaver(
|
||||
__exc_value: Optional[BaseException],
|
||||
__traceback: Optional[TracebackType],
|
||||
) -> Optional[bool]:
|
||||
return
|
||||
return self.stack.__exit__(__exc_type, __exc_value, __traceback)
|
||||
|
||||
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||
"""Get a checkpoint tuple from the in-memory storage.
|
||||
@@ -364,8 +375,12 @@ class MemorySaver(
|
||||
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
||||
checkpoint_id = config["configurable"]["checkpoint_id"]
|
||||
outer_key = (thread_id, checkpoint_ns, checkpoint_id)
|
||||
outer_writes_ = self.writes.get(outer_key)
|
||||
for idx, (c, v) in enumerate(writes):
|
||||
inner_key = (task_id, WRITES_IDX_MAP.get(c, idx))
|
||||
if inner_key[1] >= 0 and outer_writes_ and inner_key in outer_writes_:
|
||||
continue
|
||||
|
||||
self.writes[outer_key][inner_key] = (task_id, c, self.serde.dumps_typed(v))
|
||||
|
||||
async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||
@@ -474,3 +489,76 @@ class MemorySaver(
|
||||
next_v = current_v + 1
|
||||
next_h = random.random()
|
||||
return f"{next_v:032}.{next_h:016}"
|
||||
|
||||
|
||||
class PersistentDict(defaultdict):
|
||||
"""Persistent dictionary with an API compatible with shelve and anydbm.
|
||||
|
||||
The dict is kept in memory, so the dictionary operations run as fast as
|
||||
a regular dictionary.
|
||||
|
||||
Write to disk is delayed until close or sync (similar to gdbm's fast mode).
|
||||
|
||||
Input file format is automatically discovered.
|
||||
Output file format is selectable between pickle, json, and csv.
|
||||
All three serialization formats are backed by fast C implementations.
|
||||
|
||||
Adapted from https://code.activestate.com/recipes/576642-persistent-dict-with-multiple-standard-file-format/
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args: Any, filename: str, **kwds: Any) -> None:
|
||||
self.flag = "c" # r=readonly, c=create, or n=new
|
||||
self.mode = None # None or an octal triple like 0644
|
||||
self.format = "pickle" # 'csv', 'json', or 'pickle'
|
||||
self.filename = filename
|
||||
super().__init__(*args, **kwds)
|
||||
|
||||
def sync(self) -> None:
|
||||
"Write dict to disk"
|
||||
if self.flag == "r":
|
||||
return
|
||||
tempname = self.filename + ".tmp"
|
||||
fileobj = open(tempname, "wb" if self.format == "pickle" else "w")
|
||||
try:
|
||||
self.dump(fileobj)
|
||||
except Exception:
|
||||
os.remove(tempname)
|
||||
raise
|
||||
finally:
|
||||
fileobj.close()
|
||||
shutil.move(tempname, self.filename) # atomic commit
|
||||
if self.mode is not None:
|
||||
os.chmod(self.filename, self.mode)
|
||||
|
||||
def close(self) -> None:
|
||||
self.sync()
|
||||
self.clear()
|
||||
|
||||
def __enter__(self) -> "PersistentDict":
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc_info: Any) -> None:
|
||||
self.close()
|
||||
|
||||
def dump(self, fileobj: Any) -> None:
|
||||
if self.format == "pickle":
|
||||
pickle.dump(dict(self), fileobj, 2)
|
||||
else:
|
||||
raise NotImplementedError("Unknown format: " + repr(self.format))
|
||||
|
||||
def load(self) -> None:
|
||||
# try formats from most restrictive to least restrictive
|
||||
if self.flag == "n":
|
||||
return
|
||||
with open(self.filename, "rb" if self.format == "pickle" else "r") as fileobj:
|
||||
for loader in (pickle.load,):
|
||||
fileobj.seek(0)
|
||||
try:
|
||||
return self.update(loader(fileobj))
|
||||
except EOFError:
|
||||
return
|
||||
except Exception:
|
||||
logging.error(f"Failed to load file: {fileobj.name}")
|
||||
raise
|
||||
raise ValueError("File not in a supported f ormat")
|
||||
|
||||
@@ -11,6 +11,8 @@ from typing_extensions import Self
|
||||
|
||||
ERROR = "__error__"
|
||||
SCHEDULED = "__scheduled__"
|
||||
INTERRUPT = "__interrupt__"
|
||||
RESUME = "__resume__"
|
||||
TASKS = "__pregel_tasks"
|
||||
|
||||
Value = TypeVar("Value", covariant=True)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.2"
|
||||
version = "2.0.5"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -314,7 +314,7 @@ async def test_cannot_put_empty_namespace() -> None:
|
||||
assert store.get(("langgraph", "foo"), "bar") is None
|
||||
|
||||
class MockAsyncBatchedStore(AsyncBatchedBaseStore):
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._store = InMemoryStore()
|
||||
|
||||
@@ -340,13 +340,17 @@ async def test_cannot_put_empty_namespace() -> None:
|
||||
await async_store.aput(("langgraph", "foo"), "bar", doc)
|
||||
|
||||
await async_store.aput(("foo", "langgraph", "foo"), "bar", doc)
|
||||
assert (await async_store.aget(("foo", "langgraph", "foo"), "bar")).value == doc
|
||||
val = await async_store.aget(("foo", "langgraph", "foo"), "bar")
|
||||
assert val is not None
|
||||
assert val.value == doc
|
||||
assert (await async_store.asearch(("foo", "langgraph", "foo")))[0].value == doc
|
||||
await async_store.adelete(("foo", "langgraph", "foo"), "bar")
|
||||
assert (await async_store.aget(("foo", "langgraph", "foo"), "bar")) is None
|
||||
|
||||
await async_store.abatch([PutOp(("valid", "namespace"), "key", doc)])
|
||||
assert (await async_store.aget(("valid", "namespace"), "key")).value == doc
|
||||
val = await async_store.aget(("valid", "namespace"), "key")
|
||||
assert val is not None
|
||||
assert val.value == doc
|
||||
assert (await async_store.asearch(("valid", "namespace")))[0].value == doc
|
||||
await async_store.adelete(("valid", "namespace"), "key")
|
||||
assert (await async_store.aget(("valid", "namespace"), "key")) is None
|
||||
|
||||
+4
-2
@@ -1,11 +1,13 @@
|
||||
.PHONY: test lint format
|
||||
.PHONY: test lint format test-integration
|
||||
|
||||
######################
|
||||
# TESTING AND COVERAGE
|
||||
######################
|
||||
|
||||
test:
|
||||
poetry run pytest tests
|
||||
poetry run pytest tests/unit_tests
|
||||
test-integration:
|
||||
poetry run pytest tests/integration_tests
|
||||
|
||||
######################
|
||||
# LINTING AND FORMATTING
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Callable, Optional, Sequence
|
||||
|
||||
import click
|
||||
import click.exceptions
|
||||
from click import secho
|
||||
|
||||
import langgraph_cli.config
|
||||
import langgraph_cli.docker
|
||||
@@ -15,6 +16,8 @@ from langgraph_cli.constants import DEFAULT_CONFIG, DEFAULT_PORT
|
||||
from langgraph_cli.docker import DockerCapabilities
|
||||
from langgraph_cli.exec import Runner, subp_exec
|
||||
from langgraph_cli.progress import Progress
|
||||
from langgraph_cli.templates import TEMPLATE_HELP_STRING, create_new
|
||||
from langgraph_cli.version import __version__
|
||||
|
||||
OPT_DOCKER_COMPOSE = click.option(
|
||||
"--docker-compose",
|
||||
@@ -148,6 +151,7 @@ OPT_POSTGRES_URI = click.option(
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version=__version__, prog_name="LangGraph CLI")
|
||||
def cli():
|
||||
pass
|
||||
|
||||
@@ -167,9 +171,7 @@ def cli():
|
||||
is_flag=True,
|
||||
help="Wait for services to start before returning. Implies --detach",
|
||||
)
|
||||
@cli.command(
|
||||
help="Start langgraph API server. For local testing, requires a LangSmith API key with access to LangGraph Cloud closed beta. Requires a license key for production use."
|
||||
)
|
||||
@cli.command(help="🚀 Launch LangGraph API server.")
|
||||
@log_command
|
||||
def up(
|
||||
config: pathlib.Path,
|
||||
@@ -334,7 +336,7 @@ def _build(
|
||||
)
|
||||
@click.argument("docker_build_args", nargs=-1, type=click.UNPROCESSED)
|
||||
@cli.command(
|
||||
help="Build langgraph API server docker image",
|
||||
help="📦 Build LangGraph API server Docker image.",
|
||||
context_settings=dict(
|
||||
ignore_unknown_options=True,
|
||||
),
|
||||
@@ -357,14 +359,86 @@ def build(
|
||||
)
|
||||
|
||||
|
||||
def _get_docker_ignore_content() -> str:
|
||||
"""Return the content of a .dockerignore file.
|
||||
|
||||
This file is used to exclude files and directories from the Docker build context.
|
||||
|
||||
It may be overly broad, but it's better to be safe than sorry.
|
||||
|
||||
The main goal is to exclude .env files by default.
|
||||
"""
|
||||
return """\
|
||||
# Ignore node_modules and other dependency directories
|
||||
node_modules
|
||||
bower_components
|
||||
vendor
|
||||
|
||||
# Ignore logs and temporary files
|
||||
*.log
|
||||
*.tmp
|
||||
*.swp
|
||||
|
||||
# Ignore .env files and other environment files
|
||||
.env
|
||||
.env.*
|
||||
*.local
|
||||
|
||||
# Ignore git-related files
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Ignore Docker-related files and configs
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
|
||||
# Ignore build and cache directories
|
||||
dist
|
||||
build
|
||||
.cache
|
||||
__pycache__
|
||||
|
||||
# Ignore IDE and editor configurations
|
||||
.vscode
|
||||
.idea
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
.DS_Store # macOS-specific
|
||||
|
||||
# Ignore test and coverage files
|
||||
coverage
|
||||
*.coverage
|
||||
*.test.js
|
||||
*.spec.js
|
||||
tests
|
||||
"""
|
||||
|
||||
|
||||
@OPT_CONFIG
|
||||
@click.argument("save_path", type=click.Path(resolve_path=True))
|
||||
@cli.command(help="Generate a Dockerfile for langgraph API server")
|
||||
@cli.command(
|
||||
help="🐳 Generate a Dockerfile for the LangGraph API server, with Docker Compose options."
|
||||
)
|
||||
@click.option(
|
||||
# Add a flag for adding a docker-compose.yml file as part of the output
|
||||
"--add-docker-compose",
|
||||
help=(
|
||||
"Add additional files for running the LangGraph API server with "
|
||||
"docker-compose. These files include a docker-compose.yml, .env file, "
|
||||
"and a .dockerignore file."
|
||||
),
|
||||
is_flag=True,
|
||||
)
|
||||
@log_command
|
||||
def dockerfile(save_path: pathlib.Path, config: pathlib.Path):
|
||||
with open(config) as f:
|
||||
def dockerfile(save_path: str, config: pathlib.Path, add_docker_compose: bool) -> None:
|
||||
save_path = pathlib.Path(save_path).absolute()
|
||||
secho(f"🔍 Validating configuration at path: {config}", fg="yellow")
|
||||
with open(config, encoding="utf-8") as f:
|
||||
config_json = langgraph_cli.config.validate_config(json.load(f))
|
||||
with open(save_path, "w") as f:
|
||||
secho("✅ Configuration validated!", fg="green")
|
||||
|
||||
secho(f"📝 Generating Dockerfile at {save_path}", fg="yellow")
|
||||
with open(str(save_path), "w", encoding="utf-8") as f:
|
||||
f.write(
|
||||
langgraph_cli.config.config_to_docker(
|
||||
config,
|
||||
@@ -374,6 +448,79 @@ def dockerfile(save_path: pathlib.Path, config: pathlib.Path):
|
||||
else "langchain/langgraph-api",
|
||||
)
|
||||
)
|
||||
secho("✅ Created: Dockerfile", fg="green")
|
||||
|
||||
if add_docker_compose:
|
||||
# Add docker compose and related files
|
||||
# Add .dockerignore file in the same directory as the Dockerfile
|
||||
with open(str(save_path.parent / ".dockerignore"), "w", encoding="utf-8") as f:
|
||||
f.write(_get_docker_ignore_content())
|
||||
secho("✅ Created: .dockerignore", fg="green")
|
||||
|
||||
# Generate a docker-compose.yml file
|
||||
path = str(save_path.parent / "docker-compose.yml")
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
with Runner() as runner:
|
||||
capabilities = langgraph_cli.docker.check_capabilities(runner)
|
||||
|
||||
compose_dict = langgraph_cli.docker.compose_as_dict(
|
||||
capabilities,
|
||||
port=8123,
|
||||
)
|
||||
# Add .env file to the docker-compose.yml for the langgraph-api service
|
||||
compose_dict["services"]["langgraph-api"]["env_file"] = [".env"]
|
||||
# Add the Dockerfile to the build context
|
||||
compose_dict["services"]["langgraph-api"]["build"] = {
|
||||
"context": ".",
|
||||
"dockerfile": save_path.name,
|
||||
}
|
||||
f.write(langgraph_cli.docker.dict_to_yaml(compose_dict))
|
||||
secho("✅ Created: docker-compose.yml", fg="green")
|
||||
|
||||
# Check if the .env file exists in the same directory as the Dockerfile
|
||||
if not (save_path.parent / ".env").exists():
|
||||
# Also add an empty .env file
|
||||
with open(str(save_path.parent / ".env"), "w", encoding="utf-8") as f:
|
||||
f.writelines(
|
||||
[
|
||||
"# Uncomment the following line to add your LangSmith API key",
|
||||
"\n",
|
||||
"# LANGSMITH_API_KEY=your-api-key",
|
||||
"\n",
|
||||
"# Or if you have a LangGraph Cloud license key, "
|
||||
"then uncomment the following line: ",
|
||||
"\n",
|
||||
"# LANGGRAPH_CLOUD_LICENSE_KEY=your-license-key",
|
||||
"\n",
|
||||
"# Add any other environment variables go below...",
|
||||
]
|
||||
)
|
||||
|
||||
secho("✅ Created: .env", fg="green")
|
||||
else:
|
||||
# Do nothing since the .env file already exists. Not a great
|
||||
# idea to overwrite in case the user has added custom env vars set
|
||||
# in the .env file already.
|
||||
secho("➖ Skipped: .env. It already exists!", fg="yellow")
|
||||
|
||||
secho(
|
||||
f"🎉 Files generated successfully at path {save_path.parent}!",
|
||||
fg="cyan",
|
||||
bold=True,
|
||||
)
|
||||
|
||||
|
||||
@click.argument("path", required=False)
|
||||
@click.option(
|
||||
"--template",
|
||||
type=str,
|
||||
help=TEMPLATE_HELP_STRING,
|
||||
)
|
||||
@cli.command("new", help="🌱 Create a new LangGraph project from a template.")
|
||||
@log_command
|
||||
def new(path: Optional[str], template: Optional[str]) -> None:
|
||||
"""Create a new LangGraph project from a template."""
|
||||
return create_new(path, template)
|
||||
|
||||
|
||||
def prepare_args_and_stdin(
|
||||
|
||||
@@ -289,17 +289,41 @@ ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
|
||||
def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image: str):
|
||||
faux_path = f"/deps/{config_path.parent.name}"
|
||||
|
||||
def test_file(file_name):
|
||||
full_path = config_path.parent / file_name
|
||||
try:
|
||||
return full_path.is_file()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
npm, yarn, pnpm = [
|
||||
test_file("package-lock.json"),
|
||||
test_file("yarn.lock"),
|
||||
test_file("pnpm-lock.yaml"),
|
||||
]
|
||||
|
||||
if yarn:
|
||||
install_cmd = "yarn install --frozen-lockfile"
|
||||
elif pnpm:
|
||||
install_cmd = "pnpm i --frozen-lockfile"
|
||||
elif npm:
|
||||
install_cmd = "npm ci"
|
||||
else:
|
||||
install_cmd = "npm i"
|
||||
|
||||
return f"""FROM {base_image}:{config['node_version']}
|
||||
|
||||
{os.linesep.join(config["dockerfile_lines"])}
|
||||
|
||||
ADD . {faux_path}
|
||||
|
||||
RUN cd {faux_path} && yarn install --frozen-lockfile
|
||||
RUN cd {faux_path} && {install_cmd}
|
||||
|
||||
ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
|
||||
|
||||
WORKDIR {faux_path}"""
|
||||
WORKDIR {faux_path}
|
||||
|
||||
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts"""
|
||||
|
||||
|
||||
def config_to_docker(config_path: pathlib.Path, config: Config, base_image: str):
|
||||
|
||||
@@ -12,34 +12,6 @@ DEFAULT_POSTGRES_URI = (
|
||||
"postgres://postgres:postgres@langgraph-postgres:5432/postgres?sslmode=disable"
|
||||
)
|
||||
|
||||
REDIS = """
|
||||
langgraph-redis:
|
||||
image: redis:6
|
||||
healthcheck:
|
||||
test: redis-cli ping
|
||||
interval: 5s
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
"""
|
||||
|
||||
DB = """
|
||||
langgraph-postgres:
|
||||
image: postgres:16
|
||||
ports:
|
||||
- "5433:5432"
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
volumes:
|
||||
- langgraph-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: pg_isready -U postgres
|
||||
start_period: 10s
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
"""
|
||||
|
||||
|
||||
class Version(NamedTuple):
|
||||
major: int
|
||||
@@ -116,28 +88,149 @@ def check_capabilities(runner) -> DockerCapabilities:
|
||||
|
||||
def debugger_compose(
|
||||
*, port: Optional[int] = None, base_url: Optional[str] = None
|
||||
) -> str:
|
||||
) -> dict:
|
||||
if port is None:
|
||||
return ""
|
||||
|
||||
compose_str = """
|
||||
langgraph-debugger:
|
||||
image: langchain/langgraph-debugger
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
langgraph-postgres:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "{port}:3968"
|
||||
"""
|
||||
config = {
|
||||
"langgraph-debugger": {
|
||||
"image": "langchain/langgraph-debugger",
|
||||
"restart": "on-failure",
|
||||
"depends_on": {
|
||||
"langgraph-postgres": {"condition": "service_healthy"},
|
||||
},
|
||||
"ports": [f'"{port}:3968"'],
|
||||
}
|
||||
}
|
||||
|
||||
if base_url:
|
||||
compose_str += """
|
||||
environment:
|
||||
VITE_STUDIO_LOCAL_GRAPH_URL: {base_url}
|
||||
"""
|
||||
config["langgraph-debugger"]["environment"] = {
|
||||
"VITE_STUDIO_LOCAL_GRAPH_URL": base_url
|
||||
}
|
||||
|
||||
return compose_str.format(port=port, base_url=base_url)
|
||||
return config
|
||||
|
||||
|
||||
# Function to convert dictionary to YAML
|
||||
def dict_to_yaml(d: dict, *, indent: int = 0) -> str:
|
||||
"""Convert a dictionary to a YAML string."""
|
||||
yaml_str = ""
|
||||
|
||||
for idx, (key, value) in enumerate(d.items()):
|
||||
# Format things in a visually appealing way
|
||||
# Use an extra newline for top-level keys only
|
||||
if idx >= 1 and indent < 2:
|
||||
yaml_str += "\n"
|
||||
space = " " * indent
|
||||
if isinstance(value, dict):
|
||||
yaml_str += f"{space}{key}:\n" + dict_to_yaml(value, indent=indent + 1)
|
||||
elif isinstance(value, list):
|
||||
yaml_str += f"{space}{key}:\n"
|
||||
for item in value:
|
||||
yaml_str += f"{space} - {item}\n"
|
||||
else:
|
||||
yaml_str += f"{space}{key}: {value}\n"
|
||||
return yaml_str
|
||||
|
||||
|
||||
def compose_as_dict(
|
||||
capabilities: DockerCapabilities,
|
||||
*,
|
||||
port: int,
|
||||
debugger_port: Optional[int] = None,
|
||||
debugger_base_url: Optional[str] = None,
|
||||
# postgres://user:password@host:port/database?option=value
|
||||
postgres_uri: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Create a docker compose file as a dictionary in YML style."""
|
||||
if postgres_uri is None:
|
||||
include_db = True
|
||||
postgres_uri = DEFAULT_POSTGRES_URI
|
||||
else:
|
||||
include_db = False
|
||||
|
||||
# The services below are defined in a non-intuitive order to match
|
||||
# the existing unit tests for this function.
|
||||
# It's fine to re-order just requires updating the unit tests, so it should
|
||||
# be done with caution.
|
||||
|
||||
# Define the Redis service first as per the test order
|
||||
services = {
|
||||
"langgraph-redis": {
|
||||
"image": "redis:6",
|
||||
"healthcheck": {
|
||||
"test": "redis-cli ping",
|
||||
"interval": "5s",
|
||||
"timeout": "1s",
|
||||
"retries": 5,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# Add Postgres service before langgraph-api if it is needed
|
||||
if include_db:
|
||||
services["langgraph-postgres"] = {
|
||||
"image": "postgres:16",
|
||||
"ports": ['"5433:5432"'],
|
||||
"environment": {
|
||||
"POSTGRES_DB": "postgres",
|
||||
"POSTGRES_USER": "postgres",
|
||||
"POSTGRES_PASSWORD": "postgres",
|
||||
},
|
||||
"volumes": ["langgraph-data:/var/lib/postgresql/data"],
|
||||
"healthcheck": {
|
||||
"test": "pg_isready -U postgres",
|
||||
"start_period": "10s",
|
||||
"timeout": "1s",
|
||||
"retries": 5,
|
||||
},
|
||||
}
|
||||
if capabilities.healthcheck_start_interval:
|
||||
services["langgraph-postgres"]["healthcheck"]["interval"] = "60s"
|
||||
services["langgraph-postgres"]["healthcheck"]["start_interval"] = "1s"
|
||||
else:
|
||||
services["langgraph-postgres"]["healthcheck"]["interval"] = "5s"
|
||||
|
||||
# Add optional debugger service if debugger_port is specified
|
||||
if debugger_port:
|
||||
services["langgraph-debugger"] = debugger_compose(
|
||||
port=debugger_port, base_url=debugger_base_url
|
||||
)["langgraph-debugger"]
|
||||
|
||||
# Add langgraph-api service
|
||||
services["langgraph-api"] = {
|
||||
"ports": [f'"{port}:8000"'],
|
||||
"depends_on": {
|
||||
"langgraph-redis": {"condition": "service_healthy"},
|
||||
},
|
||||
"environment": {
|
||||
"REDIS_URI": "redis://langgraph-redis:6379",
|
||||
"POSTGRES_URI": postgres_uri,
|
||||
},
|
||||
}
|
||||
|
||||
# If Postgres is included, add it to the dependencies of langgraph-api
|
||||
if include_db:
|
||||
services["langgraph-api"]["depends_on"]["langgraph-postgres"] = {
|
||||
"condition": "service_healthy"
|
||||
}
|
||||
|
||||
# Additional healthcheck for langgraph-api if required
|
||||
if capabilities.healthcheck_start_interval:
|
||||
services["langgraph-api"]["healthcheck"] = {
|
||||
"test": "python /api/healthcheck.py",
|
||||
"interval": "60s",
|
||||
"start_interval": "1s",
|
||||
"start_period": "10s",
|
||||
}
|
||||
|
||||
# Final compose dictionary with volumes included if needed
|
||||
compose_dict = {}
|
||||
if include_db:
|
||||
compose_dict["volumes"] = {"langgraph-data": {"driver": "local"}}
|
||||
compose_dict["services"] = services
|
||||
|
||||
return compose_dict
|
||||
|
||||
|
||||
def compose(
|
||||
@@ -149,54 +242,13 @@ def compose(
|
||||
# postgres://user:password@host:port/database?option=value
|
||||
postgres_uri: Optional[str] = None,
|
||||
) -> str:
|
||||
if postgres_uri is None:
|
||||
include_db = True
|
||||
postgres_uri = DEFAULT_POSTGRES_URI
|
||||
else:
|
||||
include_db = False
|
||||
|
||||
db = DB.format() if include_db else ""
|
||||
volumes = (
|
||||
"""volumes:
|
||||
langgraph-data:
|
||||
driver: local
|
||||
"""
|
||||
if include_db
|
||||
else ""
|
||||
"""Create a docker compose file as a string."""
|
||||
compose_content = compose_as_dict(
|
||||
capabilities,
|
||||
port=port,
|
||||
debugger_port=debugger_port,
|
||||
debugger_base_url=debugger_base_url,
|
||||
postgres_uri=postgres_uri,
|
||||
)
|
||||
if db:
|
||||
if capabilities.healthcheck_start_interval:
|
||||
db += """
|
||||
interval: 60s
|
||||
start_interval: 1s"""
|
||||
else:
|
||||
db += """
|
||||
interval: 5s"""
|
||||
|
||||
compose_str = f"""{volumes}services:
|
||||
{REDIS}
|
||||
{db}
|
||||
{debugger_compose(port=debugger_port, base_url=debugger_base_url)}
|
||||
langgraph-api:
|
||||
ports:
|
||||
- "{port}:8000\"
|
||||
depends_on:
|
||||
langgraph-redis:
|
||||
condition: service_healthy"""
|
||||
if include_db:
|
||||
compose_str += """
|
||||
langgraph-postgres:
|
||||
condition: service_healthy"""
|
||||
compose_str += f"""
|
||||
environment:
|
||||
REDIS_URI: redis://langgraph-redis:6379
|
||||
POSTGRES_URI: {postgres_uri}
|
||||
"""
|
||||
if capabilities.healthcheck_start_interval:
|
||||
compose_str += """ healthcheck:
|
||||
test: python /api/healthcheck.py
|
||||
interval: 60s
|
||||
start_interval: 1s
|
||||
start_period: 10s"""
|
||||
|
||||
compose_str = dict_to_yaml(compose_content)
|
||||
return compose_str
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from io import BytesIO
|
||||
from typing import Dict, Optional
|
||||
from urllib import error, request
|
||||
from zipfile import ZipFile
|
||||
|
||||
import click
|
||||
|
||||
TEMPLATES: Dict[str, Dict[str, str]] = {
|
||||
"New LangGraph Project": {
|
||||
"description": "A simple, minimal chatbot with memory.",
|
||||
"python": "https://github.com/langchain-ai/new-langgraph-project/archive/refs/heads/main.zip",
|
||||
"js": "https://github.com/langchain-ai/new-langgraphjs-project/archive/refs/heads/main.zip",
|
||||
},
|
||||
"ReAct Agent": {
|
||||
"description": "A simple agent that can be flexibly extended to many tools.",
|
||||
"python": "https://github.com/langchain-ai/react-agent/archive/refs/heads/main.zip",
|
||||
"js": "https://github.com/langchain-ai/react-agent-js/archive/refs/heads/main.zip",
|
||||
},
|
||||
"Memory Agent": {
|
||||
"description": "A ReAct-style agent with an additional tool to store memories for use across conversational threads.",
|
||||
"python": "https://github.com/langchain-ai/memory-agent/archive/refs/heads/main.zip",
|
||||
"js": "https://github.com/langchain-ai/memory-agent-js/archive/refs/heads/main.zip",
|
||||
},
|
||||
"Retrieval Agent": {
|
||||
"description": "An agent that includes a retrieval-based question-answering system.",
|
||||
"python": "https://github.com/langchain-ai/retrieval-agent-template/archive/refs/heads/main.zip",
|
||||
"js": "https://github.com/langchain-ai/retrieval-agent-template-js/archive/refs/heads/main.zip",
|
||||
},
|
||||
"Data-enrichment Agent": {
|
||||
"description": "An agent that performs web searches and organizes its findings into a structured format.",
|
||||
"python": "https://github.com/langchain-ai/data-enrichment/archive/refs/heads/main.zip",
|
||||
"js": "https://github.com/langchain-ai/data-enrichment-js/archive/refs/heads/main.zip",
|
||||
},
|
||||
}
|
||||
|
||||
# Generate TEMPLATE_IDS programmatically
|
||||
TEMPLATE_ID_TO_CONFIG = {
|
||||
f"{name.lower().replace(' ', '-')}-{lang}": (name, lang, url)
|
||||
for name, versions in TEMPLATES.items()
|
||||
for lang, url in versions.items()
|
||||
if lang in {"python", "js"}
|
||||
}
|
||||
|
||||
TEMPLATE_IDS = list(TEMPLATE_ID_TO_CONFIG.keys())
|
||||
|
||||
TEMPLATE_HELP_STRING = (
|
||||
"The name of the template to use. Available options:\n"
|
||||
+ "\n".join(f"{id_}" for id_ in TEMPLATE_ID_TO_CONFIG)
|
||||
)
|
||||
|
||||
|
||||
def _choose_template() -> str:
|
||||
"""Presents a list of templates to the user and prompts them to select one.
|
||||
|
||||
Returns:
|
||||
str: The URL of the selected template.
|
||||
"""
|
||||
click.secho("🌟 Please select a template:", bold=True, fg="yellow")
|
||||
for idx, (template_name, template_info) in enumerate(TEMPLATES.items(), 1):
|
||||
click.secho(f"{idx}. ", nl=False, fg="cyan")
|
||||
click.secho(template_name, fg="cyan", nl=False)
|
||||
click.secho(f" - {template_info['description']}", fg="white")
|
||||
|
||||
# Get the template choice from the user, defaulting to the first template if blank
|
||||
template_choice: Optional[int] = click.prompt(
|
||||
"Enter the number of your template choice (default is 1)",
|
||||
type=int,
|
||||
default=1,
|
||||
show_default=False,
|
||||
)
|
||||
|
||||
template_keys = list(TEMPLATES.keys())
|
||||
if 1 <= template_choice <= len(template_keys):
|
||||
selected_template: str = template_keys[template_choice - 1]
|
||||
else:
|
||||
click.secho("❌ Invalid choice. Please try again.", fg="red")
|
||||
return _choose_template()
|
||||
|
||||
# Prompt the user to choose between Python or JS/TS version
|
||||
click.secho(
|
||||
f"\nYou selected: {selected_template} - {TEMPLATES[selected_template]['description']}",
|
||||
fg="green",
|
||||
)
|
||||
version_choice: int = click.prompt(
|
||||
"Choose language (1 for Python 🐍, 2 for JS/TS 🌐)", type=int
|
||||
)
|
||||
|
||||
if version_choice == 1:
|
||||
return TEMPLATES[selected_template]["python"]
|
||||
elif version_choice == 2:
|
||||
return TEMPLATES[selected_template]["js"]
|
||||
else:
|
||||
click.secho("❌ Invalid choice. Please try again.", fg="red")
|
||||
return _choose_template()
|
||||
|
||||
|
||||
def _download_repo_with_requests(repo_url: str, path: str) -> None:
|
||||
"""Download a ZIP archive from the given URL and extracts it to the specified path.
|
||||
|
||||
Args:
|
||||
repo_url (str): The URL of the repository to download.
|
||||
path (str): The path where the repository should be extracted.
|
||||
"""
|
||||
click.secho("📥 Attempting to download repository as a ZIP archive...", fg="yellow")
|
||||
click.secho(f"URL: {repo_url}", fg="yellow")
|
||||
try:
|
||||
with request.urlopen(repo_url) as response:
|
||||
if response.status == 200:
|
||||
with ZipFile(BytesIO(response.read())) as zip_file:
|
||||
zip_file.extractall(path)
|
||||
# Move extracted contents to path
|
||||
for item in os.listdir(path):
|
||||
if item.endswith("-main"):
|
||||
extracted_dir = os.path.join(path, item)
|
||||
for filename in os.listdir(extracted_dir):
|
||||
shutil.move(os.path.join(extracted_dir, filename), path)
|
||||
shutil.rmtree(extracted_dir)
|
||||
click.secho(
|
||||
f"✅ Downloaded and extracted repository to {path}", fg="green"
|
||||
)
|
||||
except error.HTTPError as e:
|
||||
click.secho(
|
||||
f"❌ Error: Failed to download repository.\n" f"Details: {e}\n",
|
||||
fg="red",
|
||||
bold=True,
|
||||
err=True,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _get_template_url(template_name: str) -> Optional[str]:
|
||||
"""
|
||||
Retrieves the template URL based on the provided template name.
|
||||
|
||||
Args:
|
||||
template_name (str): The name of the template.
|
||||
|
||||
Returns:
|
||||
Optional[str]: The URL of the template if found, else None.
|
||||
"""
|
||||
if template_name in TEMPLATES:
|
||||
click.secho(f"Template selected: {template_name}", fg="green")
|
||||
version_choice: int = click.prompt(
|
||||
"Choose version (1 for Python 🐍, 2 for JS/TS 🌐)", type=int
|
||||
)
|
||||
|
||||
if version_choice == 1:
|
||||
return TEMPLATES[template_name]["python"]
|
||||
elif version_choice == 2:
|
||||
return TEMPLATES[template_name]["js"]
|
||||
else:
|
||||
click.secho("❌ Invalid choice. Please try again.", fg="red")
|
||||
return None
|
||||
else:
|
||||
click.secho(
|
||||
f"Template '{template_name}' not found. Please select from the available options.",
|
||||
fg="red",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def create_new(path: Optional[str], template: Optional[str]) -> None:
|
||||
"""Create a new LangGraph project at the specified PATH using the chosen TEMPLATE.
|
||||
|
||||
Args:
|
||||
path (Optional[str]): The path where the new project will be created.
|
||||
template (Optional[str]): The name of the template to use.
|
||||
"""
|
||||
# Prompt for path if not provided
|
||||
if not path:
|
||||
path = click.prompt(
|
||||
"📂 Please specify the path to create the application", default="."
|
||||
)
|
||||
|
||||
path = os.path.abspath(path) # Ensure path is absolute
|
||||
|
||||
# Check if path exists and is not empty
|
||||
if os.path.exists(path) and os.listdir(path):
|
||||
click.secho(
|
||||
"❌ The specified directory already exists and is not empty. "
|
||||
"Aborting to prevent overwriting files.",
|
||||
fg="red",
|
||||
bold=True,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Get template URL either from command-line argument or
|
||||
# through interactive selection
|
||||
if template:
|
||||
if template not in TEMPLATE_ID_TO_CONFIG:
|
||||
# Format available options in a readable way with descriptions
|
||||
template_options = ""
|
||||
for id_ in TEMPLATE_IDS:
|
||||
name, lang, _ = TEMPLATE_ID_TO_CONFIG[id_]
|
||||
description = TEMPLATES[name]["description"]
|
||||
|
||||
# Add each template option with color formatting
|
||||
template_options += (
|
||||
click.style("- ", fg="yellow", bold=True)
|
||||
+ click.style(f"{id_}", fg="cyan")
|
||||
+ click.style(f": {description}", fg="white")
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
# Display error message with colors and formatting
|
||||
click.secho("❌ Error:", fg="red", bold=True, nl=False)
|
||||
click.secho(f" Template '{template}' not found.", fg="red")
|
||||
click.secho(
|
||||
"Please select from the available options:\n", fg="yellow", bold=True
|
||||
)
|
||||
click.secho(template_options, fg="cyan")
|
||||
sys.exit(1)
|
||||
_, _, template_url = TEMPLATE_ID_TO_CONFIG[template]
|
||||
else:
|
||||
template_url = _choose_template()
|
||||
|
||||
# Download and extract the template
|
||||
_download_repo_with_requests(template_url, path)
|
||||
|
||||
click.secho(f"🎉 New project created at {path}", fg="green", bold=True)
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-cli"
|
||||
version = "0.1.53"
|
||||
version = "0.1.54"
|
||||
description = "CLI for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from langgraph_cli.templates import TEMPLATE_ID_TO_CONFIG
|
||||
|
||||
|
||||
@pytest.mark.parametrize("template_key", TEMPLATE_ID_TO_CONFIG.keys())
|
||||
def test_template_urls_work(template_key: str) -> None:
|
||||
"""Integration test to verify that all template URLs are reachable."""
|
||||
_, _, template_url = TEMPLATE_ID_TO_CONFIG[template_key]
|
||||
response = requests.head(template_url)
|
||||
# Returns 302 on a successful HEAD request
|
||||
assert response.status_code == 302, f"URL {template_url} is not reachable."
|
||||
@@ -0,0 +1,244 @@
|
||||
import json
|
||||
import pathlib
|
||||
import shutil
|
||||
import tempfile
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from langgraph_cli.cli import cli, prepare_args_and_stdin
|
||||
from langgraph_cli.config import Config, validate_config
|
||||
from langgraph_cli.docker import DEFAULT_POSTGRES_URI, DockerCapabilities, Version
|
||||
from langgraph_cli.util import clean_empty_lines
|
||||
|
||||
DEFAULT_DOCKER_CAPABILITIES = DockerCapabilities(
|
||||
version_docker=Version(26, 1, 1),
|
||||
version_compose=Version(2, 27, 0),
|
||||
healthcheck_start_interval=True,
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def temporary_config_folder(config_content: dict):
|
||||
# Create a temporary directory
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
try:
|
||||
# Define the path for the config.json file
|
||||
config_path = Path(temp_dir) / "config.json"
|
||||
|
||||
# Write the provided dictionary content to config.json
|
||||
with open(config_path, "w", encoding="utf-8") as config_file:
|
||||
json.dump(config_content, config_file)
|
||||
|
||||
# Yield the temporary directory path for use within the context
|
||||
yield config_path.parent
|
||||
finally:
|
||||
# Cleanup the temporary directory and its contents
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
|
||||
def test_prepare_args_and_stdin() -> None:
|
||||
# this basically serves as an end-to-end test for using config and docker helpers
|
||||
config_path = pathlib.Path("./langgraph.json")
|
||||
config = validate_config(
|
||||
Config(dependencies=["."], graphs={"agent": "agent.py:graph"})
|
||||
)
|
||||
port = 8000
|
||||
debugger_port = 8001
|
||||
debugger_graph_url = f"http://127.0.0.1:{port}"
|
||||
|
||||
actual_args, actual_stdin = prepare_args_and_stdin(
|
||||
capabilities=DEFAULT_DOCKER_CAPABILITIES,
|
||||
config_path=config_path,
|
||||
config=config,
|
||||
docker_compose=pathlib.Path("custom-docker-compose.yml"),
|
||||
port=port,
|
||||
debugger_port=debugger_port,
|
||||
debugger_base_url=debugger_graph_url,
|
||||
watch=True,
|
||||
)
|
||||
|
||||
expected_args = [
|
||||
"--project-directory",
|
||||
".",
|
||||
"-f",
|
||||
"custom-docker-compose.yml",
|
||||
"-f",
|
||||
"-",
|
||||
]
|
||||
expected_stdin = f"""volumes:
|
||||
langgraph-data:
|
||||
driver: local
|
||||
services:
|
||||
langgraph-redis:
|
||||
image: redis:6
|
||||
healthcheck:
|
||||
test: redis-cli ping
|
||||
interval: 5s
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
langgraph-postgres:
|
||||
image: postgres:16
|
||||
ports:
|
||||
- "5433:5432"
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
volumes:
|
||||
- langgraph-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: pg_isready -U postgres
|
||||
start_period: 10s
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
interval: 60s
|
||||
start_interval: 1s
|
||||
langgraph-debugger:
|
||||
image: langchain/langgraph-debugger
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
langgraph-postgres:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "{debugger_port}:3968"
|
||||
environment:
|
||||
VITE_STUDIO_LOCAL_GRAPH_URL: {debugger_graph_url}
|
||||
langgraph-api:
|
||||
ports:
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
langgraph-redis:
|
||||
condition: service_healthy
|
||||
langgraph-postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
REDIS_URI: redis://langgraph-redis:6379
|
||||
POSTGRES_URI: {DEFAULT_POSTGRES_URI}
|
||||
healthcheck:
|
||||
test: python /api/healthcheck.py
|
||||
interval: 60s
|
||||
start_interval: 1s
|
||||
start_period: 10s
|
||||
|
||||
pull_policy: build
|
||||
build:
|
||||
context: .
|
||||
dockerfile_inline: |
|
||||
FROM langchain/langgraph-api:3.11
|
||||
ADD . /deps/
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}'
|
||||
WORKDIR /deps/
|
||||
|
||||
develop:
|
||||
watch:
|
||||
- path: langgraph.json
|
||||
action: rebuild
|
||||
- path: .
|
||||
action: rebuild\
|
||||
"""
|
||||
assert actual_args == expected_args
|
||||
assert clean_empty_lines(actual_stdin) == expected_stdin
|
||||
|
||||
|
||||
def test_version_option() -> None:
|
||||
"""Test the --version option of the CLI."""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["--version"])
|
||||
|
||||
# Verify that the command executed successfully
|
||||
assert result.exit_code == 0, "Expected exit code 0 for --version option"
|
||||
|
||||
# Check that the output contains the correct version information
|
||||
assert (
|
||||
"LangGraph CLI, version" in result.output
|
||||
), "Expected version information in output"
|
||||
|
||||
|
||||
def test_dockerfile_command_basic() -> None:
|
||||
"""Test the 'dockerfile' command with basic configuration."""
|
||||
runner = CliRunner()
|
||||
config_content = {
|
||||
"node_version": "20", # Add any other necessary configuration fields
|
||||
"graphs": {"agent": "agent.py:graph"},
|
||||
}
|
||||
|
||||
with temporary_config_folder(config_content) as temp_dir:
|
||||
save_path = temp_dir / "Dockerfile"
|
||||
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
["dockerfile", str(save_path), "--config", str(temp_dir / "config.json")],
|
||||
)
|
||||
|
||||
# Assert command was successful
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "✅ Created: Dockerfile" in result.output
|
||||
|
||||
# Check if Dockerfile was created
|
||||
assert save_path.exists()
|
||||
|
||||
|
||||
def test_dockerfile_command_with_docker_compose() -> None:
|
||||
"""Test the 'dockerfile' command with Docker Compose configuration."""
|
||||
runner = CliRunner()
|
||||
config_content = {
|
||||
"dependencies": ["./my_agent"],
|
||||
"graphs": {"agent": "./my_agent/agent.py:graph"},
|
||||
"env": ".env",
|
||||
}
|
||||
with temporary_config_folder(config_content) as temp_dir:
|
||||
save_path = temp_dir / "Dockerfile"
|
||||
# Add agent.py file
|
||||
agent_path = temp_dir / "my_agent" / "agent.py"
|
||||
agent_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
agent_path.touch()
|
||||
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
[
|
||||
"dockerfile",
|
||||
str(save_path),
|
||||
"--config",
|
||||
str(temp_dir / "config.json"),
|
||||
"--add-docker-compose",
|
||||
],
|
||||
)
|
||||
|
||||
# Assert command was successful
|
||||
assert result.exit_code == 0
|
||||
assert "✅ Created: Dockerfile" in result.output
|
||||
assert "✅ Created: .dockerignore" in result.output
|
||||
assert "✅ Created: docker-compose.yml" in result.output
|
||||
assert (
|
||||
"✅ Created: .env" in result.output or "➖ Skipped: .env" in result.output
|
||||
)
|
||||
assert "🎉 Files generated successfully" in result.output
|
||||
|
||||
# Check if Dockerfile, .dockerignore, docker-compose.yml, and .env were created
|
||||
assert save_path.exists()
|
||||
assert (temp_dir / ".dockerignore").exists()
|
||||
assert (temp_dir / "docker-compose.yml").exists()
|
||||
assert (temp_dir / ".env").exists() or "➖ Skipped: .env" in result.output
|
||||
|
||||
|
||||
def test_dockerfile_command_with_bad_config() -> None:
|
||||
"""Test the 'dockerfile' command with basic configuration."""
|
||||
runner = CliRunner()
|
||||
config_content = {
|
||||
"node_version": "20" # Add any other necessary configuration fields
|
||||
}
|
||||
|
||||
with temporary_config_folder(config_content) as temp_dir:
|
||||
save_path = temp_dir / "Dockerfile"
|
||||
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
["dockerfile", str(save_path), "--config", str(temp_dir / "conf.json")],
|
||||
)
|
||||
|
||||
# Assert command was successful
|
||||
assert result.exit_code == 2
|
||||
assert "conf.json' does not exist" in result.output
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Unit tests for the 'new' CLI command.
|
||||
|
||||
This command creates a new LangGraph project using a specified template.
|
||||
"""
|
||||
|
||||
import os
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from unittest.mock import MagicMock, patch
|
||||
from urllib import request
|
||||
from zipfile import ZipFile
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
from langgraph_cli.cli import cli
|
||||
from langgraph_cli.templates import TEMPLATE_ID_TO_CONFIG
|
||||
|
||||
|
||||
@patch.object(request, "urlopen")
|
||||
def test_create_new_with_mocked_download(mock_urlopen: MagicMock) -> None:
|
||||
"""Test the 'new' CLI command with a mocked download response using urllib."""
|
||||
# Mock the response content to simulate a ZIP file
|
||||
mock_zip_content = BytesIO()
|
||||
with ZipFile(mock_zip_content, "w") as mock_zip:
|
||||
mock_zip.writestr("test-file.txt", "Test content.")
|
||||
|
||||
# Create a mock response that behaves like a context manager
|
||||
mock_response = MagicMock()
|
||||
mock_response.read.return_value = mock_zip_content.getvalue()
|
||||
mock_response.__enter__.return_value = mock_response # Setup enter context
|
||||
mock_response.status = 200
|
||||
|
||||
mock_urlopen.return_value = mock_response
|
||||
|
||||
with TemporaryDirectory() as temp_dir:
|
||||
runner = CliRunner()
|
||||
template = next(
|
||||
iter(TEMPLATE_ID_TO_CONFIG)
|
||||
) # Select the first template for the test
|
||||
result = runner.invoke(cli, ["new", temp_dir, "--template", template])
|
||||
|
||||
# Verify CLI command execution and success
|
||||
assert result.exit_code == 0, result.output
|
||||
assert (
|
||||
"New project created" in result.output
|
||||
), "Expected success message in output."
|
||||
|
||||
# Verify that the directory is not empty
|
||||
assert os.listdir(temp_dir), "Expected files to be created in temp directory."
|
||||
|
||||
# Check for a known file in the extracted content
|
||||
extracted_files = [f.name for f in Path(temp_dir).glob("*")]
|
||||
assert (
|
||||
"test-file.txt" in extracted_files
|
||||
), "Expected 'test-file.txt' in the extracted content."
|
||||
|
||||
|
||||
def test_invalid_template_id() -> None:
|
||||
"""Test that an invalid template ID passed via CLI results in a graceful error."""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli, ["new", "dummy_path", "--template", "invalid-template-id"]
|
||||
)
|
||||
|
||||
# Verify the command failed and proper message is displayed
|
||||
assert result.exit_code != 0, "Expected non-zero exit code for invalid template."
|
||||
assert (
|
||||
"Template 'invalid-template-id' not found" in result.output
|
||||
), "Expected error message in output."
|
||||
@@ -0,0 +1,16 @@
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def disable_analytics_env() -> None:
|
||||
"""Disable analytics for unit tests LANGGRAPH_CLI_NO_ANALYTICS."""
|
||||
# First check if the environment variable is already set, if so, log a warning prior
|
||||
# to overriding it.
|
||||
if "LANGGRAPH_CLI_NO_ANALYTICS" in os.environ:
|
||||
print("⚠️ LANGGRAPH_CLI_NO_ANALYTICS is set. Overriding it for the test.")
|
||||
|
||||
with patch.dict(os.environ, {"LANGGRAPH_CLI_NO_ANALYTICS": "0"}):
|
||||
yield
|
||||
@@ -1,117 +0,0 @@
|
||||
import pathlib
|
||||
|
||||
from langgraph_cli.cli import prepare_args_and_stdin
|
||||
from langgraph_cli.config import Config, validate_config
|
||||
from langgraph_cli.docker import DEFAULT_POSTGRES_URI, DockerCapabilities, Version
|
||||
from langgraph_cli.util import clean_empty_lines
|
||||
|
||||
DEFAULT_DOCKER_CAPABILITIES = DockerCapabilities(
|
||||
version_docker=Version(26, 1, 1),
|
||||
version_compose=Version(2, 27, 0),
|
||||
healthcheck_start_interval=True,
|
||||
)
|
||||
|
||||
|
||||
def test_prepare_args_and_stdin():
|
||||
# this basically serves as an end-to-end test for using config and docker helpers
|
||||
config_path = pathlib.Path("./langgraph.json")
|
||||
config = validate_config(
|
||||
Config(dependencies=["."], graphs={"agent": "agent.py:graph"})
|
||||
)
|
||||
port = 8000
|
||||
debugger_port = 8001
|
||||
debugger_graph_url = f"http://127.0.0.1:{port}"
|
||||
|
||||
actual_args, actual_stdin = prepare_args_and_stdin(
|
||||
capabilities=DEFAULT_DOCKER_CAPABILITIES,
|
||||
config_path=config_path,
|
||||
config=config,
|
||||
docker_compose="custom-docker-compose.yml",
|
||||
port=port,
|
||||
debugger_port=debugger_port,
|
||||
debugger_base_url=debugger_graph_url,
|
||||
watch=True,
|
||||
)
|
||||
|
||||
expected_args = [
|
||||
"--project-directory",
|
||||
".",
|
||||
"-f",
|
||||
"custom-docker-compose.yml",
|
||||
"-f",
|
||||
"-",
|
||||
]
|
||||
expected_stdin = f"""volumes:
|
||||
langgraph-data:
|
||||
driver: local
|
||||
services:
|
||||
langgraph-redis:
|
||||
image: redis:6
|
||||
healthcheck:
|
||||
test: redis-cli ping
|
||||
interval: 5s
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
langgraph-postgres:
|
||||
image: postgres:16
|
||||
ports:
|
||||
- "5433:5432"
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
volumes:
|
||||
- langgraph-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: pg_isready -U postgres
|
||||
start_period: 10s
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
interval: 60s
|
||||
start_interval: 1s
|
||||
langgraph-debugger:
|
||||
image: langchain/langgraph-debugger
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
langgraph-postgres:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "{debugger_port}:3968"
|
||||
environment:
|
||||
VITE_STUDIO_LOCAL_GRAPH_URL: {debugger_graph_url}
|
||||
langgraph-api:
|
||||
ports:
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
langgraph-redis:
|
||||
condition: service_healthy
|
||||
langgraph-postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
REDIS_URI: redis://langgraph-redis:6379
|
||||
POSTGRES_URI: {DEFAULT_POSTGRES_URI}
|
||||
healthcheck:
|
||||
test: python /api/healthcheck.py
|
||||
interval: 60s
|
||||
start_interval: 1s
|
||||
start_period: 10s
|
||||
|
||||
pull_policy: build
|
||||
build:
|
||||
context: .
|
||||
dockerfile_inline: |
|
||||
FROM langchain/langgraph-api:3.11
|
||||
ADD . /deps/
|
||||
RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
|
||||
ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}'
|
||||
WORKDIR /deps/
|
||||
|
||||
develop:
|
||||
watch:
|
||||
- path: langgraph.json
|
||||
action: rebuild
|
||||
- path: .
|
||||
action: rebuild\
|
||||
"""
|
||||
assert actual_args == expected_args
|
||||
assert clean_empty_lines(actual_stdin) == expected_stdin
|
||||
@@ -250,9 +250,10 @@ def test_config_to_docker_nodejs():
|
||||
ARG meow
|
||||
ARG foo
|
||||
ADD . /deps/unit_tests
|
||||
RUN cd /deps/unit_tests && yarn install --frozen-lockfile
|
||||
RUN cd /deps/unit_tests && npm i
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}'
|
||||
WORKDIR /deps/unit_tests"""
|
||||
WORKDIR /deps/unit_tests
|
||||
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts"""
|
||||
|
||||
assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ test:
|
||||
exit $$EXIT_CODE
|
||||
|
||||
test_watch:
|
||||
make start-postgres && poetry run ptw . -- --ff -v -x -n auto --dist worksteal --snapshot-update --tb short $(TEST); \
|
||||
make start-postgres && poetry run ptw . -- --ff -vv -x -n auto --dist worksteal --snapshot-update --tb short $(TEST); \
|
||||
EXIT_CODE=$$?; \
|
||||
make stop-postgres; \
|
||||
exit $$EXIT_CODE
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import sys
|
||||
from os import getenv
|
||||
from types import MappingProxyType
|
||||
from typing import Any, Literal, Mapping, cast
|
||||
|
||||
@@ -10,6 +11,7 @@ from langgraph.types import Interrupt, Send # noqa: F401
|
||||
# --- Empty read-only containers ---
|
||||
EMPTY_MAP: Mapping[str, Any] = MappingProxyType({})
|
||||
EMPTY_SEQ: tuple[str, ...] = tuple()
|
||||
MISSING = object()
|
||||
|
||||
# --- Public constants ---
|
||||
TAG_NOSTREAM = sys.intern("langsmith:nostream")
|
||||
@@ -20,12 +22,16 @@ START = sys.intern("__start__")
|
||||
"""The first (maybe virtual) node in graph-style Pregel."""
|
||||
END = sys.intern("__end__")
|
||||
"""The last (maybe virtual) node in graph-style Pregel."""
|
||||
SELF = sys.intern("__self__")
|
||||
"""The implicit branch that handles each node's Control values."""
|
||||
|
||||
# --- Reserved write keys ---
|
||||
INPUT = sys.intern("__input__")
|
||||
# for values passed as input to the graph
|
||||
INTERRUPT = sys.intern("__interrupt__")
|
||||
# for dynamic interrupts raised by nodes
|
||||
RESUME = sys.intern("__resume__")
|
||||
# for values passed to resume a node after an interrupt
|
||||
ERROR = sys.intern("__error__")
|
||||
# for errors raised by nodes
|
||||
NO_WRITES = sys.intern("__no_writes__")
|
||||
@@ -67,6 +73,8 @@ CONFIG_KEY_CHECKPOINT_NS = sys.intern("checkpoint_ns")
|
||||
# holds the current checkpoint_ns, "" for root graph
|
||||
CONFIG_KEY_NODE_FINISHED = sys.intern("__pregel_node_finished")
|
||||
# callback to be called when a node is finished
|
||||
CONFIG_KEY_RESUME_VALUE = sys.intern("__pregel_resume_value")
|
||||
# holds the value that "answers" an interrupt() call
|
||||
|
||||
# --- Other constants ---
|
||||
PUSH = sys.intern("__pregel_push")
|
||||
@@ -79,12 +87,17 @@ NS_END = sys.intern(":")
|
||||
# for checkpoint_ns, for each level, separates the namespace from the task_id
|
||||
CONF = cast(Literal["configurable"], sys.intern("configurable"))
|
||||
# key for the configurable dict in RunnableConfig
|
||||
FF_SEND_V2 = getenv("LANGGRAPH_FF_SEND_V2", "false").lower() == "true"
|
||||
# temporary flag to enable new Send semantics
|
||||
NULL_TASK_ID = sys.intern("00000000-0000-0000-0000-000000000000")
|
||||
# the task_id to use for writes that are not associated with a task
|
||||
|
||||
RESERVED = {
|
||||
TAG_HIDDEN,
|
||||
# reserved write keys
|
||||
INPUT,
|
||||
INTERRUPT,
|
||||
RESUME,
|
||||
ERROR,
|
||||
NO_WRITES,
|
||||
SCHEDULED,
|
||||
|
||||
@@ -70,7 +70,7 @@ class NodeInterrupt(GraphInterrupt):
|
||||
"""Raised by a node to interrupt execution."""
|
||||
|
||||
def __init__(self, value: Any) -> None:
|
||||
super().__init__([Interrupt(value)])
|
||||
super().__init__([Interrupt(value=value)])
|
||||
|
||||
|
||||
class GraphDelegate(Exception):
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from langgraph.graph.graph import END, START, Graph
|
||||
from langgraph.graph.message import MessageGraph, MessagesState, add_messages
|
||||
from langgraph.graph.state import StateGraph
|
||||
from langgraph.graph.state import GraphCommand, StateGraph
|
||||
|
||||
__all__ = [
|
||||
"END",
|
||||
"START",
|
||||
"Graph",
|
||||
"StateGraph",
|
||||
"GraphCommand",
|
||||
"MessageGraph",
|
||||
"add_messages",
|
||||
"MessagesState",
|
||||
|
||||
@@ -27,6 +27,7 @@ from typing_extensions import Self
|
||||
|
||||
from langgraph.channels.ephemeral_value import EphemeralValue
|
||||
from langgraph.constants import (
|
||||
EMPTY_SEQ,
|
||||
END,
|
||||
NS_END,
|
||||
NS_SEP,
|
||||
@@ -47,6 +48,7 @@ logger = logging.getLogger(__name__)
|
||||
class NodeSpec(NamedTuple):
|
||||
runnable: Runnable
|
||||
metadata: Optional[dict[str, Any]] = None
|
||||
ends: Optional[tuple[str, ...]] = EMPTY_SEQ
|
||||
|
||||
|
||||
class Branch(NamedTuple):
|
||||
@@ -123,7 +125,7 @@ class Branch(NamedTuple):
|
||||
result: Any,
|
||||
config: RunnableConfig,
|
||||
) -> Union[Runnable, Any]:
|
||||
if not isinstance(result, list):
|
||||
if not isinstance(result, (list, tuple)):
|
||||
result = [result]
|
||||
if self.ends:
|
||||
destinations: Sequence[Union[Send, str]] = [
|
||||
@@ -364,6 +366,9 @@ class Graph:
|
||||
for node in self.nodes:
|
||||
if node != start and node != branch.then:
|
||||
all_sources.add(node)
|
||||
for name, spec in self.nodes.items():
|
||||
if spec.ends:
|
||||
all_sources.add(name)
|
||||
# validate sources
|
||||
for source in all_sources:
|
||||
if source not in self.nodes and source != START:
|
||||
@@ -387,6 +392,9 @@ class Graph:
|
||||
for node in self.nodes:
|
||||
if node != start and node != branch.then:
|
||||
all_targets.add(node)
|
||||
for name, spec in self.nodes.items():
|
||||
if spec.ends:
|
||||
all_targets.update(spec.ends)
|
||||
# validate targets
|
||||
for node in self.nodes:
|
||||
if node not in all_targets:
|
||||
@@ -565,18 +573,12 @@ class CompiledGraph(Pregel):
|
||||
metadata["__interrupt"] = "before"
|
||||
elif key in self.interrupt_after_nodes:
|
||||
metadata["__interrupt"] = "after"
|
||||
if xray:
|
||||
subgraph = (
|
||||
subgraphs[key].get_graph(
|
||||
config=config,
|
||||
xray=xray - 1
|
||||
if isinstance(xray, int)
|
||||
and not isinstance(xray, bool)
|
||||
and xray > 0
|
||||
else xray,
|
||||
)
|
||||
if key in subgraphs
|
||||
else node.get_graph(config=config)
|
||||
if xray and key in subgraphs:
|
||||
subgraph = subgraphs[key].get_graph(
|
||||
config=config,
|
||||
xray=xray - 1
|
||||
if isinstance(xray, int) and not isinstance(xray, bool) and xray > 0
|
||||
else xray,
|
||||
)
|
||||
subgraph.trim_first_node()
|
||||
subgraph.trim_last_node()
|
||||
@@ -620,5 +622,9 @@ class CompiledGraph(Pregel):
|
||||
)
|
||||
if branch.then is not None:
|
||||
add_edge(end, branch.then)
|
||||
for key, n in self.builder.nodes.items():
|
||||
if n.ends:
|
||||
for end in n.ends:
|
||||
add_edge(key, end, conditional=True)
|
||||
|
||||
return graph
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import dataclasses
|
||||
import inspect
|
||||
import logging
|
||||
import typing
|
||||
import warnings
|
||||
from functools import partial
|
||||
from inspect import isclass, isfunction, ismethod, signature
|
||||
from types import FunctionType
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Generic,
|
||||
Literal,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
@@ -14,6 +17,7 @@ from typing import (
|
||||
Type,
|
||||
Union,
|
||||
cast,
|
||||
get_args,
|
||||
get_origin,
|
||||
get_type_hints,
|
||||
overload,
|
||||
@@ -32,7 +36,7 @@ from langgraph.channels.dynamic_barrier_value import DynamicBarrierValue, WaitFo
|
||||
from langgraph.channels.ephemeral_value import EphemeralValue
|
||||
from langgraph.channels.last_value import LastValue
|
||||
from langgraph.channels.named_barrier_value import NamedBarrierValue
|
||||
from langgraph.constants import NS_END, NS_SEP, TAG_HIDDEN
|
||||
from langgraph.constants import EMPTY_SEQ, NS_END, NS_SEP, SELF, TAG_HIDDEN
|
||||
from langgraph.errors import ErrorCode, InvalidUpdateError, create_error_message
|
||||
from langgraph.graph.graph import END, START, Branch, CompiledGraph, Graph, Send
|
||||
from langgraph.managed.base import (
|
||||
@@ -46,10 +50,10 @@ from langgraph.managed.base import (
|
||||
from langgraph.pregel.read import ChannelRead, PregelNode
|
||||
from langgraph.pregel.write import SKIP_WRITE, ChannelWrite, ChannelWriteEntry
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import All, Checkpointer, RetryPolicy
|
||||
from langgraph.types import _DC_KWARGS, All, Checkpointer, Command, N, RetryPolicy
|
||||
from langgraph.utils.fields import get_field_default
|
||||
from langgraph.utils.pydantic import create_model
|
||||
from langgraph.utils.runnable import coerce_to_runnable
|
||||
from langgraph.utils.runnable import RunnableCallable, coerce_to_runnable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -66,11 +70,37 @@ def _warn_invalid_state_schema(schema: Union[Type[Any], Any]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _get_node_name(node: RunnableLike) -> str:
|
||||
if isinstance(node, Runnable):
|
||||
return node.get_name()
|
||||
elif callable(node):
|
||||
return getattr(node, "__name__", node.__class__.__name__)
|
||||
else:
|
||||
raise TypeError(f"Unsupported node type: {type(node)}")
|
||||
|
||||
|
||||
@dataclasses.dataclass(**_DC_KWARGS)
|
||||
class GraphCommand(Generic[N], Command[N]):
|
||||
"""One or more commands to update a StateGraph's state and go to, or send messages to nodes."""
|
||||
|
||||
goto: Union[str, Sequence[str]] = ()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
# get all non-None values
|
||||
contents = ", ".join(
|
||||
f"{key}={value!r}"
|
||||
for key, value in dataclasses.asdict(self).items()
|
||||
if value
|
||||
)
|
||||
return f"Command({contents})"
|
||||
|
||||
|
||||
class StateNodeSpec(NamedTuple):
|
||||
runnable: Runnable
|
||||
metadata: Optional[dict[str, Any]]
|
||||
input: Type[Any]
|
||||
retry_policy: Optional[RetryPolicy]
|
||||
ends: Optional[tuple[str, ...]] = EMPTY_SEQ
|
||||
|
||||
|
||||
class StateGraph(Graph):
|
||||
@@ -222,7 +252,7 @@ class StateGraph(Graph):
|
||||
ValueError: If the key is already being used as a state key.
|
||||
|
||||
Returns:
|
||||
None
|
||||
StateGraph
|
||||
"""
|
||||
...
|
||||
|
||||
@@ -246,7 +276,7 @@ class StateGraph(Graph):
|
||||
ValueError: If the key is already being used as a state key.
|
||||
|
||||
Returns:
|
||||
None
|
||||
StateGraph
|
||||
"""
|
||||
...
|
||||
|
||||
@@ -299,7 +329,7 @@ class StateGraph(Graph):
|
||||
```
|
||||
|
||||
Returns:
|
||||
None
|
||||
StateGraph
|
||||
"""
|
||||
if not isinstance(node, str):
|
||||
action = node
|
||||
@@ -338,8 +368,33 @@ class StateGraph(Graph):
|
||||
f"'{character}' is a reserved character and is not allowed in the node names."
|
||||
)
|
||||
|
||||
if input is None:
|
||||
input = _get_input_schema_from_type_hint(action)
|
||||
ends = EMPTY_SEQ
|
||||
try:
|
||||
if (isfunction(action) or ismethod(getattr(action, "__call__", None))) and (
|
||||
hints := get_type_hints(getattr(action, "__call__"))
|
||||
or get_type_hints(action)
|
||||
):
|
||||
if input is None:
|
||||
first_parameter_name = next(
|
||||
iter(
|
||||
inspect.signature(
|
||||
cast(FunctionType, action)
|
||||
).parameters.keys()
|
||||
)
|
||||
)
|
||||
if input_hint := hints.get(first_parameter_name):
|
||||
if isinstance(input_hint, type) and get_type_hints(input_hint):
|
||||
input = input_hint
|
||||
if (
|
||||
(rtn := hints.get("return"))
|
||||
and get_origin(rtn) in (Command, GraphCommand)
|
||||
and (rargs := get_args(rtn))
|
||||
and get_origin(rargs[0]) is Literal
|
||||
and (vals := get_args(rargs[0]))
|
||||
):
|
||||
ends = vals
|
||||
except (TypeError, StopIteration):
|
||||
pass
|
||||
if input is not None:
|
||||
self._add_schema(input)
|
||||
self.nodes[cast(str, node)] = StateNodeSpec(
|
||||
@@ -347,6 +402,7 @@ class StateGraph(Graph):
|
||||
metadata,
|
||||
input=input or self.schema,
|
||||
retry_policy=retry,
|
||||
ends=ends,
|
||||
)
|
||||
return self
|
||||
|
||||
@@ -363,7 +419,7 @@ class StateGraph(Graph):
|
||||
ValueError: If the start key is 'END' or if the start key or end key is not present in the graph.
|
||||
|
||||
Returns:
|
||||
None
|
||||
StateGraph
|
||||
"""
|
||||
if isinstance(start_key, str):
|
||||
return super().add_edge(start_key, end_key)
|
||||
@@ -386,6 +442,48 @@ class StateGraph(Graph):
|
||||
self.waiting_edges.add((tuple(start_key), end_key))
|
||||
return self
|
||||
|
||||
def add_sequence(
|
||||
self,
|
||||
nodes: Sequence[Union[RunnableLike, tuple[str, RunnableLike]]],
|
||||
) -> Self:
|
||||
"""Add a sequence of nodes that will be executed in the provided order.
|
||||
|
||||
Args:
|
||||
nodes: A sequence of RunnableLike objects (e.g. a LangChain Runnable or a callable) or (name, RunnableLike) tuples.
|
||||
If no names are provided, the name will be inferred from the node object (e.g. a runnable or a callable name).
|
||||
Each node will be executed in the order provided.
|
||||
|
||||
Raises:
|
||||
ValueError: if the sequence is empty.
|
||||
ValueError: if the sequence contains duplicate node names.
|
||||
|
||||
Returns:
|
||||
StateGraph
|
||||
"""
|
||||
if len(nodes) < 1:
|
||||
raise ValueError("Sequence requires at least one node.")
|
||||
|
||||
previous_name: Optional[str] = None
|
||||
for node in nodes:
|
||||
if isinstance(node, tuple) and len(node) == 2:
|
||||
name, node = node
|
||||
else:
|
||||
name = _get_node_name(node)
|
||||
|
||||
if name in self.nodes:
|
||||
raise ValueError(
|
||||
f"Node names must be unique: node with the name '{name}' already exists. "
|
||||
"If you need to use two different runnables/callables with the same name (for example, using `lambda`), please provide them as tuples (name, runnable/callable)."
|
||||
)
|
||||
|
||||
self.add_node(name, node)
|
||||
if previous_name is not None:
|
||||
self.add_edge(previous_name, name)
|
||||
|
||||
previous_name = name
|
||||
|
||||
return self
|
||||
|
||||
def compile(
|
||||
self,
|
||||
checkpointer: Checkpointer = None,
|
||||
@@ -401,9 +499,11 @@ class StateGraph(Graph):
|
||||
streamed, batched, and run asynchronously.
|
||||
|
||||
Args:
|
||||
checkpointer (Checkpointer): An optional checkpoint saver object.
|
||||
This serves as a fully versioned "memory" for the graph, allowing
|
||||
the graph to be paused and resumed, and replayed from any point.
|
||||
checkpointer (Optional[Union[Checkpointer, Literal[False]]]): A checkpoint saver object or flag.
|
||||
If provided, this Checkpointer serves as a fully versioned "short-term memory" for the graph,
|
||||
allowing it to be paused, resumed, and replayed from any point.
|
||||
If None, it may inherit the parent graph's checkpointer when used as a subgraph.
|
||||
If False, it will not use or inherit any checkpointer.
|
||||
interrupt_before (Optional[Sequence[str]]): An optional list of node names to interrupt before.
|
||||
interrupt_after (Optional[Sequence[str]]): An optional list of node names to interrupt after.
|
||||
debug (bool): A flag indicating whether to enable debug mode.
|
||||
@@ -468,6 +568,9 @@ class StateGraph(Graph):
|
||||
for key, node in self.nodes.items():
|
||||
compiled.attach_node(key, node)
|
||||
|
||||
for key, node in self.nodes.items():
|
||||
compiled.attach_branch(key, SELF, CONTROL_BRANCH, with_reader=False)
|
||||
|
||||
for start, end in self.edges:
|
||||
compiled.attach_edge(start, end)
|
||||
|
||||
@@ -518,11 +621,23 @@ class CompiledStateGraph(CompiledGraph):
|
||||
if is_writable_managed_value(v)
|
||||
]
|
||||
|
||||
def _get_root(input: Any) -> Any:
|
||||
if isinstance(input, Command):
|
||||
return input.update
|
||||
else:
|
||||
return input
|
||||
|
||||
def _get_state_key(input: Union[None, dict, Any], *, key: str) -> Any:
|
||||
if input is None:
|
||||
return SKIP_WRITE
|
||||
elif isinstance(input, dict):
|
||||
if all(k not in output_keys for k in input):
|
||||
raise InvalidUpdateError(
|
||||
f"Expected node {key} to update at least one of {output_keys}, got {input}"
|
||||
)
|
||||
return input.get(key, SKIP_WRITE)
|
||||
elif isinstance(input, Command):
|
||||
return _get_state_key(input.update, key=key)
|
||||
elif get_type_hints(type(input)):
|
||||
value = getattr(input, key, SKIP_WRITE)
|
||||
return value if value is not None else SKIP_WRITE
|
||||
@@ -535,7 +650,7 @@ class CompiledStateGraph(CompiledGraph):
|
||||
|
||||
# state updaters
|
||||
write_entries = (
|
||||
[ChannelWriteEntry("__root__", skip_none=True)]
|
||||
[ChannelWriteEntry("__root__", skip_none=True, mapper=_get_root)]
|
||||
if output_keys == ["__root__"]
|
||||
else [
|
||||
ChannelWriteEntry(key, mapper=partial(_get_state_key, key=key))
|
||||
@@ -578,7 +693,6 @@ class CompiledStateGraph(CompiledGraph):
|
||||
ChannelWrite(
|
||||
[ChannelWriteEntry(key, key)] + write_entries,
|
||||
tags=[TAG_HIDDEN],
|
||||
require_at_least_one_of=output_keys,
|
||||
),
|
||||
],
|
||||
metadata=node.metadata,
|
||||
@@ -615,7 +729,9 @@ class CompiledStateGraph(CompiledGraph):
|
||||
[ChannelWriteEntry(channel_name, start)], tags=[TAG_HIDDEN]
|
||||
)
|
||||
|
||||
def attach_branch(self, start: str, name: str, branch: Branch) -> None:
|
||||
def attach_branch(
|
||||
self, start: str, name: str, branch: Branch, *, with_reader: bool = True
|
||||
) -> None:
|
||||
def branch_writer(
|
||||
packets: Sequence[Union[str, Send]], config: RunnableConfig
|
||||
) -> None:
|
||||
@@ -648,7 +764,8 @@ class CompiledStateGraph(CompiledGraph):
|
||||
else self.builder.schema
|
||||
)
|
||||
self.nodes[start] |= branch.run(
|
||||
branch_writer, _get_state_reader(self.builder, schema)
|
||||
branch_writer,
|
||||
_get_state_reader(self.builder, schema) if with_reader else None,
|
||||
)
|
||||
|
||||
# attach branch subscribers
|
||||
@@ -697,6 +814,46 @@ def _coerce_state(schema: Type[Any], input: dict[str, Any]) -> dict[str, Any]:
|
||||
return schema(**input)
|
||||
|
||||
|
||||
def _control_branch(value: Any) -> Sequence[Union[str, Send]]:
|
||||
if isinstance(value, Send):
|
||||
return [value]
|
||||
if not isinstance(value, GraphCommand):
|
||||
return EMPTY_SEQ
|
||||
rtn: list[Union[str, Send]] = []
|
||||
if isinstance(value.goto, str):
|
||||
rtn.append(value.goto)
|
||||
else:
|
||||
rtn.extend(value.goto)
|
||||
if isinstance(value.send, Send):
|
||||
rtn.append(value.send)
|
||||
else:
|
||||
rtn.extend(value.send)
|
||||
return rtn
|
||||
|
||||
|
||||
async def _acontrol_branch(value: Any) -> Sequence[Union[str, Send]]:
|
||||
if isinstance(value, Send):
|
||||
return [value]
|
||||
if not isinstance(value, GraphCommand):
|
||||
return EMPTY_SEQ
|
||||
rtn: list[Union[str, Send]] = []
|
||||
if isinstance(value.goto, str):
|
||||
rtn.append(value.goto)
|
||||
else:
|
||||
rtn.extend(value.goto)
|
||||
if isinstance(value.send, Send):
|
||||
rtn.append(value.send)
|
||||
else:
|
||||
rtn.extend(value.send)
|
||||
return rtn
|
||||
|
||||
|
||||
CONTROL_BRANCH_PATH = RunnableCallable(
|
||||
_control_branch, _acontrol_branch, tags=[TAG_HIDDEN], trace=False, recurse=False
|
||||
)
|
||||
CONTROL_BRANCH = Branch(CONTROL_BRANCH_PATH, None)
|
||||
|
||||
|
||||
def _get_channels(
|
||||
schema: Type[dict],
|
||||
) -> tuple[dict[str, BaseChannel], dict[str, ManagedValueSpec]]:
|
||||
@@ -823,21 +980,3 @@ def _get_schema(
|
||||
if k in channels and isinstance(channels[k], BaseChannel)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _get_input_schema_from_type_hint(
|
||||
action: Optional[RunnableLike],
|
||||
) -> Optional[Type[Any]]:
|
||||
if not isfunction(action) and not ismethod(getattr(action, "__call__", None)):
|
||||
return None
|
||||
action = cast(Callable, action)
|
||||
|
||||
try:
|
||||
hints = get_type_hints(getattr(action, "__call__")) or get_type_hints(action)
|
||||
first_parameter_name = next(iter(inspect.signature(action).parameters.keys()))
|
||||
input_hint = hints.get(first_parameter_name)
|
||||
if isinstance(input_hint, type) and get_type_hints(input_hint):
|
||||
return input_hint
|
||||
except (TypeError, StopIteration):
|
||||
pass
|
||||
return None
|
||||
|
||||
@@ -54,6 +54,7 @@ from langgraph.checkpoint.base import (
|
||||
)
|
||||
from langgraph.constants import (
|
||||
CONF,
|
||||
CONFIG_KEY_CHECKPOINT_ID,
|
||||
CONFIG_KEY_CHECKPOINT_NS,
|
||||
CONFIG_KEY_CHECKPOINTER,
|
||||
CONFIG_KEY_NODE_FINISHED,
|
||||
@@ -64,9 +65,15 @@ from langgraph.constants import (
|
||||
CONFIG_KEY_STREAM,
|
||||
CONFIG_KEY_STREAM_WRITER,
|
||||
CONFIG_KEY_TASK_ID,
|
||||
END,
|
||||
ERROR,
|
||||
INPUT,
|
||||
INTERRUPT,
|
||||
NS_END,
|
||||
NS_SEP,
|
||||
NULL_TASK_ID,
|
||||
PUSH,
|
||||
SCHEDULED,
|
||||
)
|
||||
from langgraph.errors import (
|
||||
ErrorCode,
|
||||
@@ -95,7 +102,13 @@ from langgraph.pregel.utils import find_subgraph_pregel, get_new_channel_version
|
||||
from langgraph.pregel.validate import validate_graph, validate_keys
|
||||
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import All, Checkpointer, LoopProtocol, StateSnapshot, StreamMode
|
||||
from langgraph.types import (
|
||||
All,
|
||||
Checkpointer,
|
||||
LoopProtocol,
|
||||
StateSnapshot,
|
||||
StreamMode,
|
||||
)
|
||||
from langgraph.utils.config import (
|
||||
ensure_config,
|
||||
merge_configs,
|
||||
@@ -439,6 +452,7 @@ class Pregel(PregelProtocol):
|
||||
config: RunnableConfig,
|
||||
saved: Optional[CheckpointTuple],
|
||||
recurse: Optional[BaseCheckpointSaver] = None,
|
||||
apply_pending_writes: bool = False,
|
||||
) -> StateSnapshot:
|
||||
if not saved:
|
||||
return StateSnapshot(
|
||||
@@ -464,12 +478,16 @@ class Pregel(PregelProtocol):
|
||||
# tasks for this checkpoint
|
||||
next_tasks = prepare_next_tasks(
|
||||
saved.checkpoint,
|
||||
saved.pending_writes or [],
|
||||
self.nodes,
|
||||
channels,
|
||||
managed,
|
||||
saved.config,
|
||||
saved.metadata.get("step", -1) + 1,
|
||||
for_execution=False,
|
||||
for_execution=True,
|
||||
store=self.store,
|
||||
checkpointer=self.checkpointer or None,
|
||||
manager=None,
|
||||
)
|
||||
# get the subgraphs
|
||||
subgraphs = dict(self.get_subgraphs())
|
||||
@@ -503,10 +521,29 @@ class Pregel(PregelProtocol):
|
||||
task_states[task.id] = subgraphs[task.name].get_state(
|
||||
config, subgraphs=True
|
||||
)
|
||||
# apply pending writes
|
||||
if null_writes := [
|
||||
w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID
|
||||
]:
|
||||
apply_writes(
|
||||
saved.checkpoint,
|
||||
channels,
|
||||
[PregelTaskWrites((), INPUT, null_writes, [])],
|
||||
None,
|
||||
)
|
||||
if apply_pending_writes and saved.pending_writes:
|
||||
for tid, k, v in saved.pending_writes:
|
||||
if k in (ERROR, INTERRUPT, SCHEDULED):
|
||||
continue
|
||||
if tid not in next_tasks:
|
||||
continue
|
||||
next_tasks[tid].writes.append((k, v))
|
||||
if tasks := [t for t in next_tasks.values() if t.writes]:
|
||||
apply_writes(saved.checkpoint, channels, tasks, None)
|
||||
# assemble the state snapshot
|
||||
return StateSnapshot(
|
||||
read_channels(channels, self.stream_channels_asis),
|
||||
tuple(t.name for t in next_tasks.values()),
|
||||
tuple(t.name for t in next_tasks.values() if not t.writes),
|
||||
patch_checkpoint_map(saved.config, saved.metadata),
|
||||
saved.metadata,
|
||||
saved.checkpoint["ts"],
|
||||
@@ -524,6 +561,7 @@ class Pregel(PregelProtocol):
|
||||
config: RunnableConfig,
|
||||
saved: Optional[CheckpointTuple],
|
||||
recurse: Optional[BaseCheckpointSaver] = None,
|
||||
apply_pending_writes: bool = False,
|
||||
) -> StateSnapshot:
|
||||
if not saved:
|
||||
return StateSnapshot(
|
||||
@@ -552,12 +590,16 @@ class Pregel(PregelProtocol):
|
||||
# tasks for this checkpoint
|
||||
next_tasks = prepare_next_tasks(
|
||||
saved.checkpoint,
|
||||
saved.pending_writes or [],
|
||||
self.nodes,
|
||||
channels,
|
||||
managed,
|
||||
saved.config,
|
||||
saved.metadata.get("step", -1) + 1,
|
||||
for_execution=False,
|
||||
for_execution=True,
|
||||
store=self.store,
|
||||
checkpointer=self.checkpointer or None,
|
||||
manager=None,
|
||||
)
|
||||
# get the subgraphs
|
||||
subgraphs = {n: g async for n, g in self.aget_subgraphs()}
|
||||
@@ -591,10 +633,29 @@ class Pregel(PregelProtocol):
|
||||
task_states[task.id] = await subgraphs[task.name].aget_state(
|
||||
config, subgraphs=True
|
||||
)
|
||||
# apply pending writes
|
||||
if null_writes := [
|
||||
w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID
|
||||
]:
|
||||
apply_writes(
|
||||
saved.checkpoint,
|
||||
channels,
|
||||
[PregelTaskWrites((), INPUT, null_writes, [])],
|
||||
None,
|
||||
)
|
||||
if apply_pending_writes and saved.pending_writes:
|
||||
for tid, k, v in saved.pending_writes:
|
||||
if k in (ERROR, INTERRUPT, SCHEDULED):
|
||||
continue
|
||||
if tid not in next_tasks:
|
||||
continue
|
||||
next_tasks[tid].writes.append((k, v))
|
||||
if tasks := [t for t in next_tasks.values() if t.writes]:
|
||||
apply_writes(saved.checkpoint, channels, tasks, None)
|
||||
# assemble the state snapshot
|
||||
return StateSnapshot(
|
||||
read_channels(channels, self.stream_channels_asis),
|
||||
tuple(t.name for t in next_tasks.values()),
|
||||
tuple(t.name for t in next_tasks.values() if not t.writes),
|
||||
patch_checkpoint_map(saved.config, saved.metadata),
|
||||
saved.metadata,
|
||||
saved.checkpoint["ts"],
|
||||
@@ -638,7 +699,10 @@ class Pregel(PregelProtocol):
|
||||
config = merge_configs(self.config, config) if self.config else config
|
||||
saved = checkpointer.get_tuple(config)
|
||||
return self._prepare_state_snapshot(
|
||||
config, saved, recurse=checkpointer if subgraphs else None
|
||||
config,
|
||||
saved,
|
||||
recurse=checkpointer if subgraphs else None,
|
||||
apply_pending_writes=CONFIG_KEY_CHECKPOINT_ID not in config[CONF],
|
||||
)
|
||||
|
||||
async def aget_state(
|
||||
@@ -672,7 +736,10 @@ class Pregel(PregelProtocol):
|
||||
config = merge_configs(self.config, config) if self.config else config
|
||||
saved = await checkpointer.aget_tuple(config)
|
||||
return await self._aprepare_state_snapshot(
|
||||
config, saved, recurse=checkpointer if subgraphs else None
|
||||
config,
|
||||
saved,
|
||||
recurse=checkpointer if subgraphs else None,
|
||||
apply_pending_writes=CONFIG_KEY_CHECKPOINT_ID not in config[CONF],
|
||||
)
|
||||
|
||||
def get_state_history(
|
||||
@@ -814,7 +881,7 @@ class Pregel(PregelProtocol):
|
||||
raise ValueError(f"Subgraph {recast_checkpoint_ns} not found")
|
||||
|
||||
# get last checkpoint
|
||||
config = merge_configs(self.config, config) if self.config else config
|
||||
config = ensure_config(self.config, config)
|
||||
saved = checkpointer.get_tuple(config)
|
||||
checkpoint = copy_checkpoint(saved.checkpoint) if saved else empty_checkpoint()
|
||||
checkpoint_previous_versions = (
|
||||
@@ -826,62 +893,176 @@ class Pregel(PregelProtocol):
|
||||
config,
|
||||
{CONFIG_KEY_CHECKPOINT_NS: config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "")},
|
||||
)
|
||||
checkpoint_metadata = config["metadata"]
|
||||
if saved:
|
||||
checkpoint_config = patch_configurable(config, saved.config[CONF])
|
||||
# find last node that updated the state, if not provided
|
||||
if values is None and as_node is None:
|
||||
next_config = checkpointer.put(
|
||||
checkpoint_config,
|
||||
create_checkpoint(checkpoint, None, step),
|
||||
{
|
||||
"source": "update",
|
||||
"step": step + 1,
|
||||
"writes": {},
|
||||
"parents": saved.metadata.get("parents", {}) if saved else {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
return patch_checkpoint_map(next_config, saved.metadata if saved else None)
|
||||
elif as_node is None and not any(
|
||||
v for vv in checkpoint["versions_seen"].values() for v in vv.values()
|
||||
):
|
||||
if (
|
||||
isinstance(self.input_channels, str)
|
||||
and self.input_channels in self.nodes
|
||||
):
|
||||
as_node = self.input_channels
|
||||
elif as_node is None:
|
||||
last_seen_by_node = sorted(
|
||||
(v, n)
|
||||
for n, seen in checkpoint["versions_seen"].items()
|
||||
if n in self.nodes
|
||||
for v in seen.values()
|
||||
)
|
||||
# if two nodes updated the state at the same time, it's ambiguous
|
||||
if last_seen_by_node:
|
||||
if len(last_seen_by_node) == 1:
|
||||
as_node = last_seen_by_node[0][1]
|
||||
elif last_seen_by_node[-1][0] != last_seen_by_node[-2][0]:
|
||||
as_node = last_seen_by_node[-1][1]
|
||||
if as_node is None:
|
||||
raise InvalidUpdateError("Ambiguous update, specify as_node")
|
||||
if as_node not in self.nodes:
|
||||
raise InvalidUpdateError(f"Node {as_node} does not exist")
|
||||
# update channels
|
||||
checkpoint_metadata = {**saved.metadata, **checkpoint_metadata}
|
||||
with ChannelsManager(
|
||||
self.channels,
|
||||
checkpoint,
|
||||
LoopProtocol(config=config, step=step + 1, stop=step + 2),
|
||||
) as (
|
||||
channels,
|
||||
managed,
|
||||
):
|
||||
) as (channels, managed):
|
||||
# no values as END, just clear all tasks
|
||||
if values is None and as_node == END:
|
||||
if saved is not None:
|
||||
# tasks for this checkpoint
|
||||
next_tasks = prepare_next_tasks(
|
||||
checkpoint,
|
||||
saved.pending_writes or [],
|
||||
self.nodes,
|
||||
channels,
|
||||
managed,
|
||||
saved.config,
|
||||
saved.metadata.get("step", -1) + 1,
|
||||
for_execution=True,
|
||||
store=self.store,
|
||||
checkpointer=self.checkpointer or None,
|
||||
manager=None,
|
||||
)
|
||||
# apply null writes
|
||||
if null_writes := [
|
||||
w[1:]
|
||||
for w in saved.pending_writes or []
|
||||
if w[0] == NULL_TASK_ID
|
||||
]:
|
||||
apply_writes(
|
||||
saved.checkpoint,
|
||||
channels,
|
||||
[PregelTaskWrites((), INPUT, null_writes, [])],
|
||||
None,
|
||||
)
|
||||
# apply writes from tasks that already ran
|
||||
for tid, k, v in saved.pending_writes or []:
|
||||
if k in (ERROR, INTERRUPT, SCHEDULED):
|
||||
continue
|
||||
if tid not in next_tasks:
|
||||
continue
|
||||
next_tasks[tid].writes.append((k, v))
|
||||
# clear all current tasks
|
||||
apply_writes(checkpoint, channels, next_tasks.values(), None)
|
||||
# save checkpoint
|
||||
next_config = checkpointer.put(
|
||||
checkpoint_config,
|
||||
create_checkpoint(checkpoint, None, step),
|
||||
{
|
||||
**checkpoint_metadata,
|
||||
"source": "update",
|
||||
"step": step + 1,
|
||||
"writes": {},
|
||||
"parents": saved.metadata.get("parents", {}) if saved else {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
return patch_checkpoint_map(
|
||||
next_config, saved.metadata if saved else None
|
||||
)
|
||||
# no values, copy checkpoint
|
||||
if values is None and as_node is None:
|
||||
next_checkpoint = create_checkpoint(checkpoint, None, step)
|
||||
# copy checkpoint
|
||||
next_config = checkpointer.put(
|
||||
checkpoint_config,
|
||||
next_checkpoint,
|
||||
{
|
||||
**checkpoint_metadata,
|
||||
"source": "update",
|
||||
"step": step + 1,
|
||||
"writes": {},
|
||||
"parents": saved.metadata.get("parents", {}) if saved else {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
return patch_checkpoint_map(
|
||||
next_config, saved.metadata if saved else None
|
||||
)
|
||||
if values is None and as_node == "__copy__":
|
||||
next_checkpoint = create_checkpoint(checkpoint, None, step)
|
||||
# copy checkpoint
|
||||
next_config = checkpointer.put(
|
||||
saved.parent_config or saved.config if saved else checkpoint_config,
|
||||
next_checkpoint,
|
||||
{
|
||||
**checkpoint_metadata,
|
||||
"source": "fork",
|
||||
"step": step + 1,
|
||||
"parents": saved.metadata.get("parents", {}) if saved else {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
return patch_checkpoint_map(
|
||||
next_config, saved.metadata if saved else None
|
||||
)
|
||||
# apply pending writes, if not on specific checkpoint
|
||||
if (
|
||||
CONFIG_KEY_CHECKPOINT_ID not in config[CONF]
|
||||
and saved is not None
|
||||
and saved.pending_writes
|
||||
):
|
||||
# tasks for this checkpoint
|
||||
next_tasks = prepare_next_tasks(
|
||||
checkpoint,
|
||||
saved.pending_writes,
|
||||
self.nodes,
|
||||
channels,
|
||||
managed,
|
||||
saved.config,
|
||||
saved.metadata.get("step", -1) + 1,
|
||||
for_execution=True,
|
||||
store=self.store,
|
||||
checkpointer=self.checkpointer or None,
|
||||
manager=None,
|
||||
)
|
||||
# apply null writes
|
||||
if null_writes := [
|
||||
w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID
|
||||
]:
|
||||
apply_writes(
|
||||
saved.checkpoint,
|
||||
channels,
|
||||
[PregelTaskWrites((), INPUT, null_writes, [])],
|
||||
None,
|
||||
)
|
||||
# apply writes
|
||||
for tid, k, v in saved.pending_writes:
|
||||
if k in (ERROR, INTERRUPT, SCHEDULED):
|
||||
continue
|
||||
if tid not in next_tasks:
|
||||
continue
|
||||
next_tasks[tid].writes.append((k, v))
|
||||
if tasks := [t for t in next_tasks.values() if t.writes]:
|
||||
apply_writes(checkpoint, channels, tasks, None)
|
||||
# find last node that updated the state, if not provided
|
||||
if as_node is None and not any(
|
||||
v for vv in checkpoint["versions_seen"].values() for v in vv.values()
|
||||
):
|
||||
if (
|
||||
isinstance(self.input_channels, str)
|
||||
and self.input_channels in self.nodes
|
||||
):
|
||||
as_node = self.input_channels
|
||||
elif as_node is None:
|
||||
last_seen_by_node = sorted(
|
||||
(v, n)
|
||||
for n, seen in checkpoint["versions_seen"].items()
|
||||
if n in self.nodes
|
||||
for v in seen.values()
|
||||
)
|
||||
# if two nodes updated the state at the same time, it's ambiguous
|
||||
if last_seen_by_node:
|
||||
if len(last_seen_by_node) == 1:
|
||||
as_node = last_seen_by_node[0][1]
|
||||
elif last_seen_by_node[-1][0] != last_seen_by_node[-2][0]:
|
||||
as_node = last_seen_by_node[-1][1]
|
||||
if as_node is None:
|
||||
raise InvalidUpdateError("Ambiguous update, specify as_node")
|
||||
if as_node not in self.nodes:
|
||||
raise InvalidUpdateError(f"Node {as_node} does not exist")
|
||||
# create task to run all writers of the chosen node
|
||||
writers = self.nodes[as_node].flat_writers
|
||||
if not writers:
|
||||
raise InvalidUpdateError(f"Node {as_node} has no writers")
|
||||
writes: deque[tuple[str, Any]] = deque()
|
||||
task = PregelTaskWrites(as_node, writes, [INTERRUPT])
|
||||
task = PregelTaskWrites((), as_node, writes, [INTERRUPT])
|
||||
task_id = str(uuid5(UUID(checkpoint["id"]), INTERRUPT))
|
||||
run = RunnableSequence(*writers) if len(writers) > 1 else writers[0]
|
||||
# execute task
|
||||
@@ -910,8 +1091,14 @@ class Pregel(PregelProtocol):
|
||||
),
|
||||
)
|
||||
# save task writes
|
||||
if saved:
|
||||
checkpointer.put_writes(checkpoint_config, task.writes, task_id)
|
||||
# channel writes are saved to current checkpoint
|
||||
# push writes are saved to next checkpoint
|
||||
channel_writes, push_writes = (
|
||||
[w for w in task.writes if w[0] != PUSH],
|
||||
[w for w in task.writes if w[0] == PUSH],
|
||||
)
|
||||
if saved and channel_writes:
|
||||
checkpointer.put_writes(checkpoint_config, channel_writes, task_id)
|
||||
# apply to checkpoint and save
|
||||
mv_writes = apply_writes(
|
||||
checkpoint, channels, [task], checkpointer.get_next_version
|
||||
@@ -922,6 +1109,7 @@ class Pregel(PregelProtocol):
|
||||
checkpoint_config,
|
||||
checkpoint,
|
||||
{
|
||||
**checkpoint_metadata,
|
||||
"source": "update",
|
||||
"step": step + 1,
|
||||
"writes": {as_node: values},
|
||||
@@ -931,6 +1119,8 @@ class Pregel(PregelProtocol):
|
||||
checkpoint_previous_versions, checkpoint["channel_versions"]
|
||||
),
|
||||
)
|
||||
if push_writes:
|
||||
checkpointer.put_writes(next_config, push_writes, task_id)
|
||||
return patch_checkpoint_map(next_config, saved.metadata if saved else None)
|
||||
|
||||
async def aupdate_state(
|
||||
@@ -966,7 +1156,7 @@ class Pregel(PregelProtocol):
|
||||
raise ValueError(f"Subgraph {recast_checkpoint_ns} not found")
|
||||
|
||||
# get last checkpoint
|
||||
config = merge_configs(self.config, config) if self.config else config
|
||||
config = ensure_config(self.config, config)
|
||||
saved = await checkpointer.aget_tuple(config)
|
||||
checkpoint = copy_checkpoint(saved.checkpoint) if saved else empty_checkpoint()
|
||||
checkpoint_previous_versions = (
|
||||
@@ -978,46 +1168,10 @@ class Pregel(PregelProtocol):
|
||||
config,
|
||||
{CONFIG_KEY_CHECKPOINT_NS: config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "")},
|
||||
)
|
||||
checkpoint_metadata = config["metadata"]
|
||||
if saved:
|
||||
checkpoint_config = patch_configurable(config, saved.config[CONF])
|
||||
# find last node that updated the state, if not provided
|
||||
if values is None and as_node is None:
|
||||
next_config = await checkpointer.aput(
|
||||
checkpoint_config,
|
||||
create_checkpoint(checkpoint, None, step),
|
||||
{
|
||||
"source": "update",
|
||||
"step": step + 1,
|
||||
"writes": {},
|
||||
"parents": saved.metadata.get("parents", {}) if saved else {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
return patch_checkpoint_map(next_config, saved.metadata if saved else None)
|
||||
elif as_node is None and not saved:
|
||||
if (
|
||||
isinstance(self.input_channels, str)
|
||||
and self.input_channels in self.nodes
|
||||
):
|
||||
as_node = self.input_channels
|
||||
elif as_node is None:
|
||||
last_seen_by_node = sorted(
|
||||
(v, n)
|
||||
for n, seen in checkpoint["versions_seen"].items()
|
||||
if n in self.nodes
|
||||
for v in seen.values()
|
||||
)
|
||||
# if two nodes updated the state at the same time, it's ambiguous
|
||||
if last_seen_by_node:
|
||||
if len(last_seen_by_node) == 1:
|
||||
as_node = last_seen_by_node[0][1]
|
||||
elif last_seen_by_node[-1][0] != last_seen_by_node[-2][0]:
|
||||
as_node = last_seen_by_node[-1][1]
|
||||
if as_node is None:
|
||||
raise InvalidUpdateError("Ambiguous update, specify as_node")
|
||||
if as_node not in self.nodes:
|
||||
raise InvalidUpdateError(f"Node {as_node} does not exist")
|
||||
# update channels, acting as the chosen node
|
||||
checkpoint_metadata = {**saved.metadata, **checkpoint_metadata}
|
||||
async with AsyncChannelsManager(
|
||||
self.channels,
|
||||
checkpoint,
|
||||
@@ -1026,12 +1180,164 @@ class Pregel(PregelProtocol):
|
||||
channels,
|
||||
managed,
|
||||
):
|
||||
# no values, just clear all tasks
|
||||
if values is None and as_node == END:
|
||||
if saved is not None:
|
||||
# tasks for this checkpoint
|
||||
next_tasks = prepare_next_tasks(
|
||||
checkpoint,
|
||||
saved.pending_writes or [],
|
||||
self.nodes,
|
||||
channels,
|
||||
managed,
|
||||
saved.config,
|
||||
saved.metadata.get("step", -1) + 1,
|
||||
for_execution=True,
|
||||
store=self.store,
|
||||
checkpointer=self.checkpointer or None,
|
||||
manager=None,
|
||||
)
|
||||
# apply null writes
|
||||
if null_writes := [
|
||||
w[1:]
|
||||
for w in saved.pending_writes or []
|
||||
if w[0] == NULL_TASK_ID
|
||||
]:
|
||||
apply_writes(
|
||||
saved.checkpoint,
|
||||
channels,
|
||||
[PregelTaskWrites((), INPUT, null_writes, [])],
|
||||
None,
|
||||
)
|
||||
# apply writes from tasks that already ran
|
||||
for tid, k, v in saved.pending_writes or []:
|
||||
if k in (ERROR, INTERRUPT, SCHEDULED):
|
||||
continue
|
||||
if tid not in next_tasks:
|
||||
continue
|
||||
next_tasks[tid].writes.append((k, v))
|
||||
# clear all current tasks
|
||||
apply_writes(checkpoint, channels, next_tasks.values(), None)
|
||||
# save checkpoint
|
||||
next_config = await checkpointer.aput(
|
||||
checkpoint_config,
|
||||
create_checkpoint(checkpoint, None, step),
|
||||
{
|
||||
**checkpoint_metadata,
|
||||
"source": "update",
|
||||
"step": step + 1,
|
||||
"writes": {},
|
||||
"parents": saved.metadata.get("parents", {}) if saved else {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
return patch_checkpoint_map(
|
||||
next_config, saved.metadata if saved else None
|
||||
)
|
||||
# no values, copy checkpoint
|
||||
if values is None and as_node is None:
|
||||
next_checkpoint = create_checkpoint(checkpoint, None, step)
|
||||
# copy checkpoint
|
||||
next_config = await checkpointer.aput(
|
||||
checkpoint_config,
|
||||
next_checkpoint,
|
||||
{
|
||||
**checkpoint_metadata,
|
||||
"source": "update",
|
||||
"step": step + 1,
|
||||
"writes": {},
|
||||
"parents": saved.metadata.get("parents", {}) if saved else {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
return patch_checkpoint_map(
|
||||
next_config, saved.metadata if saved else None
|
||||
)
|
||||
if values is None and as_node == "__copy__":
|
||||
next_checkpoint = create_checkpoint(checkpoint, None, step)
|
||||
# copy checkpoint
|
||||
next_config = await checkpointer.aput(
|
||||
saved.parent_config or saved.config if saved else checkpoint_config,
|
||||
next_checkpoint,
|
||||
{
|
||||
**checkpoint_metadata,
|
||||
"source": "fork",
|
||||
"step": step + 1,
|
||||
"parents": saved.metadata.get("parents", {}) if saved else {},
|
||||
},
|
||||
{},
|
||||
)
|
||||
return patch_checkpoint_map(
|
||||
next_config, saved.metadata if saved else None
|
||||
)
|
||||
# apply pending writes, if not on specific checkpoint
|
||||
if (
|
||||
CONFIG_KEY_CHECKPOINT_ID not in config[CONF]
|
||||
and saved is not None
|
||||
and saved.pending_writes
|
||||
):
|
||||
# tasks for this checkpoint
|
||||
next_tasks = prepare_next_tasks(
|
||||
checkpoint,
|
||||
saved.pending_writes,
|
||||
self.nodes,
|
||||
channels,
|
||||
managed,
|
||||
saved.config,
|
||||
saved.metadata.get("step", -1) + 1,
|
||||
for_execution=True,
|
||||
store=self.store,
|
||||
checkpointer=self.checkpointer or None,
|
||||
manager=None,
|
||||
)
|
||||
# apply null writes
|
||||
if null_writes := [
|
||||
w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID
|
||||
]:
|
||||
apply_writes(
|
||||
saved.checkpoint,
|
||||
channels,
|
||||
[PregelTaskWrites((), INPUT, null_writes, [])],
|
||||
None,
|
||||
)
|
||||
for tid, k, v in saved.pending_writes:
|
||||
if k in (ERROR, INTERRUPT, SCHEDULED):
|
||||
continue
|
||||
if tid not in next_tasks:
|
||||
continue
|
||||
next_tasks[tid].writes.append((k, v))
|
||||
if tasks := [t for t in next_tasks.values() if t.writes]:
|
||||
apply_writes(checkpoint, channels, tasks, None)
|
||||
# find last node that updated the state, if not provided
|
||||
if as_node is None and not saved:
|
||||
if (
|
||||
isinstance(self.input_channels, str)
|
||||
and self.input_channels in self.nodes
|
||||
):
|
||||
as_node = self.input_channels
|
||||
elif as_node is None:
|
||||
last_seen_by_node = sorted(
|
||||
(v, n)
|
||||
for n, seen in checkpoint["versions_seen"].items()
|
||||
if n in self.nodes
|
||||
for v in seen.values()
|
||||
)
|
||||
# if two nodes updated the state at the same time, it's ambiguous
|
||||
if last_seen_by_node:
|
||||
if len(last_seen_by_node) == 1:
|
||||
as_node = last_seen_by_node[0][1]
|
||||
elif last_seen_by_node[-1][0] != last_seen_by_node[-2][0]:
|
||||
as_node = last_seen_by_node[-1][1]
|
||||
if as_node is None:
|
||||
raise InvalidUpdateError("Ambiguous update, specify as_node")
|
||||
if as_node not in self.nodes:
|
||||
raise InvalidUpdateError(f"Node {as_node} does not exist")
|
||||
# create task to run all writers of the chosen node
|
||||
writers = self.nodes[as_node].flat_writers
|
||||
if not writers:
|
||||
raise InvalidUpdateError(f"Node {as_node} has no writers")
|
||||
writes: deque[tuple[str, Any]] = deque()
|
||||
task = PregelTaskWrites(as_node, writes, [INTERRUPT])
|
||||
task = PregelTaskWrites((), as_node, writes, [INTERRUPT])
|
||||
task_id = str(uuid5(UUID(checkpoint["id"]), INTERRUPT))
|
||||
run = RunnableSequence(*writers) if len(writers) > 1 else writers[0]
|
||||
# execute task
|
||||
@@ -1060,18 +1366,28 @@ class Pregel(PregelProtocol):
|
||||
),
|
||||
)
|
||||
# save task writes
|
||||
if saved:
|
||||
await checkpointer.aput_writes(checkpoint_config, writes, task_id)
|
||||
# channel writes are saved to current checkpoint
|
||||
# push writes are saved to next checkpoint
|
||||
channel_writes, push_writes = (
|
||||
[w for w in task.writes if w[0] != PUSH],
|
||||
[w for w in task.writes if w[0] == PUSH],
|
||||
)
|
||||
if saved and channel_writes:
|
||||
await checkpointer.aput_writes(
|
||||
checkpoint_config, channel_writes, task_id
|
||||
)
|
||||
# apply to checkpoint and save
|
||||
mv_writes = apply_writes(
|
||||
checkpoint, channels, [task], checkpointer.get_next_version
|
||||
)
|
||||
assert not mv_writes, "Can't write to SharedValues from update_state"
|
||||
checkpoint = create_checkpoint(checkpoint, channels, step + 1)
|
||||
# save checkpoint, after applying writes
|
||||
next_config = await checkpointer.aput(
|
||||
checkpoint_config,
|
||||
checkpoint,
|
||||
{
|
||||
**checkpoint_metadata,
|
||||
"source": "update",
|
||||
"step": step + 1,
|
||||
"writes": {as_node: values},
|
||||
@@ -1081,6 +1397,9 @@ class Pregel(PregelProtocol):
|
||||
checkpoint_previous_versions, checkpoint["channel_versions"]
|
||||
),
|
||||
)
|
||||
# save push writes
|
||||
if push_writes:
|
||||
await checkpointer.aput_writes(next_config, push_writes, task_id)
|
||||
return patch_checkpoint_map(next_config, saved.metadata if saved else None)
|
||||
|
||||
def _defaults(
|
||||
@@ -1283,12 +1602,16 @@ class Pregel(PregelProtocol):
|
||||
specs=self.channels,
|
||||
output_keys=output_keys,
|
||||
stream_keys=self.stream_channels_asis,
|
||||
interrupt_before=interrupt_before_,
|
||||
interrupt_after=interrupt_after_,
|
||||
manager=run_manager,
|
||||
debug=debug,
|
||||
) as loop:
|
||||
# create runner
|
||||
runner = PregelRunner(
|
||||
submit=loop.submit,
|
||||
put_writes=loop.put_writes,
|
||||
schedule_task=loop.accept_push,
|
||||
node_finished=config[CONF].get(CONFIG_KEY_NODE_FINISHED),
|
||||
)
|
||||
# enable subgraph streaming
|
||||
@@ -1319,12 +1642,7 @@ class Pregel(PregelProtocol):
|
||||
# channel updates from step N are only visible in step N+1
|
||||
# channels are guaranteed to be immutable for the duration of the step,
|
||||
# with channel updates applied only at the transition between steps
|
||||
while loop.tick(
|
||||
input_keys=self.input_channels,
|
||||
interrupt_before=interrupt_before_,
|
||||
interrupt_after=interrupt_after_,
|
||||
manager=run_manager,
|
||||
):
|
||||
while loop.tick(input_keys=self.input_channels):
|
||||
for _ in runner.tick(
|
||||
loop.tasks.values(),
|
||||
timeout=self.step_timeout,
|
||||
@@ -1505,11 +1823,16 @@ class Pregel(PregelProtocol):
|
||||
specs=self.channels,
|
||||
output_keys=output_keys,
|
||||
stream_keys=self.stream_channels_asis,
|
||||
interrupt_before=interrupt_before_,
|
||||
interrupt_after=interrupt_after_,
|
||||
manager=run_manager,
|
||||
debug=debug,
|
||||
) as loop:
|
||||
# create runner
|
||||
runner = PregelRunner(
|
||||
submit=loop.submit,
|
||||
put_writes=loop.put_writes,
|
||||
schedule_task=loop.accept_push,
|
||||
use_astream=do_stream is not None,
|
||||
node_finished=config[CONF].get(CONFIG_KEY_NODE_FINISHED),
|
||||
)
|
||||
@@ -1529,12 +1852,7 @@ class Pregel(PregelProtocol):
|
||||
# channel updates from step N are only visible in step N+1
|
||||
# channels are guaranteed to be immutable for the duration of the step,
|
||||
# with channel updates applied only at the transition between steps
|
||||
while loop.tick(
|
||||
input_keys=self.input_channels,
|
||||
interrupt_before=interrupt_before_,
|
||||
interrupt_after=interrupt_after_,
|
||||
manager=run_manager,
|
||||
):
|
||||
while loop.tick(input_keys=self.input_channels):
|
||||
async for _ in runner.atick(
|
||||
loop.tasks.values(),
|
||||
timeout=self.step_timeout,
|
||||
|
||||
@@ -25,6 +25,7 @@ from langgraph.channels.base import BaseChannel
|
||||
from langgraph.checkpoint.base import (
|
||||
BaseCheckpointSaver,
|
||||
Checkpoint,
|
||||
PendingWrite,
|
||||
V,
|
||||
copy_checkpoint,
|
||||
)
|
||||
@@ -35,17 +36,21 @@ from langgraph.constants import (
|
||||
CONFIG_KEY_CHECKPOINT_NS,
|
||||
CONFIG_KEY_CHECKPOINTER,
|
||||
CONFIG_KEY_READ,
|
||||
CONFIG_KEY_RESUME_VALUE,
|
||||
CONFIG_KEY_SEND,
|
||||
CONFIG_KEY_STORE,
|
||||
CONFIG_KEY_TASK_ID,
|
||||
EMPTY_SEQ,
|
||||
INTERRUPT,
|
||||
MISSING,
|
||||
NO_WRITES,
|
||||
NS_END,
|
||||
NS_SEP,
|
||||
NULL_TASK_ID,
|
||||
PULL,
|
||||
PUSH,
|
||||
RESERVED,
|
||||
RESUME,
|
||||
TAG_HIDDEN,
|
||||
TASKS,
|
||||
Send,
|
||||
@@ -67,6 +72,9 @@ class WritesProtocol(Protocol):
|
||||
"""Protocol for objects containing writes to be applied to checkpoint.
|
||||
Implemented by PregelTaskWrites and PregelExecutableTask."""
|
||||
|
||||
@property
|
||||
def path(self) -> tuple[Union[str, int, tuple], ...]: ...
|
||||
|
||||
@property
|
||||
def name(self) -> str: ...
|
||||
|
||||
@@ -81,6 +89,7 @@ class PregelTaskWrites(NamedTuple):
|
||||
"""Simplest implementation of WritesProtocol, for usage with writes that
|
||||
don't originate from a runnable task, eg. graph input, update_state, etc."""
|
||||
|
||||
path: tuple[Union[str, int, tuple], ...]
|
||||
name: str
|
||||
writes: Sequence[tuple[str, Any]]
|
||||
triggers: Sequence[str]
|
||||
@@ -168,7 +177,7 @@ def local_write(
|
||||
"""Function injected under CONFIG_KEY_SEND in task config, to write to channels.
|
||||
Validates writes and forwards them to `commit` function."""
|
||||
for chan, value in writes:
|
||||
if chan == TASKS:
|
||||
if chan in (PUSH, TASKS):
|
||||
if not isinstance(value, Send):
|
||||
raise InvalidUpdateError(f"Expected Send, got {value}")
|
||||
if value.node not in process_keys:
|
||||
@@ -190,6 +199,14 @@ def apply_writes(
|
||||
"""Apply writes from a set of tasks (usually the tasks from a Pregel step)
|
||||
to the checkpoint and channels, and return managed values writes to be applied
|
||||
externally."""
|
||||
# sort tasks on path, to ensure deterministic order for update application
|
||||
# any path parts after the 3rd are ignored for sorting
|
||||
# (we use them for eg. task ids which aren't good for sorting)
|
||||
tasks = sorted(tasks, key=lambda t: t.path[:3])
|
||||
# if no task has triggers this is applying writes from the null task only
|
||||
# so we don't do anything other than update the channels written to
|
||||
bump_step = any(t.triggers for t in tasks)
|
||||
|
||||
# update seen versions
|
||||
for task in tasks:
|
||||
checkpoint["versions_seen"].setdefault(task.name, {}).update(
|
||||
@@ -220,7 +237,7 @@ def apply_writes(
|
||||
)
|
||||
|
||||
# clear pending sends
|
||||
if checkpoint["pending_sends"]:
|
||||
if checkpoint["pending_sends"] and bump_step:
|
||||
checkpoint["pending_sends"].clear()
|
||||
|
||||
# Group writes by channel
|
||||
@@ -228,9 +245,9 @@ def apply_writes(
|
||||
pending_writes_by_managed: dict[str, list[Any]] = defaultdict(list)
|
||||
for task in tasks:
|
||||
for chan, val in task.writes:
|
||||
if chan == NO_WRITES:
|
||||
if chan in (NO_WRITES, PUSH, RESUME, INTERRUPT):
|
||||
pass
|
||||
elif chan == TASKS:
|
||||
elif chan == TASKS: # TODO: remove branch in 1.0
|
||||
checkpoint["pending_sends"].append(val)
|
||||
elif chan in channels:
|
||||
pending_writes_by_channel[chan].append(val)
|
||||
@@ -255,13 +272,14 @@ def apply_writes(
|
||||
updated_channels.add(chan)
|
||||
|
||||
# Channels that weren't updated in this step are notified of a new step
|
||||
for chan in channels:
|
||||
if chan not in updated_channels:
|
||||
if channels[chan].update([]) and get_next_version is not None:
|
||||
checkpoint["channel_versions"][chan] = get_next_version(
|
||||
max_version,
|
||||
channels[chan],
|
||||
)
|
||||
if bump_step:
|
||||
for chan in channels:
|
||||
if chan not in updated_channels:
|
||||
if channels[chan].update([]) and get_next_version is not None:
|
||||
checkpoint["channel_versions"][chan] = get_next_version(
|
||||
max_version,
|
||||
channels[chan],
|
||||
)
|
||||
|
||||
# Return managed values writes to be applied externally
|
||||
return pending_writes_by_managed
|
||||
@@ -270,6 +288,7 @@ def apply_writes(
|
||||
@overload
|
||||
def prepare_next_tasks(
|
||||
checkpoint: Checkpoint,
|
||||
pending_writes: Sequence[PendingWrite],
|
||||
processes: Mapping[str, PregelNode],
|
||||
channels: Mapping[str, BaseChannel],
|
||||
managed: ManagedValueMapping,
|
||||
@@ -286,6 +305,7 @@ def prepare_next_tasks(
|
||||
@overload
|
||||
def prepare_next_tasks(
|
||||
checkpoint: Checkpoint,
|
||||
pending_writes: Sequence[PendingWrite],
|
||||
processes: Mapping[str, PregelNode],
|
||||
channels: Mapping[str, BaseChannel],
|
||||
managed: ManagedValueMapping,
|
||||
@@ -301,6 +321,7 @@ def prepare_next_tasks(
|
||||
|
||||
def prepare_next_tasks(
|
||||
checkpoint: Checkpoint,
|
||||
pending_writes: Sequence[PendingWrite],
|
||||
processes: Mapping[str, PregelNode],
|
||||
channels: Mapping[str, BaseChannel],
|
||||
managed: ManagedValueMapping,
|
||||
@@ -315,13 +336,14 @@ def prepare_next_tasks(
|
||||
"""Prepare the set of tasks that will make up the next Pregel step.
|
||||
This is the union of all PUSH tasks (Sends) and PULL tasks (nodes triggered
|
||||
by edges)."""
|
||||
tasks: dict[str, Union[PregelTask, PregelExecutableTask]] = {}
|
||||
# Consume pending packets
|
||||
for idx, _ in enumerate(checkpoint["pending_sends"]):
|
||||
tasks: list[Union[PregelTask, PregelExecutableTask]] = []
|
||||
# Consume pending_sends from previous step (legacy version of Send)
|
||||
for idx, _ in enumerate(checkpoint["pending_sends"]): # TODO: remove branch in 1.0
|
||||
if task := prepare_single_task(
|
||||
(PUSH, idx),
|
||||
None,
|
||||
checkpoint=checkpoint,
|
||||
pending_writes=pending_writes,
|
||||
processes=processes,
|
||||
channels=channels,
|
||||
managed=managed,
|
||||
@@ -332,7 +354,7 @@ def prepare_next_tasks(
|
||||
checkpointer=checkpointer,
|
||||
manager=manager,
|
||||
):
|
||||
tasks[task.id] = task
|
||||
tasks.append(task)
|
||||
# Check if any processes should be run in next step
|
||||
# If so, prepare the values to be passed to them
|
||||
for name in processes:
|
||||
@@ -340,6 +362,7 @@ def prepare_next_tasks(
|
||||
(PULL, name),
|
||||
None,
|
||||
checkpoint=checkpoint,
|
||||
pending_writes=pending_writes,
|
||||
processes=processes,
|
||||
channels=channels,
|
||||
managed=managed,
|
||||
@@ -350,15 +373,74 @@ def prepare_next_tasks(
|
||||
checkpointer=checkpointer,
|
||||
manager=manager,
|
||||
):
|
||||
tasks[task.id] = task
|
||||
return tasks
|
||||
tasks.append(task)
|
||||
# Consume pending Sends from this step (new version of Send)
|
||||
if any(c == PUSH for _, c, _ in pending_writes):
|
||||
# group writes by task id
|
||||
grouped_by_task = defaultdict(list)
|
||||
for tid, c, _ in pending_writes:
|
||||
grouped_by_task[tid].append(c)
|
||||
# prepare send tasks from grouped writes
|
||||
# 1. start from sends originating from existing tasks
|
||||
tidx = 0
|
||||
while tidx < len(tasks):
|
||||
task = tasks[tidx]
|
||||
if twrites := grouped_by_task.pop(task.id, None):
|
||||
for idx, c in enumerate(twrites):
|
||||
if c != PUSH:
|
||||
continue
|
||||
if next_task := prepare_single_task(
|
||||
(PUSH, task.path, idx, task.id),
|
||||
None,
|
||||
checkpoint=checkpoint,
|
||||
pending_writes=pending_writes,
|
||||
processes=processes,
|
||||
channels=channels,
|
||||
managed=managed,
|
||||
config=config,
|
||||
step=step,
|
||||
for_execution=for_execution,
|
||||
store=store,
|
||||
checkpointer=checkpointer,
|
||||
manager=manager,
|
||||
):
|
||||
tasks.append(next_task)
|
||||
tidx += 1
|
||||
# key tasks by id
|
||||
task_map = {t.id: t for t in tasks}
|
||||
# 2. create new tasks for remaining sends (eg. from update_state)
|
||||
for tid, writes in grouped_by_task.items():
|
||||
task = task_map.get(tid)
|
||||
for idx, c in enumerate(writes):
|
||||
if c != PUSH:
|
||||
continue
|
||||
if next_task := prepare_single_task(
|
||||
(PUSH, task.path if task else (), idx, tid),
|
||||
None,
|
||||
checkpoint=checkpoint,
|
||||
pending_writes=pending_writes,
|
||||
processes=processes,
|
||||
channels=channels,
|
||||
managed=managed,
|
||||
config=config,
|
||||
step=step,
|
||||
for_execution=for_execution,
|
||||
store=store,
|
||||
checkpointer=checkpointer,
|
||||
manager=manager,
|
||||
):
|
||||
task_map[next_task.id] = next_task
|
||||
else:
|
||||
task_map = {t.id: t for t in tasks}
|
||||
return task_map
|
||||
|
||||
|
||||
def prepare_single_task(
|
||||
task_path: tuple[str, Union[int, str]],
|
||||
task_path: tuple[Union[str, int, tuple], ...],
|
||||
task_id_checksum: Optional[str],
|
||||
*,
|
||||
checkpoint: Checkpoint,
|
||||
pending_writes: Sequence[PendingWrite],
|
||||
processes: Mapping[str, PregelNode],
|
||||
channels: Mapping[str, BaseChannel],
|
||||
managed: ManagedValueMapping,
|
||||
@@ -376,31 +458,74 @@ def prepare_single_task(
|
||||
parent_ns = configurable.get(CONFIG_KEY_CHECKPOINT_NS, "")
|
||||
|
||||
if task_path[0] == PUSH:
|
||||
idx = int(task_path[1])
|
||||
if idx >= len(checkpoint["pending_sends"]):
|
||||
return
|
||||
packet = checkpoint["pending_sends"][idx]
|
||||
if not isinstance(packet, Send):
|
||||
logger.warning(
|
||||
f"Ignoring invalid packet type {type(packet)} in pending sends"
|
||||
if len(task_path) == 2: # TODO: remove branch in 1.0
|
||||
# legacy SEND tasks, executed in superstep n+1
|
||||
# (PUSH, idx of pending send)
|
||||
idx = cast(int, task_path[1])
|
||||
if idx >= len(checkpoint["pending_sends"]):
|
||||
return
|
||||
packet = checkpoint["pending_sends"][idx]
|
||||
if not isinstance(packet, Send):
|
||||
logger.warning(
|
||||
f"Ignoring invalid packet type {type(packet)} in pending sends"
|
||||
)
|
||||
return
|
||||
if packet.node not in processes:
|
||||
logger.warning(
|
||||
f"Ignoring unknown node name {packet.node} in pending sends"
|
||||
)
|
||||
return
|
||||
# create task id
|
||||
triggers = [PUSH]
|
||||
checkpoint_ns = (
|
||||
f"{parent_ns}{NS_SEP}{packet.node}" if parent_ns else packet.node
|
||||
)
|
||||
task_id = _uuid5_str(
|
||||
checkpoint_id,
|
||||
checkpoint_ns,
|
||||
str(step),
|
||||
packet.node,
|
||||
PUSH,
|
||||
str(idx),
|
||||
)
|
||||
elif len(task_path) == 4:
|
||||
# new PUSH tasks, executed in superstep n
|
||||
# (PUSH, parent task path, idx of PUSH write, id of parent task)
|
||||
task_path_t = cast(tuple[str, tuple, int, str], task_path)
|
||||
writes_for_path = [w for w in pending_writes if w[0] == task_path_t[3]]
|
||||
if task_path_t[2] >= len(writes_for_path):
|
||||
logger.warning(
|
||||
f"Ignoring invalid write index {task_path[2]} in pending writes"
|
||||
)
|
||||
return
|
||||
packet = writes_for_path[task_path_t[2]][2]
|
||||
if not isinstance(packet, Send):
|
||||
logger.warning(
|
||||
f"Ignoring invalid packet type {type(packet)} in pending writes"
|
||||
)
|
||||
return
|
||||
if packet.node not in processes:
|
||||
logger.warning(
|
||||
f"Ignoring unknown node name {packet.node} in pending writes"
|
||||
)
|
||||
return
|
||||
# create task id
|
||||
triggers = [PUSH]
|
||||
checkpoint_ns = (
|
||||
f"{parent_ns}{NS_SEP}{packet.node}" if parent_ns else packet.node
|
||||
)
|
||||
task_id = _uuid5_str(
|
||||
checkpoint_id,
|
||||
checkpoint_ns,
|
||||
str(step),
|
||||
packet.node,
|
||||
PUSH,
|
||||
_tuple_str(task_path[1]),
|
||||
str(task_path[2]),
|
||||
)
|
||||
else:
|
||||
logger.warning(f"Ignoring invalid PUSH task path {task_path}")
|
||||
return
|
||||
if packet.node not in processes:
|
||||
logger.warning(f"Ignoring unknown node name {packet.node} in pending sends")
|
||||
return
|
||||
# create task id
|
||||
triggers = [PUSH]
|
||||
checkpoint_ns = (
|
||||
f"{parent_ns}{NS_SEP}{packet.node}" if parent_ns else packet.node
|
||||
)
|
||||
task_id = _uuid5_str(
|
||||
checkpoint_id,
|
||||
checkpoint_ns,
|
||||
str(step),
|
||||
packet.node,
|
||||
PUSH,
|
||||
str(idx),
|
||||
)
|
||||
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
|
||||
metadata = {
|
||||
"langgraph_step": step,
|
||||
@@ -410,7 +535,7 @@ def prepare_single_task(
|
||||
"langgraph_checkpoint_ns": task_checkpoint_ns,
|
||||
}
|
||||
if task_id_checksum is not None:
|
||||
assert task_id == task_id_checksum
|
||||
assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
|
||||
if for_execution:
|
||||
proc = processes[packet.node]
|
||||
if node := proc.node:
|
||||
@@ -444,7 +569,9 @@ def prepare_single_task(
|
||||
checkpoint,
|
||||
channels,
|
||||
managed,
|
||||
PregelTaskWrites(packet.node, writes, triggers),
|
||||
PregelTaskWrites(
|
||||
task_path, packet.node, writes, triggers
|
||||
),
|
||||
config,
|
||||
),
|
||||
CONFIG_KEY_STORE: (
|
||||
@@ -460,6 +587,14 @@ def prepare_single_task(
|
||||
},
|
||||
CONFIG_KEY_CHECKPOINT_ID: None,
|
||||
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
|
||||
CONFIG_KEY_RESUME_VALUE: next(
|
||||
(
|
||||
v
|
||||
for tid, c, v in pending_writes
|
||||
if tid in (NULL_TASK_ID, task_id) and c == RESUME
|
||||
),
|
||||
MISSING,
|
||||
),
|
||||
},
|
||||
),
|
||||
triggers,
|
||||
@@ -472,6 +607,7 @@ def prepare_single_task(
|
||||
else:
|
||||
return PregelTask(task_id, packet.node, task_path)
|
||||
elif task_path[0] == PULL:
|
||||
# (PULL, node name)
|
||||
name = cast(str, task_path[1])
|
||||
if name not in processes:
|
||||
return
|
||||
@@ -552,7 +688,7 @@ def prepare_single_task(
|
||||
checkpoint,
|
||||
channels,
|
||||
managed,
|
||||
PregelTaskWrites(name, writes, triggers),
|
||||
PregelTaskWrites(task_path, name, writes, triggers),
|
||||
config,
|
||||
),
|
||||
CONFIG_KEY_STORE: (
|
||||
@@ -568,6 +704,15 @@ def prepare_single_task(
|
||||
},
|
||||
CONFIG_KEY_CHECKPOINT_ID: None,
|
||||
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
|
||||
CONFIG_KEY_RESUME_VALUE: next(
|
||||
(
|
||||
v
|
||||
for tid, c, v in pending_writes
|
||||
if tid in (NULL_TASK_ID, task_id)
|
||||
and c == RESUME
|
||||
),
|
||||
MISSING,
|
||||
),
|
||||
},
|
||||
),
|
||||
triggers,
|
||||
@@ -633,3 +778,12 @@ def _uuid5_str(namespace: bytes, *parts: str) -> str:
|
||||
sha.update(b"".join(p.encode() for p in parts))
|
||||
hex = sha.hexdigest()
|
||||
return f"{hex[:8]}-{hex[8:12]}-{hex[12:16]}-{hex[16:20]}-{hex[20:32]}"
|
||||
|
||||
|
||||
def _tuple_str(tup: Union[str, int, tuple]) -> str:
|
||||
"""Generate a string representation of a tuple."""
|
||||
return (
|
||||
f"({', '.join(_tuple_str(x) for x in tup)})"
|
||||
if isinstance(tup, (tuple, list))
|
||||
else str(tup)
|
||||
)
|
||||
|
||||
@@ -208,7 +208,7 @@ def print_step_tasks(step: int, next_tasks: list[PregelExecutableTask]) -> None:
|
||||
print(
|
||||
f"{get_colored_text(f'[{step}:tasks]', color='blue')} "
|
||||
+ get_bolded_text(
|
||||
f"Starting step {step} with {n_tasks} task{'s' if n_tasks != 1 else ''}:\n"
|
||||
f"Starting {n_tasks} task{'s' if n_tasks != 1 else ''} for step {step}:\n"
|
||||
)
|
||||
+ "\n".join(
|
||||
f"- {get_colored_text(task.name, 'green')} -> {pformat(task.input)}"
|
||||
|
||||
@@ -1,11 +1,31 @@
|
||||
from typing import Any, Iterator, Literal, Mapping, Optional, Sequence, TypeVar, Union
|
||||
from uuid import UUID
|
||||
|
||||
from langchain_core.runnables.utils import AddableDict
|
||||
|
||||
from langgraph.channels.base import BaseChannel, EmptyChannelError
|
||||
from langgraph.constants import EMPTY_SEQ, ERROR, INTERRUPT, TAG_HIDDEN
|
||||
from langgraph.constants import (
|
||||
EMPTY_SEQ,
|
||||
ERROR,
|
||||
FF_SEND_V2,
|
||||
INTERRUPT,
|
||||
NULL_TASK_ID,
|
||||
PUSH,
|
||||
RESUME,
|
||||
TAG_HIDDEN,
|
||||
TASKS,
|
||||
)
|
||||
from langgraph.pregel.log import logger
|
||||
from langgraph.types import PregelExecutableTask
|
||||
from langgraph.types import Command, PregelExecutableTask, Send
|
||||
|
||||
|
||||
def is_task_id(task_id: str) -> bool:
|
||||
"""Check if a string is a valid task id."""
|
||||
try:
|
||||
UUID(task_id)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def read_channel(
|
||||
@@ -44,6 +64,36 @@ def read_channels(
|
||||
return values
|
||||
|
||||
|
||||
def map_command(
|
||||
cmd: Command,
|
||||
) -> Iterator[tuple[str, str, Any]]:
|
||||
"""Map input chunk to a sequence of pending writes in the form (channel, value)."""
|
||||
if cmd.send:
|
||||
if isinstance(cmd.send, (tuple, list)):
|
||||
sends = cmd.send
|
||||
else:
|
||||
sends = [cmd.send]
|
||||
for send in sends:
|
||||
if not isinstance(send, Send):
|
||||
raise TypeError(
|
||||
f"In Command.send, expected Send, got {type(send).__name__}"
|
||||
)
|
||||
yield (NULL_TASK_ID, PUSH if FF_SEND_V2 else TASKS, send)
|
||||
if cmd.resume:
|
||||
if isinstance(cmd.resume, dict) and all(is_task_id(k) for k in cmd.resume):
|
||||
for tid, resume in cmd.resume.items():
|
||||
yield (tid, RESUME, resume)
|
||||
else:
|
||||
yield (NULL_TASK_ID, RESUME, cmd.resume)
|
||||
if cmd.update:
|
||||
if not isinstance(cmd.update, dict):
|
||||
raise TypeError(
|
||||
f"Expected cmd.update to be a dict mapping channel names to update values, got {type(cmd.update).__name__}"
|
||||
)
|
||||
for k, v in cmd.update.items():
|
||||
yield (NULL_TASK_ID, k, v)
|
||||
|
||||
|
||||
def map_input(
|
||||
input_channels: Union[str, Sequence[str]],
|
||||
chunk: Optional[Union[dict[str, Any], Any]],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
from collections import deque
|
||||
from collections import defaultdict, deque
|
||||
from contextlib import AsyncExitStack, ExitStack
|
||||
from types import TracebackType
|
||||
from typing import (
|
||||
@@ -52,6 +52,9 @@ from langgraph.constants import (
|
||||
INPUT,
|
||||
INTERRUPT,
|
||||
NS_SEP,
|
||||
NULL_TASK_ID,
|
||||
PUSH,
|
||||
RESUME,
|
||||
SCHEDULED,
|
||||
TAG_HIDDEN,
|
||||
)
|
||||
@@ -74,6 +77,7 @@ from langgraph.pregel.algo import (
|
||||
apply_writes,
|
||||
increment,
|
||||
prepare_next_tasks,
|
||||
prepare_single_task,
|
||||
should_interrupt,
|
||||
)
|
||||
from langgraph.pregel.debug import (
|
||||
@@ -90,6 +94,7 @@ from langgraph.pregel.executor import (
|
||||
Submit,
|
||||
)
|
||||
from langgraph.pregel.io import (
|
||||
map_command,
|
||||
map_input,
|
||||
map_output_updates,
|
||||
map_output_values,
|
||||
@@ -100,7 +105,13 @@ from langgraph.pregel.manager import AsyncChannelsManager, ChannelsManager
|
||||
from langgraph.pregel.read import PregelNode
|
||||
from langgraph.pregel.utils import get_new_channel_versions
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import All, LoopProtocol, PregelExecutableTask, StreamProtocol
|
||||
from langgraph.types import (
|
||||
All,
|
||||
Command,
|
||||
LoopProtocol,
|
||||
PregelExecutableTask,
|
||||
StreamProtocol,
|
||||
)
|
||||
from langgraph.utils.config import patch_configurable
|
||||
|
||||
V = TypeVar("V")
|
||||
@@ -130,6 +141,9 @@ class PregelLoop(LoopProtocol):
|
||||
stream_keys: Union[str, Sequence[str]]
|
||||
skip_done_tasks: bool
|
||||
is_nested: bool
|
||||
manager: Union[None, AsyncParentRunManager, ParentRunManager]
|
||||
interrupt_after: Union[All, Sequence[str]]
|
||||
interrupt_before: Union[All, Sequence[str]]
|
||||
|
||||
checkpointer_get_next_version: GetNextVersion
|
||||
checkpointer_put_writes: Optional[
|
||||
@@ -162,6 +176,7 @@ class PregelLoop(LoopProtocol):
|
||||
"pending", "done", "interrupt_before", "interrupt_after", "out_of_steps"
|
||||
]
|
||||
tasks: dict[str, PregelExecutableTask]
|
||||
to_interrupt: list[PregelExecutableTask]
|
||||
output: Union[None, dict[str, Any], Any] = None
|
||||
|
||||
# public
|
||||
@@ -178,6 +193,9 @@ class PregelLoop(LoopProtocol):
|
||||
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]],
|
||||
output_keys: Union[str, Sequence[str]],
|
||||
stream_keys: Union[str, Sequence[str]],
|
||||
interrupt_after: Union[All, Sequence[str]] = EMPTY_SEQ,
|
||||
interrupt_before: Union[All, Sequence[str]] = EMPTY_SEQ,
|
||||
manager: Union[None, AsyncParentRunManager, ParentRunManager] = None,
|
||||
check_subgraphs: bool = True,
|
||||
debug: bool = False,
|
||||
) -> None:
|
||||
@@ -194,6 +212,9 @@ class PregelLoop(LoopProtocol):
|
||||
self.specs = specs
|
||||
self.output_keys = output_keys
|
||||
self.stream_keys = stream_keys
|
||||
self.interrupt_after = interrupt_after
|
||||
self.interrupt_before = interrupt_before
|
||||
self.manager = manager
|
||||
self.is_nested = CONFIG_KEY_TASK_ID in self.config.get(CONF, {})
|
||||
self.skip_done_tasks = (
|
||||
CONFIG_KEY_CHECKPOINT_ID not in config[CONF]
|
||||
@@ -261,15 +282,60 @@ class PregelLoop(LoopProtocol):
|
||||
task_id,
|
||||
)
|
||||
# output writes
|
||||
self._output_writes(task_id, writes)
|
||||
if hasattr(self, "tasks"):
|
||||
self._output_writes(task_id, writes)
|
||||
|
||||
def accept_push(
|
||||
self, task: PregelExecutableTask, write_idx: int
|
||||
) -> Optional[PregelExecutableTask]:
|
||||
"""Accept a PUSH from a task, potentially returning a new task to start."""
|
||||
# don't start if an earlier PUSH has already triggered an interrupt
|
||||
if self.to_interrupt:
|
||||
return
|
||||
# don't start if we should interrupt *after* the original task
|
||||
if should_interrupt(self.checkpoint, self.interrupt_after, [task]):
|
||||
self.to_interrupt.append(task)
|
||||
return
|
||||
if pushed := cast(
|
||||
Optional[PregelExecutableTask],
|
||||
prepare_single_task(
|
||||
(PUSH, task.path, write_idx, task.id),
|
||||
None,
|
||||
checkpoint=self.checkpoint,
|
||||
pending_writes=[(task.id, *w) for w in task.writes],
|
||||
processes=self.nodes,
|
||||
channels=self.channels,
|
||||
managed=self.managed,
|
||||
config=self.config,
|
||||
step=self.step,
|
||||
for_execution=True,
|
||||
store=self.store,
|
||||
checkpointer=self.checkpointer,
|
||||
manager=self.manager,
|
||||
),
|
||||
):
|
||||
# don't start if we should interrupt *before* the new task
|
||||
if should_interrupt(self.checkpoint, self.interrupt_before, [pushed]):
|
||||
self.to_interrupt.append(pushed)
|
||||
return
|
||||
# produce debug output
|
||||
self._emit("debug", map_debug_tasks, self.step, [pushed])
|
||||
# debug flag
|
||||
if self.debug:
|
||||
print_step_tasks(self.step, [pushed])
|
||||
# save the new task
|
||||
self.tasks[pushed.id] = pushed
|
||||
# match any pending writes to the new task
|
||||
if self.skip_done_tasks:
|
||||
self._match_writes({pushed.id: pushed})
|
||||
# return the new task, to be started, if not run before
|
||||
if not pushed.writes:
|
||||
return pushed
|
||||
|
||||
def tick(
|
||||
self,
|
||||
*,
|
||||
input_keys: Union[str, Sequence[str]],
|
||||
interrupt_after: Union[All, Sequence[str]] = EMPTY_SEQ,
|
||||
interrupt_before: Union[All, Sequence[str]] = EMPTY_SEQ,
|
||||
manager: Union[None, AsyncParentRunManager, ParentRunManager] = None,
|
||||
) -> bool:
|
||||
"""Execute a single iteration of the Pregel loop.
|
||||
Returns True if more iterations are needed."""
|
||||
@@ -278,6 +344,10 @@ class PregelLoop(LoopProtocol):
|
||||
|
||||
if self.input not in (INPUT_DONE, INPUT_RESUMING):
|
||||
self._first(input_keys=input_keys)
|
||||
elif self.to_interrupt:
|
||||
# if we need to interrupt, do so
|
||||
self.status = "interrupt_before"
|
||||
raise GraphInterrupt()
|
||||
elif all(task.writes for task in self.tasks.values()):
|
||||
writes = [w for t in self.tasks.values() for w in t.writes]
|
||||
# debug flag
|
||||
@@ -307,6 +377,8 @@ class PregelLoop(LoopProtocol):
|
||||
)
|
||||
# clear pending writes
|
||||
self.checkpoint_pending_writes.clear()
|
||||
# "not skip_done_tasks" only applies to first tick after resuming
|
||||
self.skip_done_tasks = True
|
||||
# save checkpoint
|
||||
self._put_checkpoint(
|
||||
{
|
||||
@@ -320,7 +392,9 @@ class PregelLoop(LoopProtocol):
|
||||
}
|
||||
)
|
||||
# after execution, check if we should interrupt
|
||||
if should_interrupt(self.checkpoint, interrupt_after, self.tasks.values()):
|
||||
if should_interrupt(
|
||||
self.checkpoint, self.interrupt_after, self.tasks.values()
|
||||
):
|
||||
self.status = "interrupt_after"
|
||||
raise GraphInterrupt()
|
||||
else:
|
||||
@@ -331,19 +405,33 @@ class PregelLoop(LoopProtocol):
|
||||
self.status = "out_of_steps"
|
||||
return False
|
||||
|
||||
# apply NULL writes
|
||||
if null_writes := [
|
||||
w[1:] for w in self.checkpoint_pending_writes if w[0] == NULL_TASK_ID
|
||||
]:
|
||||
mv_writes = apply_writes(
|
||||
self.checkpoint,
|
||||
self.channels,
|
||||
[PregelTaskWrites((), INPUT, null_writes, [])],
|
||||
self.checkpointer_get_next_version,
|
||||
)
|
||||
for key, values in mv_writes.items():
|
||||
self._update_mv(key, values)
|
||||
# prepare next tasks
|
||||
self.tasks = prepare_next_tasks(
|
||||
self.checkpoint,
|
||||
self.checkpoint_pending_writes,
|
||||
self.nodes,
|
||||
self.channels,
|
||||
self.managed,
|
||||
self.config,
|
||||
self.step,
|
||||
for_execution=True,
|
||||
manager=manager,
|
||||
manager=self.manager,
|
||||
store=self.store,
|
||||
checkpointer=self.checkpointer,
|
||||
)
|
||||
self.to_interrupt = []
|
||||
|
||||
# produce debug output
|
||||
if self._checkpointer_put_after_previous is not None:
|
||||
@@ -381,36 +469,16 @@ class PregelLoop(LoopProtocol):
|
||||
|
||||
# if there are pending writes from a previous loop, apply them
|
||||
if self.skip_done_tasks and self.checkpoint_pending_writes:
|
||||
for tid, k, v in self.checkpoint_pending_writes:
|
||||
if k in (ERROR, INTERRUPT):
|
||||
continue
|
||||
if task := self.tasks.get(tid):
|
||||
if k == SCHEDULED:
|
||||
if v == max(
|
||||
self.checkpoint["versions_seen"]
|
||||
.get(INTERRUPT, {})
|
||||
.values(),
|
||||
default=None,
|
||||
):
|
||||
self.tasks[tid] = task._replace(scheduled=True)
|
||||
else:
|
||||
task.writes.append((k, v))
|
||||
# print output for any tasks we applied previous writes to
|
||||
for task in self.tasks.values():
|
||||
if task.writes:
|
||||
self._output_writes(task.id, task.writes, cached=True)
|
||||
self._match_writes(self.tasks)
|
||||
|
||||
# if all tasks have finished, re-tick
|
||||
if all(task.writes for task in self.tasks.values()):
|
||||
return self.tick(
|
||||
input_keys=input_keys,
|
||||
interrupt_after=interrupt_after,
|
||||
interrupt_before=interrupt_before,
|
||||
manager=manager,
|
||||
)
|
||||
return self.tick(input_keys=input_keys)
|
||||
|
||||
# before execution, check if we should interrupt
|
||||
if should_interrupt(self.checkpoint, interrupt_before, self.tasks.values()):
|
||||
if should_interrupt(
|
||||
self.checkpoint, self.interrupt_before, self.tasks.values()
|
||||
):
|
||||
self.status = "interrupt_before"
|
||||
raise GraphInterrupt()
|
||||
|
||||
@@ -421,10 +489,29 @@ class PregelLoop(LoopProtocol):
|
||||
if self.debug:
|
||||
print_step_tasks(self.step, list(self.tasks.values()))
|
||||
|
||||
# print output for any tasks we applied previous writes to
|
||||
for task in self.tasks.values():
|
||||
if task.writes:
|
||||
self._output_writes(task.id, task.writes, cached=True)
|
||||
|
||||
return True
|
||||
|
||||
# private
|
||||
|
||||
def _match_writes(self, tasks: Mapping[str, PregelExecutableTask]) -> None:
|
||||
for tid, k, v in self.checkpoint_pending_writes:
|
||||
if k in (ERROR, INTERRUPT, RESUME):
|
||||
continue
|
||||
if task := tasks.get(tid):
|
||||
if k == SCHEDULED:
|
||||
if v == max(
|
||||
self.checkpoint["versions_seen"].get(INTERRUPT, {}).values(),
|
||||
default=None,
|
||||
):
|
||||
self.tasks[tid] = task._replace(scheduled=True)
|
||||
else:
|
||||
task.writes.append((k, v))
|
||||
|
||||
def _first(self, *, input_keys: Union[str, Sequence[str]]) -> None:
|
||||
# resuming from previous checkpoint requires
|
||||
# - finding a previous checkpoint
|
||||
@@ -445,8 +532,20 @@ class PregelLoop(LoopProtocol):
|
||||
self._emit(
|
||||
"values", map_output_values, self.output_keys, True, self.channels
|
||||
)
|
||||
# map command to writes
|
||||
elif isinstance(self.input, Command):
|
||||
writes: defaultdict[str, list[tuple[str, Any]]] = defaultdict(list)
|
||||
# group writes by task ID
|
||||
for tid, c, v in map_command(self.input):
|
||||
writes[tid].append((c, v))
|
||||
if not writes:
|
||||
raise EmptyInputError("Received empty Command input")
|
||||
# save writes
|
||||
for tid, ws in writes.items():
|
||||
self.put_writes(tid, ws)
|
||||
# map inputs to channel updates
|
||||
elif input_writes := deque(map_input(input_keys, self.input)):
|
||||
# TODO shouldn't these writes be passed to put_writes too?
|
||||
# check if we should delegate (used by subgraphs in distributed mode)
|
||||
if self.config[CONF].get(CONFIG_KEY_DELEGATE):
|
||||
raise GraphDelegate(
|
||||
@@ -460,6 +559,7 @@ class PregelLoop(LoopProtocol):
|
||||
# discard any unfinished tasks from previous checkpoint
|
||||
discard_tasks = prepare_next_tasks(
|
||||
self.checkpoint,
|
||||
self.checkpoint_pending_writes,
|
||||
self.nodes,
|
||||
self.channels,
|
||||
self.managed,
|
||||
@@ -474,7 +574,10 @@ class PregelLoop(LoopProtocol):
|
||||
mv_writes = apply_writes(
|
||||
self.checkpoint,
|
||||
self.channels,
|
||||
[*discard_tasks.values(), PregelTaskWrites(INPUT, input_writes, [])],
|
||||
[
|
||||
*discard_tasks.values(),
|
||||
PregelTaskWrites((), INPUT, input_writes, []),
|
||||
],
|
||||
self.checkpointer_get_next_version,
|
||||
)
|
||||
assert not mv_writes, "Can't write to SharedValues in graph input"
|
||||
@@ -491,6 +594,8 @@ class PregelLoop(LoopProtocol):
|
||||
)
|
||||
|
||||
def _put_checkpoint(self, metadata: CheckpointMetadata) -> None:
|
||||
for k, v in self.config["metadata"].items():
|
||||
metadata.setdefault(k, v) # type: ignore
|
||||
# assign step and parents
|
||||
metadata["step"] = self.step
|
||||
metadata["parents"] = self.config[CONF].get(CONFIG_KEY_CHECKPOINT_MAP, {})
|
||||
@@ -568,11 +673,33 @@ class PregelLoop(LoopProtocol):
|
||||
# save final output
|
||||
self.output = read_channels(self.channels, self.output_keys)
|
||||
if suppress:
|
||||
# suppress interrupt
|
||||
# emit one last "values" event, with pending writes applied
|
||||
if (
|
||||
hasattr(self, "tasks")
|
||||
and self.checkpoint_pending_writes
|
||||
and any(task.writes for task in self.tasks.values())
|
||||
):
|
||||
mv_writes = apply_writes(
|
||||
self.checkpoint,
|
||||
self.channels,
|
||||
self.tasks.values(),
|
||||
self.checkpointer_get_next_version,
|
||||
)
|
||||
for key, values in mv_writes.items():
|
||||
self._update_mv(key, values)
|
||||
self._emit(
|
||||
"values",
|
||||
map_output_values,
|
||||
self.output_keys,
|
||||
[w for t in self.tasks.values() for w in t.writes],
|
||||
self.channels,
|
||||
)
|
||||
# emit INTERRUPT event
|
||||
self._emit(
|
||||
"updates",
|
||||
lambda: iter([{INTERRUPT: cast(GraphInterrupt, exc_value).args[0]}]),
|
||||
)
|
||||
# suppress interrupt
|
||||
return True
|
||||
|
||||
def _emit(
|
||||
@@ -626,6 +753,9 @@ class SyncPregelLoop(PregelLoop, ContextManager):
|
||||
checkpointer: Optional[BaseCheckpointSaver],
|
||||
nodes: Mapping[str, PregelNode],
|
||||
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]],
|
||||
manager: Union[None, AsyncParentRunManager, ParentRunManager] = None,
|
||||
interrupt_after: Union[All, Sequence[str]] = EMPTY_SEQ,
|
||||
interrupt_before: Union[All, Sequence[str]] = EMPTY_SEQ,
|
||||
output_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
|
||||
stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
|
||||
check_subgraphs: bool = True,
|
||||
@@ -641,7 +771,10 @@ class SyncPregelLoop(PregelLoop, ContextManager):
|
||||
specs=specs,
|
||||
output_keys=output_keys,
|
||||
stream_keys=stream_keys,
|
||||
interrupt_after=interrupt_after,
|
||||
interrupt_before=interrupt_before,
|
||||
check_subgraphs=check_subgraphs,
|
||||
manager=manager,
|
||||
debug=debug,
|
||||
)
|
||||
self.stack = ExitStack()
|
||||
@@ -752,6 +885,9 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager):
|
||||
checkpointer: Optional[BaseCheckpointSaver],
|
||||
nodes: Mapping[str, PregelNode],
|
||||
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]],
|
||||
interrupt_after: Union[All, Sequence[str]] = EMPTY_SEQ,
|
||||
interrupt_before: Union[All, Sequence[str]] = EMPTY_SEQ,
|
||||
manager: Union[None, AsyncParentRunManager, ParentRunManager] = None,
|
||||
output_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
|
||||
stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
|
||||
check_subgraphs: bool = True,
|
||||
@@ -767,7 +903,10 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager):
|
||||
specs=specs,
|
||||
output_keys=output_keys,
|
||||
stream_keys=stream_keys,
|
||||
interrupt_after=interrupt_after,
|
||||
interrupt_before=interrupt_before,
|
||||
check_subgraphs=check_subgraphs,
|
||||
manager=manager,
|
||||
debug=debug,
|
||||
)
|
||||
self.stack = AsyncExitStack()
|
||||
|
||||
@@ -2,9 +2,15 @@ import asyncio
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
from typing import Optional, Sequence
|
||||
from functools import partial
|
||||
from typing import Any, Callable, Optional, Sequence
|
||||
|
||||
from langgraph.constants import CONF, CONFIG_KEY_CHECKPOINT_NS, CONFIG_KEY_RESUMING
|
||||
from langgraph.constants import (
|
||||
CONF,
|
||||
CONFIG_KEY_CHECKPOINT_NS,
|
||||
CONFIG_KEY_RESUMING,
|
||||
CONFIG_KEY_SEND,
|
||||
)
|
||||
from langgraph.errors import _SEEN_CHECKPOINT_NS, GraphInterrupt
|
||||
from langgraph.types import PregelExecutableTask, RetryPolicy
|
||||
from langgraph.utils.config import patch_configurable
|
||||
@@ -15,12 +21,17 @@ logger = logging.getLogger(__name__)
|
||||
def run_with_retry(
|
||||
task: PregelExecutableTask,
|
||||
retry_policy: Optional[RetryPolicy],
|
||||
writer: Optional[
|
||||
Callable[[PregelExecutableTask, Sequence[tuple[str, Any]]], None]
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Run a task with retries."""
|
||||
retry_policy = task.retry_policy or retry_policy
|
||||
interval = retry_policy.initial_interval if retry_policy else 0
|
||||
attempts = 0
|
||||
config = task.config
|
||||
if writer is not None:
|
||||
config = patch_configurable(config, {CONFIG_KEY_SEND: partial(writer, task)})
|
||||
while True:
|
||||
try:
|
||||
# clear any writes from previous attempts
|
||||
@@ -84,12 +95,17 @@ async def arun_with_retry(
|
||||
task: PregelExecutableTask,
|
||||
retry_policy: Optional[RetryPolicy],
|
||||
stream: bool = False,
|
||||
writer: Optional[
|
||||
Callable[[PregelExecutableTask, Sequence[tuple[str, Any]]], None]
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Run a task asynchronously with retries."""
|
||||
retry_policy = task.retry_policy or retry_policy
|
||||
interval = retry_policy.initial_interval if retry_policy else 0
|
||||
attempts = 0
|
||||
config = task.config
|
||||
if writer is not None:
|
||||
config = patch_configurable(config, {CONFIG_KEY_SEND: partial(writer, task)})
|
||||
while True:
|
||||
try:
|
||||
# clear any writes from previous attempts
|
||||
|
||||
@@ -14,7 +14,15 @@ from typing import (
|
||||
cast,
|
||||
)
|
||||
|
||||
from langgraph.constants import ERROR, INTERRUPT, NO_WRITES, TAG_HIDDEN
|
||||
from langgraph.constants import (
|
||||
CONF,
|
||||
CONFIG_KEY_SEND,
|
||||
ERROR,
|
||||
INTERRUPT,
|
||||
NO_WRITES,
|
||||
PUSH,
|
||||
TAG_HIDDEN,
|
||||
)
|
||||
from langgraph.errors import GraphDelegate, GraphInterrupt
|
||||
from langgraph.pregel.executor import Submit
|
||||
from langgraph.pregel.retry import arun_with_retry, run_with_retry
|
||||
@@ -31,6 +39,9 @@ class PregelRunner:
|
||||
*,
|
||||
submit: Submit,
|
||||
put_writes: Callable[[str, Sequence[tuple[str, Any]]], None],
|
||||
schedule_task: Callable[
|
||||
[PregelExecutableTask, int], Optional[PregelExecutableTask]
|
||||
],
|
||||
use_astream: bool = False,
|
||||
node_finished: Optional[Callable[[str], None]] = None,
|
||||
) -> None:
|
||||
@@ -38,6 +49,7 @@ class PregelRunner:
|
||||
self.put_writes = put_writes
|
||||
self.use_astream = use_astream
|
||||
self.node_finished = node_finished
|
||||
self.schedule_task = schedule_task
|
||||
|
||||
def tick(
|
||||
self,
|
||||
@@ -48,27 +60,58 @@ class PregelRunner:
|
||||
retry_policy: Optional[RetryPolicy] = None,
|
||||
get_waiter: Optional[Callable[[], concurrent.futures.Future[None]]] = None,
|
||||
) -> Iterator[None]:
|
||||
def writer(
|
||||
task: PregelExecutableTask, writes: Sequence[tuple[str, Any]]
|
||||
) -> None:
|
||||
prev_length = len(task.writes)
|
||||
# delegate to the underlying writer
|
||||
task.config[CONF][CONFIG_KEY_SEND](writes)
|
||||
for idx, w in enumerate(task.writes):
|
||||
# find the index for the newly inserted writes
|
||||
if idx < prev_length:
|
||||
continue
|
||||
assert writes[idx - prev_length] is w
|
||||
# bail if not a PUSH write
|
||||
if w[0] != PUSH:
|
||||
continue
|
||||
# schedule the next task, if the callback returns one
|
||||
if next_task := self.schedule_task(task, idx):
|
||||
# if the parent task was retried,
|
||||
# the next task might already be running
|
||||
if any(
|
||||
t == next_task.id for t in futures.values() if t is not None
|
||||
):
|
||||
continue
|
||||
# schedule the next task
|
||||
futures[
|
||||
self.submit(
|
||||
run_with_retry,
|
||||
next_task,
|
||||
retry_policy,
|
||||
writer=writer,
|
||||
__reraise_on_exit__=reraise,
|
||||
)
|
||||
] = next_task
|
||||
|
||||
tasks = tuple(tasks)
|
||||
futures: dict[concurrent.futures.Future, Optional[PregelExecutableTask]] = {}
|
||||
# give control back to the caller
|
||||
yield
|
||||
# fast path if single task with no timeout and no waiter
|
||||
if len(tasks) == 1 and timeout is None and get_waiter is None:
|
||||
t = tasks[0]
|
||||
try:
|
||||
run_with_retry(t, retry_policy)
|
||||
run_with_retry(t, retry_policy, writer=writer)
|
||||
self.commit(t, None)
|
||||
except Exception as exc:
|
||||
self.commit(t, exc)
|
||||
if reraise:
|
||||
raise
|
||||
return
|
||||
if not futures: # maybe `t` schuduled another task
|
||||
return
|
||||
# add waiter task if requested
|
||||
if get_waiter is not None:
|
||||
futures: dict[concurrent.futures.Future, Optional[PregelExecutableTask]] = {
|
||||
get_waiter(): None
|
||||
}
|
||||
else:
|
||||
futures = {}
|
||||
futures[get_waiter()] = None
|
||||
# execute tasks, and wait for one to fail or all to finish.
|
||||
# each task is independent from all other concurrent tasks
|
||||
# yield updates/debug output as each task finishes
|
||||
@@ -79,10 +122,11 @@ class PregelRunner:
|
||||
run_with_retry,
|
||||
t,
|
||||
retry_policy,
|
||||
writer=writer,
|
||||
__reraise_on_exit__=reraise,
|
||||
)
|
||||
] = t
|
||||
all_futures = futures.copy()
|
||||
done_futures: set[concurrent.futures.Future] = set()
|
||||
end_time = timeout + time.monotonic() if timeout else None
|
||||
while len(futures) > (1 if get_waiter is not None else 0):
|
||||
done, inflight = concurrent.futures.wait(
|
||||
@@ -99,6 +143,8 @@ class PregelRunner:
|
||||
if inflight and get_waiter is not None:
|
||||
futures[get_waiter()] = None
|
||||
else:
|
||||
# store for panic check
|
||||
done_futures.add(fut)
|
||||
# task finished, commit writes
|
||||
self.commit(task, _exception(fut))
|
||||
else:
|
||||
@@ -110,7 +156,10 @@ class PregelRunner:
|
||||
# give control back to the caller
|
||||
yield
|
||||
# panic on failure or timeout
|
||||
_panic_or_proceed(all_futures, panic=reraise)
|
||||
_panic_or_proceed(
|
||||
done_futures.union(f for f, t in futures.items() if t is not None),
|
||||
panic=reraise,
|
||||
)
|
||||
|
||||
async def atick(
|
||||
self,
|
||||
@@ -121,28 +170,67 @@ class PregelRunner:
|
||||
retry_policy: Optional[RetryPolicy] = None,
|
||||
get_waiter: Optional[Callable[[], asyncio.Future[None]]] = None,
|
||||
) -> AsyncIterator[None]:
|
||||
def writer(
|
||||
task: PregelExecutableTask, writes: Sequence[tuple[str, Any]]
|
||||
) -> None:
|
||||
prev_length = len(task.writes)
|
||||
# delegate to the underlying writer
|
||||
task.config[CONF][CONFIG_KEY_SEND](writes)
|
||||
for idx, w in enumerate(task.writes):
|
||||
# find the index for the newly inserted writes
|
||||
if idx < prev_length:
|
||||
continue
|
||||
assert writes[idx - prev_length] is w
|
||||
# bail if not a PUSH write
|
||||
if w[0] != PUSH:
|
||||
continue
|
||||
# schedule the next task, if the callback returns one
|
||||
if next_task := self.schedule_task(task, idx):
|
||||
# if the parent task was retried,
|
||||
# the next task might already be running
|
||||
if any(
|
||||
t == next_task.id for t in futures.values() if t is not None
|
||||
):
|
||||
continue
|
||||
# schedule the next task
|
||||
futures[
|
||||
cast(
|
||||
asyncio.Future,
|
||||
self.submit(
|
||||
arun_with_retry,
|
||||
next_task,
|
||||
retry_policy,
|
||||
stream=self.use_astream,
|
||||
writer=writer,
|
||||
__name__=t.name,
|
||||
__cancel_on_exit__=True,
|
||||
__reraise_on_exit__=reraise,
|
||||
),
|
||||
)
|
||||
] = next_task
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
tasks = tuple(tasks)
|
||||
futures: dict[asyncio.Future, Optional[PregelExecutableTask]] = {}
|
||||
# give control back to the caller
|
||||
yield
|
||||
# fast path if single task with no waiter and no timeout
|
||||
if len(tasks) == 1 and get_waiter is None and timeout is None:
|
||||
t = tasks[0]
|
||||
try:
|
||||
await arun_with_retry(t, retry_policy, stream=self.use_astream)
|
||||
await arun_with_retry(
|
||||
t, retry_policy, stream=self.use_astream, writer=writer
|
||||
)
|
||||
self.commit(t, None)
|
||||
except Exception as exc:
|
||||
self.commit(t, exc)
|
||||
if reraise:
|
||||
raise
|
||||
return
|
||||
if not futures: # maybe `t` schuduled another task
|
||||
return
|
||||
# add waiter task if requested
|
||||
if get_waiter is not None:
|
||||
futures: dict[asyncio.Future, Optional[PregelExecutableTask]] = {
|
||||
get_waiter(): None
|
||||
}
|
||||
else:
|
||||
futures = {}
|
||||
futures[get_waiter()] = None
|
||||
# execute tasks, and wait for one to fail or all to finish.
|
||||
# each task is independent from all other concurrent tasks
|
||||
# yield updates/debug output as each task finishes
|
||||
@@ -156,13 +244,14 @@ class PregelRunner:
|
||||
t,
|
||||
retry_policy,
|
||||
stream=self.use_astream,
|
||||
writer=writer,
|
||||
__name__=t.name,
|
||||
__cancel_on_exit__=True,
|
||||
__reraise_on_exit__=reraise,
|
||||
),
|
||||
)
|
||||
] = t
|
||||
all_futures = futures.copy()
|
||||
done_futures: set[asyncio.Future] = set()
|
||||
end_time = timeout + loop.time() if timeout else None
|
||||
while len(futures) > (1 if get_waiter is not None else 0):
|
||||
done, inflight = await asyncio.wait(
|
||||
@@ -179,6 +268,8 @@ class PregelRunner:
|
||||
if inflight and get_waiter is not None:
|
||||
futures[get_waiter()] = None
|
||||
else:
|
||||
# store for panic check
|
||||
done_futures.add(fut)
|
||||
# task finished, commit writes
|
||||
self.commit(task, _exception(fut))
|
||||
else:
|
||||
@@ -194,7 +285,9 @@ class PregelRunner:
|
||||
fut.cancel()
|
||||
# panic on failure or timeout
|
||||
_panic_or_proceed(
|
||||
all_futures, timeout_exc_cls=asyncio.TimeoutError, panic=reraise
|
||||
done_futures.union(f for f, t in futures.items() if t is not None),
|
||||
timeout_exc_cls=asyncio.TimeoutError,
|
||||
panic=reraise,
|
||||
)
|
||||
|
||||
def commit(
|
||||
@@ -250,10 +343,7 @@ def _exception(
|
||||
|
||||
|
||||
def _panic_or_proceed(
|
||||
futs: Union[
|
||||
dict[concurrent.futures.Future, Optional[PregelExecutableTask]],
|
||||
dict[asyncio.Future, Optional[PregelExecutableTask]],
|
||||
],
|
||||
futs: Union[set[concurrent.futures.Future], set[asyncio.Future]],
|
||||
*,
|
||||
timeout_exc_cls: Type[Exception] = TimeoutError,
|
||||
panic: bool = True,
|
||||
@@ -261,10 +351,8 @@ def _panic_or_proceed(
|
||||
"""Cancel remaining tasks if any failed, re-raise exception if panic is True."""
|
||||
done: set[Union[concurrent.futures.Future[Any], asyncio.Future[Any]]] = set()
|
||||
inflight: set[Union[concurrent.futures.Future[Any], asyncio.Future[Any]]] = set()
|
||||
for fut, val in futs.items():
|
||||
if val is None:
|
||||
continue
|
||||
elif fut.done():
|
||||
for fut in futs:
|
||||
if fut.done():
|
||||
done.add(fut)
|
||||
else:
|
||||
inflight.add(fut)
|
||||
|
||||
@@ -14,7 +14,7 @@ from typing import (
|
||||
from langchain_core.runnables import Runnable, RunnableConfig
|
||||
from langchain_core.runnables.utils import ConfigurableFieldSpec
|
||||
|
||||
from langgraph.constants import CONF, CONFIG_KEY_SEND, TASKS, Send
|
||||
from langgraph.constants import CONF, CONFIG_KEY_SEND, FF_SEND_V2, PUSH, TASKS, Send
|
||||
from langgraph.errors import InvalidUpdateError
|
||||
from langgraph.utils.runnable import RunnableCallable
|
||||
|
||||
@@ -112,14 +112,18 @@ class ChannelWrite(RunnableCallable):
|
||||
# validate
|
||||
for w in writes:
|
||||
if isinstance(w, ChannelWriteEntry):
|
||||
if w.channel == TASKS:
|
||||
if w.channel in (TASKS, PUSH):
|
||||
raise InvalidUpdateError(
|
||||
"Cannot write to the reserved channel TASKS"
|
||||
)
|
||||
if w.value is PASSTHROUGH:
|
||||
raise InvalidUpdateError("PASSTHROUGH value must be replaced")
|
||||
# split packets and entries
|
||||
sends = [(TASKS, packet) for packet in writes if isinstance(packet, Send)]
|
||||
sends = [
|
||||
(PUSH if FF_SEND_V2 else TASKS, packet)
|
||||
for packet in writes
|
||||
if isinstance(packet, Send)
|
||||
]
|
||||
entries = [write for write in writes if isinstance(write, ChannelWriteEntry)]
|
||||
# process entries into values
|
||||
values = [
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import dataclasses
|
||||
import sys
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Generic,
|
||||
Hashable,
|
||||
Literal,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Sequence,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
@@ -44,6 +48,11 @@ StreamWriter = Callable[[Any], None]
|
||||
Always injected into nodes if requested as a keyword argument, but it's a no-op
|
||||
when not using stream_mode="custom"."""
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
_DC_KWARGS = {"kw_only": True, "slots": True, "frozen": True}
|
||||
else:
|
||||
_DC_KWARGS = {"frozen": True}
|
||||
|
||||
|
||||
def default_retry_on(exc: Exception) -> bool:
|
||||
import httpx
|
||||
@@ -101,16 +110,18 @@ class CachePolicy(NamedTuple):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclasses.dataclass(**_DC_KWARGS)
|
||||
class Interrupt:
|
||||
value: Any
|
||||
resumable: bool = False
|
||||
ns: Optional[Sequence[str]] = None
|
||||
when: Literal["during"] = "during"
|
||||
|
||||
|
||||
class PregelTask(NamedTuple):
|
||||
id: str
|
||||
name: str
|
||||
path: tuple[Union[str, int], ...]
|
||||
path: tuple[Union[str, int, tuple], ...]
|
||||
error: Optional[Exception] = None
|
||||
interrupts: tuple[Interrupt, ...] = ()
|
||||
state: Union[None, RunnableConfig, "StateSnapshot"] = None
|
||||
@@ -127,7 +138,7 @@ class PregelExecutableTask(NamedTuple):
|
||||
retry_policy: Optional[RetryPolicy]
|
||||
cache_policy: Optional[CachePolicy]
|
||||
id: str
|
||||
path: tuple[Union[str, int], ...]
|
||||
path: tuple[Union[str, int, tuple], ...]
|
||||
scheduled: bool = False
|
||||
|
||||
|
||||
@@ -221,6 +232,27 @@ class Send:
|
||||
)
|
||||
|
||||
|
||||
N = TypeVar("N", bound=Hashable)
|
||||
|
||||
|
||||
@dataclasses.dataclass(**_DC_KWARGS)
|
||||
class Command(Generic[N]):
|
||||
"""One or more commands to update the graph's state and send messages to nodes."""
|
||||
|
||||
update: Optional[dict[str, Any]] = None
|
||||
send: Union[Send, Sequence[Send]] = ()
|
||||
resume: Optional[Union[Any, dict[str, Any]]] = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
# get all non-None values
|
||||
contents = ", ".join(
|
||||
f"{key}={value!r}"
|
||||
for key, value in dataclasses.asdict(self).items()
|
||||
if value
|
||||
)
|
||||
return f"Command({contents})"
|
||||
|
||||
|
||||
StreamChunk = tuple[tuple[str, ...], str, Any]
|
||||
|
||||
|
||||
@@ -261,3 +293,28 @@ class LoopProtocol:
|
||||
self.store = store
|
||||
self.step = step
|
||||
self.stop = stop
|
||||
|
||||
|
||||
def interrupt(value: Any) -> Any:
|
||||
from langgraph.constants import (
|
||||
CONFIG_KEY_CHECKPOINT_NS,
|
||||
CONFIG_KEY_RESUME_VALUE,
|
||||
MISSING,
|
||||
NS_SEP,
|
||||
)
|
||||
from langgraph.errors import GraphInterrupt
|
||||
from langgraph.utils.config import get_configurable
|
||||
|
||||
conf = get_configurable()
|
||||
if (resume := conf.get(CONFIG_KEY_RESUME_VALUE, MISSING)) and resume is not MISSING:
|
||||
return resume
|
||||
else:
|
||||
raise GraphInterrupt(
|
||||
(
|
||||
Interrupt(
|
||||
value=value,
|
||||
resumable=True,
|
||||
ns=cast(str, conf[CONFIG_KEY_CHECKPOINT_NS]).split(NS_SEP),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import asyncio
|
||||
import sys
|
||||
from collections import ChainMap
|
||||
from typing import Any, Optional, Sequence
|
||||
|
||||
@@ -290,3 +292,18 @@ def ensure_config(*configs: Optional[RunnableConfig]) -> RunnableConfig:
|
||||
):
|
||||
empty["metadata"][key] = value
|
||||
return empty
|
||||
|
||||
|
||||
def get_configurable() -> dict[str, Any]:
|
||||
if sys.version_info < (3, 11):
|
||||
try:
|
||||
if asyncio.current_task():
|
||||
raise RuntimeError(
|
||||
"Python 3.11 or later required to use this in an async context"
|
||||
)
|
||||
except RuntimeError:
|
||||
pass
|
||||
if var_config := var_child_runnable_config.get():
|
||||
return var_config[CONF]
|
||||
else:
|
||||
raise RuntimeError("Called get_configurable outside of a runnable context")
|
||||
|
||||
Generated
+3
-3
@@ -1359,7 +1359,7 @@ typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.2"
|
||||
version = "2.0.4"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -1429,7 +1429,7 @@ url = "../checkpoint-sqlite"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.1.35"
|
||||
version = "0.1.36"
|
||||
description = "SDK for interacting with LangGraph API"
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -3425,4 +3425,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.9.0,<4.0"
|
||||
content-hash = "f0b7800f90041227ba4d7b72482c70fabdbacc95e8a3a784fdebc88721728a91"
|
||||
content-hash = "9bf5668d3f70f3b77457906732404a6401583a5966f70a72ef10a68f2a5b27ad"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph"
|
||||
version = "0.2.44"
|
||||
version = "0.2.50"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -10,7 +10,7 @@ repository = "https://www.github.com/langchain-ai/langgraph"
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9.0,<4.0"
|
||||
langchain-core = ">=0.2.43,<0.4.0,!=0.3.0,!=0.3.1,!=0.3.2,!=0.3.3,!=0.3.4,!=0.3.5,!=0.3.6,!=0.3.7,!=0.3.8,!=0.3.9,!=0.3.10,!=0.3.11,!=0.3.12,!=0.3.13,!=0.3.14"
|
||||
langgraph-checkpoint = "^2.0.0"
|
||||
langgraph-checkpoint = "^2.0.4"
|
||||
langgraph-sdk = "^0.1.32"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
||||
@@ -840,65 +840,16 @@
|
||||
"data": "__start__"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "schema",
|
||||
"data": "Parallel<agent_outcome>Input"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "schema",
|
||||
"data": "Parallel<agent_outcome>Output"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate"
|
||||
],
|
||||
"name": "PromptTemplate"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"language_models",
|
||||
"fake",
|
||||
"FakeStreamingListLLM"
|
||||
],
|
||||
"name": "FakeStreamingListLLM"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"runnables",
|
||||
"base",
|
||||
"RunnableLambda"
|
||||
],
|
||||
"name": "agent_parser"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"id": "agent",
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain",
|
||||
"schema",
|
||||
"runnable",
|
||||
"RunnablePassthrough"
|
||||
"RunnableAssign"
|
||||
],
|
||||
"name": "Passthrough"
|
||||
"name": "agent"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -926,46 +877,22 @@
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": 3,
|
||||
"target": 4
|
||||
},
|
||||
{
|
||||
"source": 4,
|
||||
"target": 5
|
||||
},
|
||||
{
|
||||
"source": 1,
|
||||
"target": 3
|
||||
},
|
||||
{
|
||||
"source": 5,
|
||||
"target": 2
|
||||
},
|
||||
{
|
||||
"source": 1,
|
||||
"target": 6
|
||||
},
|
||||
{
|
||||
"source": 6,
|
||||
"target": 2
|
||||
},
|
||||
{
|
||||
"source": "__start__",
|
||||
"target": 1
|
||||
"target": "agent"
|
||||
},
|
||||
{
|
||||
"source": "tools",
|
||||
"target": 1
|
||||
"target": "agent"
|
||||
},
|
||||
{
|
||||
"source": 2,
|
||||
"source": "agent",
|
||||
"target": "tools",
|
||||
"data": "continue",
|
||||
"conditional": true
|
||||
},
|
||||
{
|
||||
"source": 2,
|
||||
"source": "agent",
|
||||
"target": "__end__",
|
||||
"data": "exit",
|
||||
"conditional": true
|
||||
@@ -977,16 +904,10 @@
|
||||
# name: test_conditional_graph[duckdb].4
|
||||
'''
|
||||
graph TD;
|
||||
PromptTemplate --> FakeStreamingListLLM;
|
||||
FakeStreamingListLLM --> agent_parser;
|
||||
Parallel_agent_outcome_Input --> PromptTemplate;
|
||||
agent_parser --> Parallel_agent_outcome_Output;
|
||||
Parallel_agent_outcome_Input --> Passthrough;
|
||||
Passthrough --> Parallel_agent_outcome_Output;
|
||||
__start__ --> Parallel_agent_outcome_Input;
|
||||
tools --> Parallel_agent_outcome_Input;
|
||||
Parallel_agent_outcome_Output -. continue .-> tools;
|
||||
Parallel_agent_outcome_Output -. exit .-> __end__;
|
||||
__start__ --> agent;
|
||||
tools --> agent;
|
||||
agent -. continue .-> tools;
|
||||
agent -. exit .-> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
@@ -1194,65 +1115,16 @@
|
||||
"data": "__start__"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "schema",
|
||||
"data": "Parallel<agent_outcome>Input"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "schema",
|
||||
"data": "Parallel<agent_outcome>Output"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate"
|
||||
],
|
||||
"name": "PromptTemplate"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"language_models",
|
||||
"fake",
|
||||
"FakeStreamingListLLM"
|
||||
],
|
||||
"name": "FakeStreamingListLLM"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"runnables",
|
||||
"base",
|
||||
"RunnableLambda"
|
||||
],
|
||||
"name": "agent_parser"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"id": "agent",
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain",
|
||||
"schema",
|
||||
"runnable",
|
||||
"RunnablePassthrough"
|
||||
"RunnableAssign"
|
||||
],
|
||||
"name": "Passthrough"
|
||||
"name": "agent"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1280,46 +1152,22 @@
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": 3,
|
||||
"target": 4
|
||||
},
|
||||
{
|
||||
"source": 4,
|
||||
"target": 5
|
||||
},
|
||||
{
|
||||
"source": 1,
|
||||
"target": 3
|
||||
},
|
||||
{
|
||||
"source": 5,
|
||||
"target": 2
|
||||
},
|
||||
{
|
||||
"source": 1,
|
||||
"target": 6
|
||||
},
|
||||
{
|
||||
"source": 6,
|
||||
"target": 2
|
||||
},
|
||||
{
|
||||
"source": "__start__",
|
||||
"target": 1
|
||||
"target": "agent"
|
||||
},
|
||||
{
|
||||
"source": "tools",
|
||||
"target": 1
|
||||
"target": "agent"
|
||||
},
|
||||
{
|
||||
"source": 2,
|
||||
"source": "agent",
|
||||
"target": "tools",
|
||||
"data": "continue",
|
||||
"conditional": true
|
||||
},
|
||||
{
|
||||
"source": 2,
|
||||
"source": "agent",
|
||||
"target": "__end__",
|
||||
"data": "exit",
|
||||
"conditional": true
|
||||
@@ -1331,16 +1179,10 @@
|
||||
# name: test_conditional_graph[memory].4
|
||||
'''
|
||||
graph TD;
|
||||
PromptTemplate --> FakeStreamingListLLM;
|
||||
FakeStreamingListLLM --> agent_parser;
|
||||
Parallel_agent_outcome_Input --> PromptTemplate;
|
||||
agent_parser --> Parallel_agent_outcome_Output;
|
||||
Parallel_agent_outcome_Input --> Passthrough;
|
||||
Passthrough --> Parallel_agent_outcome_Output;
|
||||
__start__ --> Parallel_agent_outcome_Input;
|
||||
tools --> Parallel_agent_outcome_Input;
|
||||
Parallel_agent_outcome_Output -. continue .-> tools;
|
||||
Parallel_agent_outcome_Output -. exit .-> __end__;
|
||||
__start__ --> agent;
|
||||
tools --> agent;
|
||||
agent -. continue .-> tools;
|
||||
agent -. exit .-> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
@@ -1548,65 +1390,16 @@
|
||||
"data": "__start__"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "schema",
|
||||
"data": "Parallel<agent_outcome>Input"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "schema",
|
||||
"data": "Parallel<agent_outcome>Output"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate"
|
||||
],
|
||||
"name": "PromptTemplate"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"language_models",
|
||||
"fake",
|
||||
"FakeStreamingListLLM"
|
||||
],
|
||||
"name": "FakeStreamingListLLM"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"runnables",
|
||||
"base",
|
||||
"RunnableLambda"
|
||||
],
|
||||
"name": "agent_parser"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"id": "agent",
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain",
|
||||
"schema",
|
||||
"runnable",
|
||||
"RunnablePassthrough"
|
||||
"RunnableAssign"
|
||||
],
|
||||
"name": "Passthrough"
|
||||
"name": "agent"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1634,46 +1427,22 @@
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": 3,
|
||||
"target": 4
|
||||
},
|
||||
{
|
||||
"source": 4,
|
||||
"target": 5
|
||||
},
|
||||
{
|
||||
"source": 1,
|
||||
"target": 3
|
||||
},
|
||||
{
|
||||
"source": 5,
|
||||
"target": 2
|
||||
},
|
||||
{
|
||||
"source": 1,
|
||||
"target": 6
|
||||
},
|
||||
{
|
||||
"source": 6,
|
||||
"target": 2
|
||||
},
|
||||
{
|
||||
"source": "__start__",
|
||||
"target": 1
|
||||
"target": "agent"
|
||||
},
|
||||
{
|
||||
"source": "tools",
|
||||
"target": 1
|
||||
"target": "agent"
|
||||
},
|
||||
{
|
||||
"source": 2,
|
||||
"source": "agent",
|
||||
"target": "tools",
|
||||
"data": "continue",
|
||||
"conditional": true
|
||||
},
|
||||
{
|
||||
"source": 2,
|
||||
"source": "agent",
|
||||
"target": "__end__",
|
||||
"data": "exit",
|
||||
"conditional": true
|
||||
@@ -1685,16 +1454,10 @@
|
||||
# name: test_conditional_graph[postgres].4
|
||||
'''
|
||||
graph TD;
|
||||
PromptTemplate --> FakeStreamingListLLM;
|
||||
FakeStreamingListLLM --> agent_parser;
|
||||
Parallel_agent_outcome_Input --> PromptTemplate;
|
||||
agent_parser --> Parallel_agent_outcome_Output;
|
||||
Parallel_agent_outcome_Input --> Passthrough;
|
||||
Passthrough --> Parallel_agent_outcome_Output;
|
||||
__start__ --> Parallel_agent_outcome_Input;
|
||||
tools --> Parallel_agent_outcome_Input;
|
||||
Parallel_agent_outcome_Output -. continue .-> tools;
|
||||
Parallel_agent_outcome_Output -. exit .-> __end__;
|
||||
__start__ --> agent;
|
||||
tools --> agent;
|
||||
agent -. continue .-> tools;
|
||||
agent -. exit .-> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
@@ -1902,65 +1665,16 @@
|
||||
"data": "__start__"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "schema",
|
||||
"data": "Parallel<agent_outcome>Input"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "schema",
|
||||
"data": "Parallel<agent_outcome>Output"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate"
|
||||
],
|
||||
"name": "PromptTemplate"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"language_models",
|
||||
"fake",
|
||||
"FakeStreamingListLLM"
|
||||
],
|
||||
"name": "FakeStreamingListLLM"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"runnables",
|
||||
"base",
|
||||
"RunnableLambda"
|
||||
],
|
||||
"name": "agent_parser"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"id": "agent",
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain",
|
||||
"schema",
|
||||
"runnable",
|
||||
"RunnablePassthrough"
|
||||
"RunnableAssign"
|
||||
],
|
||||
"name": "Passthrough"
|
||||
"name": "agent"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1988,46 +1702,22 @@
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": 3,
|
||||
"target": 4
|
||||
},
|
||||
{
|
||||
"source": 4,
|
||||
"target": 5
|
||||
},
|
||||
{
|
||||
"source": 1,
|
||||
"target": 3
|
||||
},
|
||||
{
|
||||
"source": 5,
|
||||
"target": 2
|
||||
},
|
||||
{
|
||||
"source": 1,
|
||||
"target": 6
|
||||
},
|
||||
{
|
||||
"source": 6,
|
||||
"target": 2
|
||||
},
|
||||
{
|
||||
"source": "__start__",
|
||||
"target": 1
|
||||
"target": "agent"
|
||||
},
|
||||
{
|
||||
"source": "tools",
|
||||
"target": 1
|
||||
"target": "agent"
|
||||
},
|
||||
{
|
||||
"source": 2,
|
||||
"source": "agent",
|
||||
"target": "tools",
|
||||
"data": "continue",
|
||||
"conditional": true
|
||||
},
|
||||
{
|
||||
"source": 2,
|
||||
"source": "agent",
|
||||
"target": "__end__",
|
||||
"data": "exit",
|
||||
"conditional": true
|
||||
@@ -2039,16 +1729,10 @@
|
||||
# name: test_conditional_graph[postgres_pipe].4
|
||||
'''
|
||||
graph TD;
|
||||
PromptTemplate --> FakeStreamingListLLM;
|
||||
FakeStreamingListLLM --> agent_parser;
|
||||
Parallel_agent_outcome_Input --> PromptTemplate;
|
||||
agent_parser --> Parallel_agent_outcome_Output;
|
||||
Parallel_agent_outcome_Input --> Passthrough;
|
||||
Passthrough --> Parallel_agent_outcome_Output;
|
||||
__start__ --> Parallel_agent_outcome_Input;
|
||||
tools --> Parallel_agent_outcome_Input;
|
||||
Parallel_agent_outcome_Output -. continue .-> tools;
|
||||
Parallel_agent_outcome_Output -. exit .-> __end__;
|
||||
__start__ --> agent;
|
||||
tools --> agent;
|
||||
agent -. continue .-> tools;
|
||||
agent -. exit .-> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
@@ -2256,65 +1940,16 @@
|
||||
"data": "__start__"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "schema",
|
||||
"data": "Parallel<agent_outcome>Input"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "schema",
|
||||
"data": "Parallel<agent_outcome>Output"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate"
|
||||
],
|
||||
"name": "PromptTemplate"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"language_models",
|
||||
"fake",
|
||||
"FakeStreamingListLLM"
|
||||
],
|
||||
"name": "FakeStreamingListLLM"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"runnables",
|
||||
"base",
|
||||
"RunnableLambda"
|
||||
],
|
||||
"name": "agent_parser"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"id": "agent",
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain",
|
||||
"schema",
|
||||
"runnable",
|
||||
"RunnablePassthrough"
|
||||
"RunnableAssign"
|
||||
],
|
||||
"name": "Passthrough"
|
||||
"name": "agent"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -2342,46 +1977,22 @@
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": 3,
|
||||
"target": 4
|
||||
},
|
||||
{
|
||||
"source": 4,
|
||||
"target": 5
|
||||
},
|
||||
{
|
||||
"source": 1,
|
||||
"target": 3
|
||||
},
|
||||
{
|
||||
"source": 5,
|
||||
"target": 2
|
||||
},
|
||||
{
|
||||
"source": 1,
|
||||
"target": 6
|
||||
},
|
||||
{
|
||||
"source": 6,
|
||||
"target": 2
|
||||
},
|
||||
{
|
||||
"source": "__start__",
|
||||
"target": 1
|
||||
"target": "agent"
|
||||
},
|
||||
{
|
||||
"source": "tools",
|
||||
"target": 1
|
||||
"target": "agent"
|
||||
},
|
||||
{
|
||||
"source": 2,
|
||||
"source": "agent",
|
||||
"target": "tools",
|
||||
"data": "continue",
|
||||
"conditional": true
|
||||
},
|
||||
{
|
||||
"source": 2,
|
||||
"source": "agent",
|
||||
"target": "__end__",
|
||||
"data": "exit",
|
||||
"conditional": true
|
||||
@@ -2393,16 +2004,10 @@
|
||||
# name: test_conditional_graph[postgres_pool].4
|
||||
'''
|
||||
graph TD;
|
||||
PromptTemplate --> FakeStreamingListLLM;
|
||||
FakeStreamingListLLM --> agent_parser;
|
||||
Parallel_agent_outcome_Input --> PromptTemplate;
|
||||
agent_parser --> Parallel_agent_outcome_Output;
|
||||
Parallel_agent_outcome_Input --> Passthrough;
|
||||
Passthrough --> Parallel_agent_outcome_Output;
|
||||
__start__ --> Parallel_agent_outcome_Input;
|
||||
tools --> Parallel_agent_outcome_Input;
|
||||
Parallel_agent_outcome_Output -. continue .-> tools;
|
||||
Parallel_agent_outcome_Output -. exit .-> __end__;
|
||||
__start__ --> agent;
|
||||
tools --> agent;
|
||||
agent -. continue .-> tools;
|
||||
agent -. exit .-> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
@@ -2610,65 +2215,16 @@
|
||||
"data": "__start__"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "schema",
|
||||
"data": "Parallel<agent_outcome>Input"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "schema",
|
||||
"data": "Parallel<agent_outcome>Output"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain",
|
||||
"prompts",
|
||||
"prompt",
|
||||
"PromptTemplate"
|
||||
],
|
||||
"name": "PromptTemplate"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"language_models",
|
||||
"fake",
|
||||
"FakeStreamingListLLM"
|
||||
],
|
||||
"name": "FakeStreamingListLLM"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain_core",
|
||||
"runnables",
|
||||
"base",
|
||||
"RunnableLambda"
|
||||
],
|
||||
"name": "agent_parser"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"id": "agent",
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langchain",
|
||||
"schema",
|
||||
"runnable",
|
||||
"RunnablePassthrough"
|
||||
"RunnableAssign"
|
||||
],
|
||||
"name": "Passthrough"
|
||||
"name": "agent"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -2696,46 +2252,22 @@
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": 3,
|
||||
"target": 4
|
||||
},
|
||||
{
|
||||
"source": 4,
|
||||
"target": 5
|
||||
},
|
||||
{
|
||||
"source": 1,
|
||||
"target": 3
|
||||
},
|
||||
{
|
||||
"source": 5,
|
||||
"target": 2
|
||||
},
|
||||
{
|
||||
"source": 1,
|
||||
"target": 6
|
||||
},
|
||||
{
|
||||
"source": 6,
|
||||
"target": 2
|
||||
},
|
||||
{
|
||||
"source": "__start__",
|
||||
"target": 1
|
||||
"target": "agent"
|
||||
},
|
||||
{
|
||||
"source": "tools",
|
||||
"target": 1
|
||||
"target": "agent"
|
||||
},
|
||||
{
|
||||
"source": 2,
|
||||
"source": "agent",
|
||||
"target": "tools",
|
||||
"data": "continue",
|
||||
"conditional": true
|
||||
},
|
||||
{
|
||||
"source": 2,
|
||||
"source": "agent",
|
||||
"target": "__end__",
|
||||
"data": "exit",
|
||||
"conditional": true
|
||||
@@ -2747,16 +2279,10 @@
|
||||
# name: test_conditional_graph[sqlite].4
|
||||
'''
|
||||
graph TD;
|
||||
PromptTemplate --> FakeStreamingListLLM;
|
||||
FakeStreamingListLLM --> agent_parser;
|
||||
Parallel_agent_outcome_Input --> PromptTemplate;
|
||||
agent_parser --> Parallel_agent_outcome_Output;
|
||||
Parallel_agent_outcome_Input --> Passthrough;
|
||||
Passthrough --> Parallel_agent_outcome_Output;
|
||||
__start__ --> Parallel_agent_outcome_Input;
|
||||
tools --> Parallel_agent_outcome_Input;
|
||||
Parallel_agent_outcome_Output -. continue .-> tools;
|
||||
Parallel_agent_outcome_Output -. exit .-> __end__;
|
||||
__start__ --> agent;
|
||||
tools --> agent;
|
||||
agent -. continue .-> tools;
|
||||
agent -. exit .-> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
@@ -5582,6 +5108,81 @@
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_send_react_interrupt_control[memory]
|
||||
'''
|
||||
%%{init: {'flowchart': {'curve': 'linear'}}}%%
|
||||
graph TD;
|
||||
__start__([<p>__start__</p>]):::first
|
||||
agent(agent)
|
||||
foo([foo]):::last
|
||||
__start__ --> agent;
|
||||
agent -.-> foo;
|
||||
classDef default fill:#f2f0ff,line-height:1.2
|
||||
classDef first fill-opacity:0
|
||||
classDef last fill:#bfb6fc
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_send_react_interrupt_control[postgres]
|
||||
'''
|
||||
%%{init: {'flowchart': {'curve': 'linear'}}}%%
|
||||
graph TD;
|
||||
__start__([<p>__start__</p>]):::first
|
||||
agent(agent)
|
||||
foo([foo]):::last
|
||||
__start__ --> agent;
|
||||
agent -.-> foo;
|
||||
classDef default fill:#f2f0ff,line-height:1.2
|
||||
classDef first fill-opacity:0
|
||||
classDef last fill:#bfb6fc
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_send_react_interrupt_control[postgres_pipe]
|
||||
'''
|
||||
%%{init: {'flowchart': {'curve': 'linear'}}}%%
|
||||
graph TD;
|
||||
__start__([<p>__start__</p>]):::first
|
||||
agent(agent)
|
||||
foo([foo]):::last
|
||||
__start__ --> agent;
|
||||
agent -.-> foo;
|
||||
classDef default fill:#f2f0ff,line-height:1.2
|
||||
classDef first fill-opacity:0
|
||||
classDef last fill:#bfb6fc
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_send_react_interrupt_control[postgres_pool]
|
||||
'''
|
||||
%%{init: {'flowchart': {'curve': 'linear'}}}%%
|
||||
graph TD;
|
||||
__start__([<p>__start__</p>]):::first
|
||||
agent(agent)
|
||||
foo([foo]):::last
|
||||
__start__ --> agent;
|
||||
agent -.-> foo;
|
||||
classDef default fill:#f2f0ff,line-height:1.2
|
||||
classDef first fill-opacity:0
|
||||
classDef last fill:#bfb6fc
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_send_react_interrupt_control[sqlite]
|
||||
'''
|
||||
%%{init: {'flowchart': {'curve': 'linear'}}}%%
|
||||
graph TD;
|
||||
__start__([<p>__start__</p>]):::first
|
||||
agent(agent)
|
||||
foo([foo]):::last
|
||||
__start__ --> agent;
|
||||
agent -.-> foo;
|
||||
classDef default fill:#f2f0ff,line-height:1.2
|
||||
classDef first fill-opacity:0
|
||||
classDef last fill:#bfb6fc
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_simple_multi_edge
|
||||
'''
|
||||
graph TD;
|
||||
|
||||
@@ -1302,6 +1302,81 @@
|
||||
+---------+
|
||||
'''
|
||||
# ---
|
||||
# name: test_send_react_interrupt_control[memory]
|
||||
'''
|
||||
%%{init: {'flowchart': {'curve': 'linear'}}}%%
|
||||
graph TD;
|
||||
__start__([<p>__start__</p>]):::first
|
||||
agent(agent)
|
||||
foo([foo]):::last
|
||||
__start__ --> agent;
|
||||
agent -.-> foo;
|
||||
classDef default fill:#f2f0ff,line-height:1.2
|
||||
classDef first fill-opacity:0
|
||||
classDef last fill:#bfb6fc
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_send_react_interrupt_control[postgres_aio]
|
||||
'''
|
||||
%%{init: {'flowchart': {'curve': 'linear'}}}%%
|
||||
graph TD;
|
||||
__start__([<p>__start__</p>]):::first
|
||||
agent(agent)
|
||||
foo([foo]):::last
|
||||
__start__ --> agent;
|
||||
agent -.-> foo;
|
||||
classDef default fill:#f2f0ff,line-height:1.2
|
||||
classDef first fill-opacity:0
|
||||
classDef last fill:#bfb6fc
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_send_react_interrupt_control[postgres_aio_pipe]
|
||||
'''
|
||||
%%{init: {'flowchart': {'curve': 'linear'}}}%%
|
||||
graph TD;
|
||||
__start__([<p>__start__</p>]):::first
|
||||
agent(agent)
|
||||
foo([foo]):::last
|
||||
__start__ --> agent;
|
||||
agent -.-> foo;
|
||||
classDef default fill:#f2f0ff,line-height:1.2
|
||||
classDef first fill-opacity:0
|
||||
classDef last fill:#bfb6fc
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_send_react_interrupt_control[postgres_aio_pool]
|
||||
'''
|
||||
%%{init: {'flowchart': {'curve': 'linear'}}}%%
|
||||
graph TD;
|
||||
__start__([<p>__start__</p>]):::first
|
||||
agent(agent)
|
||||
foo([foo]):::last
|
||||
__start__ --> agent;
|
||||
agent -.-> foo;
|
||||
classDef default fill:#f2f0ff,line-height:1.2
|
||||
classDef first fill-opacity:0
|
||||
classDef last fill:#bfb6fc
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_send_react_interrupt_control[sqlite_aio]
|
||||
'''
|
||||
%%{init: {'flowchart': {'curve': 'linear'}}}%%
|
||||
graph TD;
|
||||
__start__([<p>__start__</p>]):::first
|
||||
agent(agent)
|
||||
foo([foo]):::last
|
||||
__start__ --> agent;
|
||||
agent -.-> foo;
|
||||
classDef default fill:#f2f0ff,line-height:1.2
|
||||
classDef first fill-opacity:0
|
||||
classDef last fill:#bfb6fc
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_weather_subgraph[duckdb_aio]
|
||||
'''
|
||||
%%{init: {'flowchart': {'curve': 'linear'}}}%%
|
||||
|
||||
@@ -21,7 +21,8 @@ from langgraph.store.base import BaseStore
|
||||
from langgraph.store.duckdb import AsyncDuckDBStore, DuckDBStore
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
from langgraph.store.postgres import AsyncPostgresStore, PostgresStore
|
||||
from tests.memory_assert import MemorySaverAssertImmutable
|
||||
|
||||
pytest.register_assert_rewrite("tests.memory_assert")
|
||||
|
||||
DEFAULT_POSTGRES_URI = "postgres://postgres:postgres@localhost:5442/"
|
||||
# TODO: fix this once core is released
|
||||
@@ -49,6 +50,8 @@ def deterministic_uuids(mocker: MockerFixture) -> MockerFixture:
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_memory():
|
||||
from tests.memory_assert import MemorySaverAssertImmutable
|
||||
|
||||
yield MemorySaverAssertImmutable()
|
||||
|
||||
|
||||
@@ -225,6 +228,8 @@ async def awith_checkpointer(
|
||||
if checkpointer_name is None:
|
||||
yield None
|
||||
elif checkpointer_name == "memory":
|
||||
from tests.memory_assert import MemorySaverAssertImmutable
|
||||
|
||||
yield MemorySaverAssertImmutable()
|
||||
elif checkpointer_name == "sqlite_aio":
|
||||
async with _checkpointer_sqlite_aio() as checkpointer:
|
||||
@@ -322,7 +327,6 @@ async def awith_store(store_name: Optional[str]) -> AsyncIterator[BaseStore]:
|
||||
ALL_CHECKPOINTERS_SYNC = [
|
||||
"memory",
|
||||
"sqlite",
|
||||
"duckdb",
|
||||
"postgres",
|
||||
"postgres_pipe",
|
||||
"postgres_pool",
|
||||
@@ -330,7 +334,6 @@ ALL_CHECKPOINTERS_SYNC = [
|
||||
ALL_CHECKPOINTERS_ASYNC = [
|
||||
"memory",
|
||||
"sqlite_aio",
|
||||
"duckdb_aio",
|
||||
"postgres_aio",
|
||||
"postgres_aio_pipe",
|
||||
"postgres_aio_pool",
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import asyncio
|
||||
import os
|
||||
import tempfile
|
||||
from collections import defaultdict
|
||||
from functools import partial
|
||||
from typing import Any, Optional
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
@@ -12,7 +15,7 @@ from langgraph.checkpoint.base import (
|
||||
SerializerProtocol,
|
||||
copy_checkpoint,
|
||||
)
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
from langgraph.checkpoint.memory import MemorySaver, PersistentDict
|
||||
|
||||
|
||||
class NoopSerializer(SerializerProtocol):
|
||||
@@ -32,9 +35,13 @@ class MemorySaverAssertImmutable(MemorySaver):
|
||||
serde: Optional[SerializerProtocol] = None,
|
||||
put_sleep: Optional[float] = None,
|
||||
) -> None:
|
||||
super().__init__(serde=serde)
|
||||
_, filename = tempfile.mkstemp()
|
||||
super().__init__(
|
||||
serde=serde, factory=partial(PersistentDict, filename=filename)
|
||||
)
|
||||
self.storage_for_copies = defaultdict(lambda: defaultdict(dict))
|
||||
self.put_sleep = put_sleep
|
||||
self.stack.callback(os.remove, filename)
|
||||
|
||||
def put(
|
||||
self,
|
||||
|
||||
@@ -11,13 +11,21 @@ def test_prepare_next_tasks() -> None:
|
||||
with ChannelsManager({}, checkpoint, config) as (channels, managed):
|
||||
assert (
|
||||
prepare_next_tasks(
|
||||
checkpoint, processes, channels, managed, config, 0, for_execution=False
|
||||
checkpoint,
|
||||
{},
|
||||
processes,
|
||||
channels,
|
||||
managed,
|
||||
config,
|
||||
0,
|
||||
for_execution=False,
|
||||
)
|
||||
== {}
|
||||
)
|
||||
assert (
|
||||
prepare_next_tasks(
|
||||
checkpoint,
|
||||
{},
|
||||
processes,
|
||||
channels,
|
||||
managed,
|
||||
|
||||
@@ -158,6 +158,7 @@ def test_no_modifier(request: pytest.FixtureRequest, checkpointer_name: str) ->
|
||||
"source": "loop",
|
||||
"writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}},
|
||||
"step": 1,
|
||||
"thread_id": "123",
|
||||
}
|
||||
assert saved.pending_writes == []
|
||||
|
||||
@@ -189,6 +190,7 @@ async def test_no_modifier_async(checkpointer_name: str) -> None:
|
||||
"source": "loop",
|
||||
"writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}},
|
||||
"step": 1,
|
||||
"thread_id": "123",
|
||||
}
|
||||
assert saved.pending_writes == []
|
||||
|
||||
|
||||
+2500
-143
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -73,7 +73,9 @@ def test_get_graph():
|
||||
"__start__": DrawableNode(
|
||||
id="__start__", name="__start__", data="__start__", metadata=None
|
||||
),
|
||||
"__end__": DrawableNode(id="__end__", name="__end__", data="__end__", metadata=None),
|
||||
"__end__": DrawableNode(
|
||||
id="__end__", name="__end__", data="__end__", metadata=None
|
||||
),
|
||||
"agent": DrawableNode(
|
||||
id="agent",
|
||||
name="agent_1",
|
||||
@@ -120,7 +122,9 @@ async def test_aget_graph():
|
||||
"__start__": DrawableNode(
|
||||
id="__start__", name="__start__", data="__start__", metadata=None
|
||||
),
|
||||
"__end__": DrawableNode(id="__end__", name="__end__", data="__end__", metadata=None),
|
||||
"__end__": DrawableNode(
|
||||
id="__end__", name="__end__", data="__end__", metadata=None
|
||||
),
|
||||
"agent": DrawableNode(
|
||||
id="agent",
|
||||
name="agent_1",
|
||||
|
||||
@@ -5,11 +5,11 @@ from typing import Annotated as Annotated2
|
||||
from typing import Any, Optional
|
||||
|
||||
import pytest
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langchain_core.runnables import RunnableConfig, RunnableLambda
|
||||
from pydantic.v1 import BaseModel
|
||||
from typing_extensions import Annotated, NotRequired, Required, TypedDict
|
||||
|
||||
from langgraph.graph.state import StateGraph, _warn_invalid_state_schema
|
||||
from langgraph.graph.state import StateGraph, _get_node_name, _warn_invalid_state_schema
|
||||
from langgraph.managed.shared_value import SharedValue
|
||||
|
||||
|
||||
@@ -286,3 +286,35 @@ def test_raises_invalid_managed():
|
||||
match="Invalid managed channels detected in BadOutputState: some_output_channel. Managed channels are not permitted in Input/Output schema.",
|
||||
):
|
||||
StateGraph(_state, input=_inp, output=_outp)
|
||||
|
||||
|
||||
def test__get_node_name() -> None:
|
||||
# default runnable name
|
||||
assert _get_node_name(RunnableLambda(func=lambda x: x)) == "RunnableLambda"
|
||||
# custom runnable name
|
||||
assert (
|
||||
_get_node_name(RunnableLambda(name="my_runnable", func=lambda x: x))
|
||||
== "my_runnable"
|
||||
)
|
||||
|
||||
# lambda
|
||||
assert _get_node_name(lambda x: x) == "<lambda>"
|
||||
|
||||
# regular function
|
||||
def func(state):
|
||||
return
|
||||
|
||||
assert _get_node_name(func) == "func"
|
||||
|
||||
class MyClass:
|
||||
def __call__(self, state):
|
||||
return
|
||||
|
||||
def class_method(self, state):
|
||||
return
|
||||
|
||||
# callable class
|
||||
assert _get_node_name(MyClass()) == "MyClass"
|
||||
|
||||
# class method
|
||||
assert _get_node_name(MyClass().class_method) == "class_method"
|
||||
|
||||
@@ -19,7 +19,7 @@ test:
|
||||
exit $$EXIT_CODE
|
||||
|
||||
test_watch:
|
||||
make start-services && poetry run ptw . -- $(TEST_PATH); \
|
||||
make start-services && poetry run ptw . -- -x $(TEST_PATH); \
|
||||
EXIT_CODE=$$?; \
|
||||
make stop-services; \
|
||||
exit $$EXIT_CODE
|
||||
|
||||
@@ -8,6 +8,7 @@ from contextlib import (
|
||||
)
|
||||
from functools import partial
|
||||
from typing import Any, Optional, Sequence
|
||||
from uuid import UUID
|
||||
|
||||
import orjson
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
@@ -37,7 +38,7 @@ from langgraph.scheduler.kafka.types import (
|
||||
Sendable,
|
||||
Topics,
|
||||
)
|
||||
from langgraph.types import LoopProtocol, RetryPolicy
|
||||
from langgraph.types import LoopProtocol, PregelExecutableTask, RetryPolicy
|
||||
from langgraph.utils.config import patch_configurable
|
||||
|
||||
|
||||
@@ -197,6 +198,7 @@ class AsyncKafkaExecutor(AbstractAsyncContextManager):
|
||||
msg["task"]["path"],
|
||||
msg["task"]["id"],
|
||||
checkpoint=saved.checkpoint,
|
||||
pending_writes=saved.pending_writes or [],
|
||||
processes=graph.nodes,
|
||||
channels=channels,
|
||||
managed=managed,
|
||||
@@ -210,13 +212,14 @@ class AsyncKafkaExecutor(AbstractAsyncContextManager):
|
||||
runner = PregelRunner(
|
||||
submit=submit,
|
||||
put_writes=partial(self._put_writes, submit, msg["config"]),
|
||||
schedule_task=self._schedule_task,
|
||||
)
|
||||
async for _ in runner.atick([task], reraise=False):
|
||||
pass
|
||||
else:
|
||||
# task was not found
|
||||
await self.graph.checkpointer.aput_writes(
|
||||
msg["config"], [(ERROR, TaskNotFound())]
|
||||
msg["config"], [(ERROR, TaskNotFound())], str(UUID(int=0))
|
||||
)
|
||||
# notify orchestrator
|
||||
fut = await self.producer.send(
|
||||
@@ -238,6 +241,14 @@ class AsyncKafkaExecutor(AbstractAsyncContextManager):
|
||||
)
|
||||
await fut
|
||||
|
||||
def _schedule_task(
|
||||
self,
|
||||
task: PregelExecutableTask,
|
||||
idx: int,
|
||||
) -> None:
|
||||
# will be scheduled by orchestrator when executor finishes
|
||||
pass
|
||||
|
||||
def _put_writes(
|
||||
self,
|
||||
submit: Submit,
|
||||
@@ -399,6 +410,7 @@ class KafkaExecutor(AbstractContextManager):
|
||||
msg["task"]["path"],
|
||||
msg["task"]["id"],
|
||||
checkpoint=saved.checkpoint,
|
||||
pending_writes=saved.pending_writes or [],
|
||||
processes=graph.nodes,
|
||||
channels=channels,
|
||||
managed=managed,
|
||||
@@ -411,13 +423,14 @@ class KafkaExecutor(AbstractContextManager):
|
||||
runner = PregelRunner(
|
||||
submit=submit,
|
||||
put_writes=partial(self._put_writes, submit, msg["config"]),
|
||||
schedule_task=self._schedule_task,
|
||||
)
|
||||
for _ in runner.tick([task], reraise=False):
|
||||
pass
|
||||
else:
|
||||
# task was not found
|
||||
self.graph.checkpointer.put_writes(
|
||||
msg["config"], [(ERROR, TaskNotFound())]
|
||||
msg["config"], [(ERROR, TaskNotFound())], str(UUID(int=0))
|
||||
)
|
||||
# notify orchestrator
|
||||
fut = self.producer.send(
|
||||
@@ -439,6 +452,14 @@ class KafkaExecutor(AbstractContextManager):
|
||||
)
|
||||
fut.result()
|
||||
|
||||
def _schedule_task(
|
||||
self,
|
||||
task: PregelExecutableTask,
|
||||
idx: int,
|
||||
) -> None:
|
||||
# will be scheduled by orchestrator when executor finishes
|
||||
pass
|
||||
|
||||
def _put_writes(
|
||||
self,
|
||||
submit: Submit,
|
||||
|
||||
@@ -161,18 +161,18 @@ class AsyncKafkaOrchestrator(AbstractAsyncContextManager):
|
||||
specs=graph.channels,
|
||||
output_keys=graph.output_channels,
|
||||
stream_keys=graph.stream_channels,
|
||||
interrupt_after=graph.interrupt_after_nodes,
|
||||
interrupt_before=graph.interrupt_before_nodes,
|
||||
check_subgraphs=False,
|
||||
) as loop:
|
||||
if loop.tick(
|
||||
input_keys=graph.input_channels,
|
||||
interrupt_after=graph.interrupt_after_nodes,
|
||||
interrupt_before=graph.interrupt_before_nodes,
|
||||
):
|
||||
if loop.tick(input_keys=graph.input_channels):
|
||||
# wait for checkpoint to be saved
|
||||
if hasattr(loop, "_put_checkpoint_fut"):
|
||||
await loop._put_checkpoint_fut
|
||||
# schedule any new tasks
|
||||
if new_tasks := [t for t in loop.tasks.values() if not t.scheduled]:
|
||||
if new_tasks := [
|
||||
t for t in loop.tasks.values() if not t.scheduled and not t.writes
|
||||
]:
|
||||
# send messages to executor
|
||||
futures = await asyncio.gather(
|
||||
*(
|
||||
@@ -351,18 +351,18 @@ class KafkaOrchestrator(AbstractContextManager):
|
||||
specs=graph.channels,
|
||||
output_keys=graph.output_channels,
|
||||
stream_keys=graph.stream_channels,
|
||||
interrupt_after=graph.interrupt_after_nodes,
|
||||
interrupt_before=graph.interrupt_before_nodes,
|
||||
check_subgraphs=False,
|
||||
) as loop:
|
||||
if loop.tick(
|
||||
input_keys=graph.input_channels,
|
||||
interrupt_after=graph.interrupt_after_nodes,
|
||||
interrupt_before=graph.interrupt_before_nodes,
|
||||
):
|
||||
if loop.tick(input_keys=graph.input_channels):
|
||||
# wait for checkpoint to be saved
|
||||
if hasattr(loop, "_put_checkpoint_fut"):
|
||||
loop._put_checkpoint_fut.result()
|
||||
# schedule any new tasks
|
||||
if new_tasks := [t for t in loop.tasks.values() if not t.scheduled]:
|
||||
if new_tasks := [
|
||||
t for t in loop.tasks.values() if not t.scheduled and not t.writes
|
||||
]:
|
||||
# send messages to executor
|
||||
futures = [
|
||||
self.producer.send(
|
||||
|
||||
@@ -24,8 +24,8 @@ class MessageToOrchestrator(TypedDict):
|
||||
|
||||
|
||||
class ExecutorTask(TypedDict):
|
||||
id: str
|
||||
path: tuple[str, ...]
|
||||
id: Optional[str]
|
||||
path: tuple[Union[str, int], ...]
|
||||
|
||||
|
||||
class MessageToExecutor(TypedDict):
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import operator
|
||||
from typing import (
|
||||
Annotated,
|
||||
Literal,
|
||||
Union,
|
||||
)
|
||||
|
||||
import pytest
|
||||
from aiokafka import AIOKafkaProducer
|
||||
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.constants import FF_SEND_V2, START
|
||||
from langgraph.errors import NodeInterrupt
|
||||
from langgraph.graph.state import CompiledStateGraph, GraphCommand, StateGraph
|
||||
from langgraph.scheduler.kafka import serde
|
||||
from langgraph.scheduler.kafka.types import MessageToOrchestrator, Topics
|
||||
from langgraph.types import Send
|
||||
from tests.any import AnyDict
|
||||
from tests.drain import drain_topics_async
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
def mk_push_graph(
|
||||
checkpointer: BaseCheckpointSaver,
|
||||
) -> CompiledStateGraph:
|
||||
# copied from test_send_dedupe_on_resume
|
||||
|
||||
class InterruptOnce:
|
||||
ticks: int = 0
|
||||
|
||||
def __call__(self, state):
|
||||
self.ticks += 1
|
||||
if self.ticks == 1:
|
||||
raise NodeInterrupt("Bahh")
|
||||
return ["|".join(("flaky", str(state)))]
|
||||
|
||||
class Node:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
self.ticks = 0
|
||||
self.__name__ = name
|
||||
|
||||
def __call__(self, state):
|
||||
self.ticks += 1
|
||||
update = (
|
||||
[self.name]
|
||||
if isinstance(state, list)
|
||||
else ["|".join((self.name, str(state)))]
|
||||
)
|
||||
if isinstance(state, GraphCommand):
|
||||
return state.copy(update=update)
|
||||
else:
|
||||
return update
|
||||
|
||||
def send_for_fun(state):
|
||||
return [
|
||||
Send("2", GraphCommand(send=Send("2", 3))),
|
||||
Send("2", GraphCommand(send=Send("flaky", 4))),
|
||||
"3.1",
|
||||
]
|
||||
|
||||
def route_to_three(state) -> Literal["3"]:
|
||||
return "3"
|
||||
|
||||
builder = StateGraph(Annotated[list, operator.add])
|
||||
builder.add_node(Node("1"))
|
||||
builder.add_node(Node("2"))
|
||||
builder.add_node(Node("3"))
|
||||
builder.add_node(Node("3.1"))
|
||||
builder.add_node("flaky", InterruptOnce())
|
||||
builder.add_edge(START, "1")
|
||||
builder.add_conditional_edges("1", send_for_fun)
|
||||
builder.add_conditional_edges("2", route_to_three)
|
||||
|
||||
return builder.compile(checkpointer=checkpointer)
|
||||
|
||||
|
||||
async def test_push_graph(topics: Topics, acheckpointer: BaseCheckpointSaver) -> None:
|
||||
if not FF_SEND_V2:
|
||||
pytest.skip("Test requires FF_SEND_V2")
|
||||
|
||||
input = ["0"]
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
graph = mk_push_graph(acheckpointer)
|
||||
graph_compare = mk_push_graph(acheckpointer)
|
||||
|
||||
# start a new run
|
||||
async with AIOKafkaProducer(value_serializer=serde.dumps) as producer:
|
||||
await producer.send_and_wait(
|
||||
topics.orchestrator,
|
||||
MessageToOrchestrator(input=input, config=config),
|
||||
)
|
||||
|
||||
# drain topics
|
||||
orch_msgs, exec_msgs = await drain_topics_async(topics, graph)
|
||||
|
||||
# check state
|
||||
state = await graph.aget_state(config)
|
||||
assert all(not t.error for t in state.tasks)
|
||||
assert state.next == ("flaky",)
|
||||
assert (
|
||||
state.values
|
||||
== await graph_compare.ainvoke(input, {"configurable": {"thread_id": "2"}})
|
||||
== [
|
||||
"0",
|
||||
"1",
|
||||
"2|Control(send=Send(node='2', arg=3))",
|
||||
"2|Control(send=Send(node='flaky', arg=4))",
|
||||
"2|3",
|
||||
]
|
||||
)
|
||||
|
||||
# check history
|
||||
history = [c async for c in graph.aget_state_history(config)]
|
||||
assert len(history) == 2
|
||||
|
||||
# check messages
|
||||
assert orch_msgs == [MessageToOrchestrator(input=input, config=config)] + [
|
||||
{
|
||||
"config": {
|
||||
"callbacks": None,
|
||||
"configurable": {
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": False,
|
||||
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
|
||||
"checkpoint_ns": "",
|
||||
"thread_id": "1",
|
||||
},
|
||||
"metadata": AnyDict(),
|
||||
"recursion_limit": 25,
|
||||
"tags": [],
|
||||
},
|
||||
"input": None,
|
||||
"finally_send": None,
|
||||
}
|
||||
for c in reversed(history)
|
||||
for _ in c.tasks
|
||||
]
|
||||
assert exec_msgs == [
|
||||
{
|
||||
"config": {
|
||||
"callbacks": None,
|
||||
"configurable": {
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": False,
|
||||
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
|
||||
"checkpoint_ns": "",
|
||||
"thread_id": "1",
|
||||
},
|
||||
"metadata": AnyDict(),
|
||||
"recursion_limit": 25,
|
||||
"tags": [],
|
||||
},
|
||||
"task": {
|
||||
"id": t.id,
|
||||
"path": _convert_path(t.path),
|
||||
},
|
||||
"finally_send": None,
|
||||
}
|
||||
for c in reversed(history)
|
||||
for t in c.tasks
|
||||
]
|
||||
|
||||
# resume the thread
|
||||
async with AIOKafkaProducer(value_serializer=serde.dumps) as producer:
|
||||
await producer.send_and_wait(
|
||||
topics.orchestrator,
|
||||
MessageToOrchestrator(input=None, config=config),
|
||||
)
|
||||
|
||||
orch_msgs, exec_msgs = await drain_topics_async(topics, graph)
|
||||
|
||||
# check final state
|
||||
state = await graph.aget_state(config)
|
||||
assert state.next == ()
|
||||
assert (
|
||||
state.values
|
||||
== await graph_compare.ainvoke(None, {"configurable": {"thread_id": "2"}})
|
||||
== [
|
||||
"0",
|
||||
"1",
|
||||
"2|Control(send=Send(node='2', arg=3))",
|
||||
"2|Control(send=Send(node='flaky', arg=4))",
|
||||
"2|3",
|
||||
"flaky|4",
|
||||
"3",
|
||||
"3.1",
|
||||
]
|
||||
)
|
||||
|
||||
# check history
|
||||
history = [c async for c in graph.aget_state_history(config)]
|
||||
assert len(history) == 4
|
||||
|
||||
# check executions
|
||||
# node "2" doesn't get called again, as we recover writes saved before
|
||||
assert graph.builder.nodes["2"].runnable.func.ticks == 3
|
||||
# node "flaky" gets called again, as it was interrupted
|
||||
assert graph.builder.nodes["flaky"].runnable.func.ticks == 2
|
||||
|
||||
|
||||
def _convert_path(
|
||||
path: tuple[Union[str, int, tuple], ...],
|
||||
) -> list[Union[str, int, list]]:
|
||||
return list(_convert_path(p) if isinstance(p, tuple) else p for p in path)
|
||||
@@ -0,0 +1,210 @@
|
||||
import operator
|
||||
from typing import (
|
||||
Annotated,
|
||||
Literal,
|
||||
Union,
|
||||
)
|
||||
|
||||
import pytest
|
||||
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.constants import FF_SEND_V2, START
|
||||
from langgraph.errors import NodeInterrupt
|
||||
from langgraph.graph.state import CompiledStateGraph, GraphCommand, StateGraph
|
||||
from langgraph.scheduler.kafka import serde
|
||||
from langgraph.scheduler.kafka.default_sync import DefaultProducer
|
||||
from langgraph.scheduler.kafka.types import MessageToOrchestrator, Topics
|
||||
from langgraph.types import Send
|
||||
from tests.any import AnyDict
|
||||
from tests.drain import drain_topics
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
def mk_push_graph(
|
||||
checkpointer: BaseCheckpointSaver,
|
||||
) -> CompiledStateGraph:
|
||||
# copied from test_send_dedupe_on_resume
|
||||
|
||||
class InterruptOnce:
|
||||
ticks: int = 0
|
||||
|
||||
def __call__(self, state):
|
||||
self.ticks += 1
|
||||
if self.ticks == 1:
|
||||
raise NodeInterrupt("Bahh")
|
||||
return ["|".join(("flaky", str(state)))]
|
||||
|
||||
class Node:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
self.ticks = 0
|
||||
self.__name__ = name
|
||||
|
||||
def __call__(self, state):
|
||||
self.ticks += 1
|
||||
update = (
|
||||
[self.name]
|
||||
if isinstance(state, list)
|
||||
else ["|".join((self.name, str(state)))]
|
||||
)
|
||||
if isinstance(state, GraphCommand):
|
||||
return state.copy(update=update)
|
||||
else:
|
||||
return update
|
||||
|
||||
def send_for_fun(state):
|
||||
return [
|
||||
Send("2", GraphCommand(send=Send("2", 3))),
|
||||
Send("2", GraphCommand(send=Send("flaky", 4))),
|
||||
"3.1",
|
||||
]
|
||||
|
||||
def route_to_three(state) -> Literal["3"]:
|
||||
return "3"
|
||||
|
||||
builder = StateGraph(Annotated[list, operator.add])
|
||||
builder.add_node(Node("1"))
|
||||
builder.add_node(Node("2"))
|
||||
builder.add_node(Node("3"))
|
||||
builder.add_node(Node("3.1"))
|
||||
builder.add_node("flaky", InterruptOnce())
|
||||
builder.add_edge(START, "1")
|
||||
builder.add_conditional_edges("1", send_for_fun)
|
||||
builder.add_conditional_edges("2", route_to_three)
|
||||
|
||||
return builder.compile(checkpointer=checkpointer)
|
||||
|
||||
|
||||
def test_push_graph(topics: Topics, acheckpointer: BaseCheckpointSaver) -> None:
|
||||
if not FF_SEND_V2:
|
||||
pytest.skip("Test requires FF_SEND_V2")
|
||||
|
||||
input = ["0"]
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
graph = mk_push_graph(acheckpointer)
|
||||
graph_compare = mk_push_graph(acheckpointer)
|
||||
|
||||
# start a new run
|
||||
with DefaultProducer() as producer:
|
||||
producer.send(
|
||||
topics.orchestrator,
|
||||
value=serde.dumps(MessageToOrchestrator(input=input, config=config)),
|
||||
)
|
||||
producer.flush()
|
||||
|
||||
# drain topics
|
||||
orch_msgs, exec_msgs = drain_topics(topics, graph)
|
||||
|
||||
# check state
|
||||
state = graph.get_state(config)
|
||||
assert all(not t.error for t in state.tasks)
|
||||
assert state.next == ("flaky",)
|
||||
assert (
|
||||
state.values
|
||||
== graph_compare.invoke(input, {"configurable": {"thread_id": "2"}})
|
||||
== [
|
||||
"0",
|
||||
"1",
|
||||
"2|Control(send=Send(node='2', arg=3))",
|
||||
"2|Control(send=Send(node='flaky', arg=4))",
|
||||
"2|3",
|
||||
]
|
||||
)
|
||||
|
||||
# check history
|
||||
history = [c for c in graph.get_state_history(config)]
|
||||
assert len(history) == 2
|
||||
|
||||
# check messages
|
||||
assert orch_msgs == [MessageToOrchestrator(input=input, config=config)] + [
|
||||
{
|
||||
"config": {
|
||||
"callbacks": None,
|
||||
"configurable": {
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": False,
|
||||
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
|
||||
"checkpoint_ns": "",
|
||||
"thread_id": "1",
|
||||
},
|
||||
"metadata": AnyDict(),
|
||||
"recursion_limit": 25,
|
||||
"tags": [],
|
||||
},
|
||||
"input": None,
|
||||
"finally_send": None,
|
||||
}
|
||||
for c in reversed(history)
|
||||
for _ in c.tasks
|
||||
]
|
||||
assert exec_msgs == [
|
||||
{
|
||||
"config": {
|
||||
"callbacks": None,
|
||||
"configurable": {
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": False,
|
||||
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
|
||||
"checkpoint_ns": "",
|
||||
"thread_id": "1",
|
||||
},
|
||||
"metadata": AnyDict(),
|
||||
"recursion_limit": 25,
|
||||
"tags": [],
|
||||
},
|
||||
"task": {
|
||||
"id": t.id,
|
||||
"path": _convert_path(t.path),
|
||||
},
|
||||
"finally_send": None,
|
||||
}
|
||||
for c in reversed(history)
|
||||
for t in c.tasks
|
||||
]
|
||||
|
||||
# resume the thread
|
||||
with DefaultProducer() as producer:
|
||||
producer.send(
|
||||
topics.orchestrator,
|
||||
value=serde.dumps(MessageToOrchestrator(input=None, config=config)),
|
||||
)
|
||||
producer.flush()
|
||||
|
||||
orch_msgs, exec_msgs = drain_topics(topics, graph)
|
||||
|
||||
# check final state
|
||||
state = graph.get_state(config)
|
||||
assert state.next == ()
|
||||
assert (
|
||||
state.values
|
||||
== graph_compare.invoke(None, {"configurable": {"thread_id": "2"}})
|
||||
== [
|
||||
"0",
|
||||
"1",
|
||||
"2|Control(send=Send(node='2', arg=3))",
|
||||
"2|Control(send=Send(node='flaky', arg=4))",
|
||||
"2|3",
|
||||
"flaky|4",
|
||||
"3",
|
||||
"3.1",
|
||||
]
|
||||
)
|
||||
|
||||
# check history
|
||||
history = [c for c in graph.get_state_history(config)]
|
||||
assert len(history) == 4
|
||||
|
||||
# check executions
|
||||
# node "2" doesn't get called again, as we recover writes saved before
|
||||
assert graph.builder.nodes["2"].runnable.func.ticks == 3
|
||||
# node "flaky" gets called again, as it was interrupted
|
||||
assert graph.builder.nodes["flaky"].runnable.func.ticks == 2
|
||||
|
||||
|
||||
def _convert_path(
|
||||
path: tuple[Union[str, int, tuple], ...],
|
||||
) -> list[Union[str, int, list]]:
|
||||
return list(_convert_path(p) if isinstance(p, tuple) else p for p in path)
|
||||
@@ -194,8 +194,9 @@ async def test_subgraph_w_interrupt(
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": False,
|
||||
'__pregel_store': None,
|
||||
"__pregel_store": None,
|
||||
"__pregel_task_id": history[0].tasks[0].id,
|
||||
"__pregel_resume_value": None,
|
||||
"checkpoint_id": None,
|
||||
"checkpoint_map": {
|
||||
"": history[0].config["configurable"]["checkpoint_id"]
|
||||
@@ -258,8 +259,9 @@ async def test_subgraph_w_interrupt(
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": False,
|
||||
'__pregel_store': None,
|
||||
"__pregel_store": None,
|
||||
"__pregel_task_id": history[0].tasks[0].id,
|
||||
"__pregel_resume_value": None,
|
||||
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
|
||||
"checkpoint_map": {
|
||||
"": history[0].config["configurable"]["checkpoint_id"]
|
||||
@@ -352,8 +354,9 @@ async def test_subgraph_w_interrupt(
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": False,
|
||||
'__pregel_store': None,
|
||||
"__pregel_store": None,
|
||||
"__pregel_task_id": history[0].tasks[0].id,
|
||||
"__pregel_resume_value": None,
|
||||
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
|
||||
"checkpoint_map": {
|
||||
"": history[0].config["configurable"]["checkpoint_id"]
|
||||
@@ -456,8 +459,9 @@ async def test_subgraph_w_interrupt(
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": True,
|
||||
'__pregel_store': None,
|
||||
"__pregel_store": None,
|
||||
"__pregel_task_id": history[1].tasks[0].id,
|
||||
"__pregel_resume_value": None,
|
||||
"checkpoint_id": None,
|
||||
"checkpoint_map": {
|
||||
"": history[1].config["configurable"]["checkpoint_id"]
|
||||
@@ -515,8 +519,9 @@ async def test_subgraph_w_interrupt(
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": True,
|
||||
'__pregel_store': None,
|
||||
"__pregel_store": None,
|
||||
"__pregel_task_id": history[1].tasks[0].id,
|
||||
"__pregel_resume_value": None,
|
||||
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
|
||||
"checkpoint_map": {
|
||||
"": history[1].config["configurable"]["checkpoint_id"]
|
||||
@@ -630,8 +635,9 @@ async def test_subgraph_w_interrupt(
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": True,
|
||||
'__pregel_store': None,
|
||||
"__pregel_store": None,
|
||||
"__pregel_task_id": history[1].tasks[0].id,
|
||||
"__pregel_resume_value": None,
|
||||
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
|
||||
"checkpoint_map": {
|
||||
"": history[1].config["configurable"]["checkpoint_id"]
|
||||
|
||||
@@ -193,8 +193,9 @@ def test_subgraph_w_interrupt(
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": False,
|
||||
'__pregel_store': None,
|
||||
"__pregel_store": None,
|
||||
"__pregel_task_id": history[0].tasks[0].id,
|
||||
"__pregel_resume_value": None,
|
||||
"checkpoint_id": None,
|
||||
"checkpoint_map": {
|
||||
"": history[0].config["configurable"]["checkpoint_id"]
|
||||
@@ -255,10 +256,11 @@ def test_subgraph_w_interrupt(
|
||||
"__pregel_read": None,
|
||||
"__pregel_send": None,
|
||||
"__pregel_ensure_latest": True,
|
||||
'__pregel_store': None,
|
||||
"__pregel_store": None,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": False,
|
||||
"__pregel_task_id": history[0].tasks[0].id,
|
||||
"__pregel_resume_value": None,
|
||||
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
|
||||
"checkpoint_map": {
|
||||
"": history[0].config["configurable"]["checkpoint_id"]
|
||||
@@ -350,9 +352,10 @@ def test_subgraph_w_interrupt(
|
||||
"__pregel_send": None,
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
'__pregel_store': None,
|
||||
"__pregel_store": None,
|
||||
"__pregel_resuming": False,
|
||||
"__pregel_task_id": history[0].tasks[0].id,
|
||||
"__pregel_resume_value": None,
|
||||
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
|
||||
"checkpoint_map": {
|
||||
"": history[0].config["configurable"]["checkpoint_id"]
|
||||
@@ -453,9 +456,10 @@ def test_subgraph_w_interrupt(
|
||||
"__pregel_send": None,
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
'__pregel_store': None,
|
||||
"__pregel_store": None,
|
||||
"__pregel_resuming": True,
|
||||
"__pregel_task_id": history[1].tasks[0].id,
|
||||
"__pregel_resume_value": None,
|
||||
"checkpoint_id": None,
|
||||
"checkpoint_map": {
|
||||
"": history[1].config["configurable"]["checkpoint_id"]
|
||||
@@ -512,9 +516,10 @@ def test_subgraph_w_interrupt(
|
||||
"__pregel_send": None,
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
'__pregel_store': None,
|
||||
"__pregel_store": None,
|
||||
"__pregel_resuming": True,
|
||||
"__pregel_task_id": history[1].tasks[0].id,
|
||||
"__pregel_resume_value": None,
|
||||
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
|
||||
"checkpoint_map": {
|
||||
"": history[1].config["configurable"]["checkpoint_id"]
|
||||
@@ -628,8 +633,9 @@ def test_subgraph_w_interrupt(
|
||||
"__pregel_ensure_latest": True,
|
||||
"__pregel_dedupe_tasks": True,
|
||||
"__pregel_resuming": True,
|
||||
'__pregel_store': None,
|
||||
"__pregel_store": None,
|
||||
"__pregel_task_id": history[1].tasks[0].id,
|
||||
"__pregel_resume_value": None,
|
||||
"checkpoint_id": c.config["configurable"]["checkpoint_id"],
|
||||
"checkpoint_map": {
|
||||
"": history[1].config["configurable"]["checkpoint_id"]
|
||||
|
||||
+11
-13
@@ -1,13 +1,11 @@
|
||||
/docs
|
||||
/*.tgz
|
||||
/*.tar
|
||||
## GENERATED create-entrypoints.js
|
||||
/client.cjs
|
||||
/client.js
|
||||
/client.d.ts
|
||||
/client.d.cts
|
||||
/index.cjs
|
||||
/index.js
|
||||
/index.d.ts
|
||||
/index.d.cts
|
||||
## END GENERATED create-entrypoints.js
|
||||
index.cjs
|
||||
index.js
|
||||
index.d.ts
|
||||
index.d.cts
|
||||
client.cjs
|
||||
client.js
|
||||
client.d.ts
|
||||
client.d.cts
|
||||
node_modules
|
||||
dist
|
||||
.yarn
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
/**
|
||||
* @param {string} relativePath
|
||||
* @returns {string}
|
||||
*/
|
||||
function abs(relativePath) {
|
||||
return resolve(dirname(fileURLToPath(import.meta.url)), relativePath);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
internals: [],
|
||||
entrypoints: { index: "index", client: "client" },
|
||||
tsConfigPath: resolve("./tsconfig.json"),
|
||||
cjsSource: "./dist-cjs",
|
||||
cjsDestination: "./dist",
|
||||
abs,
|
||||
};
|
||||
@@ -1,14 +1,12 @@
|
||||
{
|
||||
"name": "@langchain/langgraph-sdk",
|
||||
"version": "0.0.20",
|
||||
"version": "0.0.25",
|
||||
"description": "Client library for interacting with the LangGraph API",
|
||||
"type": "module",
|
||||
"packageManager": "yarn@1.22.19",
|
||||
"scripts": {
|
||||
"clean": "rm -rf dist/ && node scripts/create-entrypoints.js clean",
|
||||
"build": "yarn clean && yarn build:esm && yarn build:cjs && node scripts/create-entrypoints.js",
|
||||
"build:esm": "rm -f src/package.json && tsc --outDir dist/ && rm -rf dist/tests dist/**/tests",
|
||||
"build:cjs": "echo '{}' > src/package.json && tsc --outDir dist-cjs/ -p tsconfig.cjs.json && node scripts/move-cjs-to-dist.js && rm -r dist-cjs src/package.json",
|
||||
"clean": "rm -rf dist/ dist-cjs/",
|
||||
"build": "yarn clean && yarn lc_build --create-entrypoints --pre --tree-shaking",
|
||||
"prepublish": "yarn run build",
|
||||
"format": "prettier --write src",
|
||||
"lint": "prettier --check src && tsc --noEmit"
|
||||
@@ -22,6 +20,7 @@
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@langchain/scripts": "^0.1.4",
|
||||
"@tsconfig/recommended": "^1.0.2",
|
||||
"@types/node": "^20.12.12",
|
||||
"@types/uuid": "^9.0.1",
|
||||
@@ -54,13 +53,13 @@
|
||||
},
|
||||
"files": [
|
||||
"dist/",
|
||||
"client.cjs",
|
||||
"client.js",
|
||||
"client.d.ts",
|
||||
"client.d.cts",
|
||||
"index.cjs",
|
||||
"index.js",
|
||||
"index.d.ts",
|
||||
"index.d.cts"
|
||||
"index.d.cts",
|
||||
"client.cjs",
|
||||
"client.js",
|
||||
"client.d.ts",
|
||||
"client.d.cts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
// This lists all the entrypoints for the library. Each key corresponds to an
|
||||
// importable path, eg. `import { Foo } from "langgraph-sdk/client"`.
|
||||
// The value is the path to the file in `src/` that exports the entrypoint.
|
||||
// This is used to generate the `exports` field in package.json.
|
||||
// Order is not important.
|
||||
const entrypoints = { client: "client" };
|
||||
|
||||
const updateJsonFile = (relativePath, updateFunction) => {
|
||||
const contents = fs.readFileSync(relativePath).toString();
|
||||
const res = updateFunction(JSON.parse(contents));
|
||||
fs.writeFileSync(relativePath, JSON.stringify(res, null, 2) + "\n");
|
||||
};
|
||||
|
||||
const generateFiles = () => {
|
||||
const files = [...Object.entries(entrypoints), ["index", "index"]].flatMap(
|
||||
([key, value]) => {
|
||||
const nrOfDots = key.split("/").length - 1;
|
||||
const relativePath = "../".repeat(nrOfDots) || "./";
|
||||
const compiledPath = `${relativePath}dist/${value}`;
|
||||
return [
|
||||
[`${key}.cjs`, `module.exports = require('${compiledPath}.cjs');`],
|
||||
[`${key}.js`, `export * from '${compiledPath}.js'`],
|
||||
[`${key}.d.ts`, `export * from '${compiledPath}.js'`],
|
||||
[`${key}.d.cts`, `export * from '${compiledPath}.cjs'`],
|
||||
];
|
||||
},
|
||||
);
|
||||
|
||||
return Object.fromEntries(files);
|
||||
};
|
||||
|
||||
const updateConfig = () => {
|
||||
// Update tsconfig.json `typedocOptions.entryPoints` field
|
||||
updateJsonFile("./tsconfig.json", (json) => ({
|
||||
...json,
|
||||
typedocOptions: {
|
||||
...json.typedocOptions,
|
||||
entryPoints: [...Object.keys(entrypoints)].map((key) => {
|
||||
const basePath = `src/${entrypoints[key]}`;
|
||||
if (fs.existsSync(`${basePath}.mts`)) {
|
||||
return `${basePath}.mts`;
|
||||
} else if (fs.existsSync(`${basePath}.ts`)) {
|
||||
return `${basePath}.ts`;
|
||||
} else {
|
||||
console.warn(
|
||||
`Warning: Neither ${basePath}.mts nor ${basePath}.ts found for entrypoint ${key}`,
|
||||
);
|
||||
return `${basePath}.ts`; // Default to .ts if neither exists
|
||||
}
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
const generatedFiles = generateFiles();
|
||||
const filenames = Object.keys(generatedFiles);
|
||||
|
||||
// Update package.json `exports` and `files` fields
|
||||
updateJsonFile("./package.json", (json) => ({
|
||||
...json,
|
||||
exports: Object.assign(
|
||||
Object.fromEntries(
|
||||
["index", ...Object.keys(entrypoints)].map((key) => {
|
||||
let entryPoint = {
|
||||
types: {
|
||||
import: `./${key}.d.ts`,
|
||||
require: `./${key}.d.cts`,
|
||||
default: `./${key}.d.ts`,
|
||||
},
|
||||
import: `./${key}.js`,
|
||||
require: `./${key}.cjs`,
|
||||
};
|
||||
|
||||
return [key === "index" ? "." : `./${key}`, entryPoint];
|
||||
}),
|
||||
),
|
||||
{
|
||||
"./package.json": "./package.json",
|
||||
},
|
||||
),
|
||||
files: ["dist/", ...filenames],
|
||||
}));
|
||||
|
||||
// Write generated files
|
||||
Object.entries(generatedFiles).forEach(([filename, content]) => {
|
||||
fs.mkdirSync(path.dirname(filename), {
|
||||
recursive: true,
|
||||
});
|
||||
fs.writeFileSync(filename, content);
|
||||
});
|
||||
|
||||
const gitignore = fs.readFileSync("./.gitignore").toString();
|
||||
const lines = gitignore.split("\n");
|
||||
const startMarker = "## GENERATED create-entrypoints.js";
|
||||
const endMarker = "## END GENERATED create-entrypoints.js";
|
||||
const startIdx = lines.findIndex((line) => line.includes(startMarker));
|
||||
const endIdx = lines.findIndex((line) => line.includes(endMarker));
|
||||
const newLines = lines.slice(0, startIdx + 1);
|
||||
if (startIdx === -1) {
|
||||
newLines.push(startMarker);
|
||||
}
|
||||
newLines.push(...filenames.map((fname) => `/${fname}`));
|
||||
if (endIdx === -1) {
|
||||
newLines.push(endMarker);
|
||||
}
|
||||
newLines.push(...lines.slice(endIdx));
|
||||
fs.writeFileSync("./.gitignore", newLines.join("\n"));
|
||||
};
|
||||
|
||||
const cleanGenerated = () => {
|
||||
const filenames = Object.keys(generateFiles());
|
||||
filenames.forEach((fname) => {
|
||||
try {
|
||||
fs.unlinkSync(fname);
|
||||
} catch {
|
||||
// ignore error
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const command = process.argv[2];
|
||||
|
||||
if (command === "clean") {
|
||||
cleanGenerated();
|
||||
} else {
|
||||
updateConfig();
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { resolve, dirname, parse, format } from "node:path";
|
||||
import { readdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
function abs(relativePath) {
|
||||
return resolve(dirname(fileURLToPath(import.meta.url)), relativePath);
|
||||
}
|
||||
|
||||
async function moveAndRename(source, dest) {
|
||||
for (const file of await readdir(abs(source), { withFileTypes: true })) {
|
||||
if (file.isDirectory()) {
|
||||
await moveAndRename(`${source}/${file.name}`, `${dest}/${file.name}`);
|
||||
} else if (file.isFile()) {
|
||||
const parsed = parse(file.name);
|
||||
|
||||
// Ignore anything that's not a .js file
|
||||
if (parsed.ext !== ".js") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rewrite any require statements to use .cjs
|
||||
const content = await readFile(abs(`${source}/${file.name}`), "utf8");
|
||||
const rewritten = content.replace(/require\("(\..+?).js"\)/g, (_, p1) => {
|
||||
return `require("${p1}.cjs")`;
|
||||
});
|
||||
|
||||
// Rename the file to .cjs
|
||||
const renamed = format({ name: parsed.name, ext: ".cjs" });
|
||||
|
||||
await writeFile(abs(`${dest}/${renamed}`), rewritten, "utf8");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moveAndRename("../dist-cjs", "../dist").catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
SearchItemsResponse,
|
||||
ListNamespaceResponse,
|
||||
Item,
|
||||
ThreadStatus,
|
||||
} from "./schema.js";
|
||||
import { AsyncCaller, AsyncCallerParams } from "./utils/async_caller.js";
|
||||
import {
|
||||
@@ -516,6 +517,11 @@ export class ThreadsClient extends BaseClient {
|
||||
* Offset to start from.
|
||||
*/
|
||||
offset?: number;
|
||||
/**
|
||||
* Thread status to filter on.
|
||||
* Must be one of 'idle', 'busy', 'interrupted' or 'error'.
|
||||
*/
|
||||
status?: ThreadStatus;
|
||||
}): Promise<Thread[]> {
|
||||
return this.fetch<Thread[]>("/threads/search", {
|
||||
method: "POST",
|
||||
@@ -523,6 +529,7 @@ export class ThreadsClient extends BaseClient {
|
||||
metadata: query?.metadata ?? undefined,
|
||||
limit: query?.limit ?? 10,
|
||||
offset: query?.offset ?? 0,
|
||||
status: query?.status,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -686,6 +693,7 @@ export class RunsClient extends BaseClient {
|
||||
}> {
|
||||
const json: Record<string, any> = {
|
||||
input: payload?.input,
|
||||
command: payload?.command,
|
||||
config: payload?.config,
|
||||
metadata: payload?.metadata,
|
||||
stream_mode: payload?.streamMode,
|
||||
@@ -773,6 +781,7 @@ export class RunsClient extends BaseClient {
|
||||
): Promise<Run> {
|
||||
const json: Record<string, any> = {
|
||||
input: payload?.input,
|
||||
command: payload?.command,
|
||||
config: payload?.config,
|
||||
metadata: payload?.metadata,
|
||||
assistant_id: assistantId,
|
||||
@@ -842,6 +851,7 @@ export class RunsClient extends BaseClient {
|
||||
): Promise<ThreadState["values"]> {
|
||||
const json: Record<string, any> = {
|
||||
input: payload?.input,
|
||||
command: payload?.command,
|
||||
config: payload?.config,
|
||||
metadata: payload?.metadata,
|
||||
assistant_id: assistantId,
|
||||
|
||||
@@ -10,7 +10,9 @@ export type {
|
||||
Metadata,
|
||||
Run,
|
||||
Thread,
|
||||
ThreadTask,
|
||||
ThreadState,
|
||||
ThreadStatus,
|
||||
Cron,
|
||||
Checkpoint,
|
||||
} from "./schema.js";
|
||||
|
||||
@@ -10,7 +10,7 @@ type RunStatus =
|
||||
| "timeout"
|
||||
| "interrupted";
|
||||
|
||||
type ThreadStatus = "idle" | "busy" | "interrupted" | "error";
|
||||
export type ThreadStatus = "idle" | "busy" | "interrupted" | "error";
|
||||
|
||||
type MultitaskStrategy = "reject" | "interrupt" | "rollback" | "enqueue";
|
||||
|
||||
@@ -79,7 +79,17 @@ export interface GraphSchema {
|
||||
|
||||
export type Subgraphs = Record<string, GraphSchema>;
|
||||
|
||||
export type Metadata = Optional<Record<string, unknown>>;
|
||||
export type Metadata = Optional<{
|
||||
source?: "input" | "loop" | "update" | (string & {});
|
||||
|
||||
step?: number;
|
||||
|
||||
writes?: Record<string, unknown> | null;
|
||||
|
||||
parents?: Record<string, string>;
|
||||
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
|
||||
export interface AssistantBase {
|
||||
/** The ID of the assistant. */
|
||||
@@ -110,7 +120,22 @@ export interface Assistant extends AssistantBase {
|
||||
/** The name of the assistant */
|
||||
name: string;
|
||||
}
|
||||
export type AssistantGraph = Record<string, Array<Record<string, unknown>>>;
|
||||
|
||||
export interface AssistantGraph {
|
||||
nodes: Array<{
|
||||
id: string | number;
|
||||
name?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data?: Record<string, any> | string;
|
||||
metadata?: unknown;
|
||||
}>;
|
||||
edges: Array<{
|
||||
source: string;
|
||||
target: string;
|
||||
data?: string;
|
||||
conditional?: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface Thread<ValuesType = DefaultValues> {
|
||||
/** The ID of the thread. */
|
||||
@@ -183,8 +208,14 @@ export interface ThreadState<ValuesType = DefaultValues> {
|
||||
export interface ThreadTask {
|
||||
id: string;
|
||||
name: string;
|
||||
result?: unknown;
|
||||
error: Optional<string>;
|
||||
interrupts: Array<Record<string, unknown>>;
|
||||
interrupts: Array<{
|
||||
value: unknown;
|
||||
when: "during";
|
||||
resumable: boolean;
|
||||
ns?: string[];
|
||||
}>;
|
||||
checkpoint: Optional<Checkpoint>;
|
||||
state: Optional<ThreadState>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { Checkpoint, Config, Metadata } from "./schema.js";
|
||||
|
||||
export type StreamMode = "values" | "messages" | "updates" | "events" | "debug";
|
||||
export type StreamMode =
|
||||
| "values"
|
||||
| "messages"
|
||||
| "updates"
|
||||
| "events"
|
||||
| "debug"
|
||||
| "custom"
|
||||
| "messages-tuple";
|
||||
export type MultitaskStrategy = "reject" | "interrupt" | "rollback" | "enqueue";
|
||||
export type OnConflictBehavior = "raise" | "do_nothing";
|
||||
export type OnCompletionBehavior = "complete" | "continue";
|
||||
@@ -16,6 +23,19 @@ export type StreamEvent =
|
||||
| "messages/complete"
|
||||
| (string & {});
|
||||
|
||||
export interface Send {
|
||||
node: string;
|
||||
input: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
update?: Record<string, unknown>;
|
||||
|
||||
resume?: unknown;
|
||||
|
||||
send?: Send | Send[];
|
||||
}
|
||||
|
||||
interface RunsInvokePayload {
|
||||
/**
|
||||
* Input to the run. Pass `null` to resume from the current state of the thread.
|
||||
@@ -45,12 +65,12 @@ interface RunsInvokePayload {
|
||||
/**
|
||||
* Interrupt execution before entering these nodes.
|
||||
*/
|
||||
interruptBefore?: string[];
|
||||
interruptBefore?: "*" | string[];
|
||||
|
||||
/**
|
||||
* Interrupt execution after leaving these nodes.
|
||||
*/
|
||||
interruptAfter?: string[];
|
||||
interruptAfter?: "*" | string[];
|
||||
|
||||
/**
|
||||
* Strategy to handle concurrent runs on the same thread. Only relevant if
|
||||
@@ -100,6 +120,11 @@ interface RunsInvokePayload {
|
||||
* Behavior if the specified run doesn't exist. Defaults to "reject".
|
||||
*/
|
||||
ifNotExists?: "create" | "reject";
|
||||
|
||||
/**
|
||||
* One or more commands to invoke the graph with.
|
||||
*/
|
||||
command?: Command;
|
||||
}
|
||||
|
||||
export interface RunsStreamPayload extends RunsInvokePayload {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"declaration": false
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "**/tests"]
|
||||
|
||||
+608
-1
@@ -25,6 +25,35 @@
|
||||
js-tokens "^4.0.0"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
"@isaacs/cliui@^8.0.2":
|
||||
version "8.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
|
||||
integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==
|
||||
dependencies:
|
||||
string-width "^5.1.2"
|
||||
string-width-cjs "npm:string-width@^4.2.0"
|
||||
strip-ansi "^7.0.1"
|
||||
strip-ansi-cjs "npm:strip-ansi@^6.0.1"
|
||||
wrap-ansi "^8.1.0"
|
||||
wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
|
||||
|
||||
"@langchain/scripts@^0.1.4":
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/scripts/-/scripts-0.1.4.tgz#8c5d03d627686f20b9522213c12e2b329e73e381"
|
||||
integrity sha512-O+mv2aqUIm3XWxYBrwFWMeTI3aHWeFR8OYjGFXKc1MGV/3LLao3PciyQvKUg1SL7FemHJ1ltDx74rKuEv8xxPA==
|
||||
dependencies:
|
||||
"@octokit/rest" "^21.0.2"
|
||||
"@rollup/wasm-node" "^4.19.0"
|
||||
axios "^1.6.7"
|
||||
commander "^11.1.0"
|
||||
glob "^10.3.10"
|
||||
lodash "^4.17.21"
|
||||
readline "^1.3.0"
|
||||
rimraf "^5.0.1"
|
||||
rollup "^4.5.2"
|
||||
ts-morph "^21.0.1"
|
||||
typescript "^5.4.5"
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
||||
@@ -46,6 +75,203 @@
|
||||
"@nodelib/fs.scandir" "2.1.5"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@octokit/auth-token@^5.0.0":
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-5.1.1.tgz#3bbfe905111332a17f72d80bd0b51a3e2fa2cf07"
|
||||
integrity sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==
|
||||
|
||||
"@octokit/core@^6.1.2":
|
||||
version "6.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/core/-/core-6.1.2.tgz#20442d0a97c411612da206411e356014d1d1bd17"
|
||||
integrity sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==
|
||||
dependencies:
|
||||
"@octokit/auth-token" "^5.0.0"
|
||||
"@octokit/graphql" "^8.0.0"
|
||||
"@octokit/request" "^9.0.0"
|
||||
"@octokit/request-error" "^6.0.1"
|
||||
"@octokit/types" "^13.0.0"
|
||||
before-after-hook "^3.0.2"
|
||||
universal-user-agent "^7.0.0"
|
||||
|
||||
"@octokit/endpoint@^10.0.0":
|
||||
version "10.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-10.1.1.tgz#1a9694e7aef6aa9d854dc78dd062945945869bcc"
|
||||
integrity sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==
|
||||
dependencies:
|
||||
"@octokit/types" "^13.0.0"
|
||||
universal-user-agent "^7.0.2"
|
||||
|
||||
"@octokit/graphql@^8.0.0":
|
||||
version "8.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-8.1.1.tgz#3cacab5f2e55d91c733e3bf481d3a3f8a5f639c4"
|
||||
integrity sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==
|
||||
dependencies:
|
||||
"@octokit/request" "^9.0.0"
|
||||
"@octokit/types" "^13.0.0"
|
||||
universal-user-agent "^7.0.0"
|
||||
|
||||
"@octokit/openapi-types@^22.2.0":
|
||||
version "22.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-22.2.0.tgz#75aa7dcd440821d99def6a60b5f014207ae4968e"
|
||||
integrity sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==
|
||||
|
||||
"@octokit/plugin-paginate-rest@^11.0.0":
|
||||
version "11.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.5.tgz#a1929b3ba3dc7b63bc73bb6d3c7a3faf2a9c7649"
|
||||
integrity sha512-cgwIRtKrpwhLoBi0CUNuY83DPGRMaWVjqVI/bGKsLJ4PzyWZNaEmhHroI2xlrVXkk6nFv0IsZpOp+ZWSWUS2AQ==
|
||||
dependencies:
|
||||
"@octokit/types" "^13.6.0"
|
||||
|
||||
"@octokit/plugin-request-log@^5.3.1":
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz#ccb75d9705de769b2aa82bcd105cc96eb0c00f69"
|
||||
integrity sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods@^13.0.0":
|
||||
version "13.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.6.tgz#b9d343dbe88a6cb70cc7fa16faa98f0a29ffe654"
|
||||
integrity sha512-wMsdyHMjSfKjGINkdGKki06VEkgdEldIGstIEyGX0wbYHGByOwN/KiM+hAAlUwAtPkP3gvXtVQA9L3ITdV2tVw==
|
||||
dependencies:
|
||||
"@octokit/types" "^13.6.1"
|
||||
|
||||
"@octokit/request-error@^6.0.1":
|
||||
version "6.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-6.1.5.tgz#907099e341c4e6179db623a0328d678024f54653"
|
||||
integrity sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==
|
||||
dependencies:
|
||||
"@octokit/types" "^13.0.0"
|
||||
|
||||
"@octokit/request@^9.0.0":
|
||||
version "9.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/request/-/request-9.1.3.tgz#42b693bc06238f43af3c037ebfd35621c6457838"
|
||||
integrity sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==
|
||||
dependencies:
|
||||
"@octokit/endpoint" "^10.0.0"
|
||||
"@octokit/request-error" "^6.0.1"
|
||||
"@octokit/types" "^13.1.0"
|
||||
universal-user-agent "^7.0.2"
|
||||
|
||||
"@octokit/rest@^21.0.2":
|
||||
version "21.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-21.0.2.tgz#9b767dbc1098daea8310fd8b76bf7a97215d5972"
|
||||
integrity sha512-+CiLisCoyWmYicH25y1cDfCrv41kRSvTq6pPWtRroRJzhsCZWZyCqGyI8foJT5LmScADSwRAnr/xo+eewL04wQ==
|
||||
dependencies:
|
||||
"@octokit/core" "^6.1.2"
|
||||
"@octokit/plugin-paginate-rest" "^11.0.0"
|
||||
"@octokit/plugin-request-log" "^5.3.1"
|
||||
"@octokit/plugin-rest-endpoint-methods" "^13.0.0"
|
||||
|
||||
"@octokit/types@^13.0.0", "@octokit/types@^13.1.0", "@octokit/types@^13.6.0", "@octokit/types@^13.6.1":
|
||||
version "13.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.6.1.tgz#432fc6c0aaae54318e5b2d3e15c22ac97fc9b15f"
|
||||
integrity sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==
|
||||
dependencies:
|
||||
"@octokit/openapi-types" "^22.2.0"
|
||||
|
||||
"@pkgjs/parseargs@^0.11.0":
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.24.4":
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.4.tgz#c460b54c50d42f27f8254c435a4f3b3e01910bc8"
|
||||
integrity sha512-jfUJrFct/hTA0XDM5p/htWKoNNTbDLY0KRwEt6pyOA6k2fmk0WVwl65PdUdJZgzGEHWx+49LilkcSaumQRyNQw==
|
||||
|
||||
"@rollup/rollup-android-arm64@4.24.4":
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.4.tgz#96e01f3a04675d8d5973ab8d3fd6bc3be21fa5e1"
|
||||
integrity sha512-j4nrEO6nHU1nZUuCfRKoCcvh7PIywQPUCBa2UsootTHvTHIoIu2BzueInGJhhvQO/2FTRdNYpf63xsgEqH9IhA==
|
||||
|
||||
"@rollup/rollup-darwin-arm64@4.24.4":
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.4.tgz#9b2ec23b17b47cbb2f771b81f86ede3ac6730bce"
|
||||
integrity sha512-GmU/QgGtBTeraKyldC7cDVVvAJEOr3dFLKneez/n7BvX57UdhOqDsVwzU7UOnYA7AAOt+Xb26lk79PldDHgMIQ==
|
||||
|
||||
"@rollup/rollup-darwin-x64@4.24.4":
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.4.tgz#f30e4ee6929e048190cf10e0daa8e8ae035b6e46"
|
||||
integrity sha512-N6oDBiZCBKlwYcsEPXGDE4g9RoxZLK6vT98M8111cW7VsVJFpNEqvJeIPfsCzbf0XEakPslh72X0gnlMi4Ddgg==
|
||||
|
||||
"@rollup/rollup-freebsd-arm64@4.24.4":
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.4.tgz#c54b2373ec5bcf71f08c4519c7ae80a0b6c8e03b"
|
||||
integrity sha512-py5oNShCCjCyjWXCZNrRGRpjWsF0ic8f4ieBNra5buQz0O/U6mMXCpC1LvrHuhJsNPgRt36tSYMidGzZiJF6mw==
|
||||
|
||||
"@rollup/rollup-freebsd-x64@4.24.4":
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.4.tgz#3bc53aa29d5a34c28ba8e00def76aa612368458e"
|
||||
integrity sha512-L7VVVW9FCnTTp4i7KrmHeDsDvjB4++KOBENYtNYAiYl96jeBThFfhP6HVxL74v4SiZEVDH/1ILscR5U9S4ms4g==
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.24.4":
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.4.tgz#c85aedd1710c9e267ee86b6d1ce355ecf7d9e8d9"
|
||||
integrity sha512-10ICosOwYChROdQoQo589N5idQIisxjaFE/PAnX2i0Zr84mY0k9zul1ArH0rnJ/fpgiqfu13TFZR5A5YJLOYZA==
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf@4.24.4":
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.4.tgz#e77313408bf13995aecde281aec0cceb08747e42"
|
||||
integrity sha512-ySAfWs69LYC7QhRDZNKqNhz2UKN8LDfbKSMAEtoEI0jitwfAG2iZwVqGACJT+kfYvvz3/JgsLlcBP+WWoKCLcw==
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu@4.24.4":
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.4.tgz#633f632397b3662108cfaa1abca2a80b85f51102"
|
||||
integrity sha512-uHYJ0HNOI6pGEeZ/5mgm5arNVTI0nLlmrbdph+pGXpC9tFHFDQmDMOEqkmUObRfosJqpU8RliYoGz06qSdtcjg==
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl@4.24.4":
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.4.tgz#63edd72b29c4cced93e16113a68e1be9fef88907"
|
||||
integrity sha512-38yiWLemQf7aLHDgTg85fh3hW9stJ0Muk7+s6tIkSUOMmi4Xbv5pH/5Bofnsb6spIwD5FJiR+jg71f0CH5OzoA==
|
||||
|
||||
"@rollup/rollup-linux-powerpc64le-gnu@4.24.4":
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.4.tgz#a9418a4173df80848c0d47df0426a0bf183c4e75"
|
||||
integrity sha512-q73XUPnkwt9ZNF2xRS4fvneSuaHw2BXuV5rI4cw0fWYVIWIBeDZX7c7FWhFQPNTnE24172K30I+dViWRVD9TwA==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.24.4":
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.4.tgz#bc9c195db036a27e5e3339b02f51526b4ce1e988"
|
||||
integrity sha512-Aie/TbmQi6UXokJqDZdmTJuZBCU3QBDA8oTKRGtd4ABi/nHgXICulfg1KI6n9/koDsiDbvHAiQO3YAUNa/7BCw==
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu@4.24.4":
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.4.tgz#1651fdf8144ae89326c01da5d52c60be63e71a82"
|
||||
integrity sha512-P8MPErVO/y8ohWSP9JY7lLQ8+YMHfTI4bAdtCi3pC2hTeqFJco2jYspzOzTUB8hwUWIIu1xwOrJE11nP+0JFAQ==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.24.4":
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.4.tgz#e473de5e4acb95fcf930a35cbb7d3e8080e57a6f"
|
||||
integrity sha512-K03TljaaoPK5FOyNMZAAEmhlyO49LaE4qCsr0lYHUKyb6QacTNF9pnfPpXnFlFD3TXuFbFbz7tJ51FujUXkXYA==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.24.4":
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.4.tgz#0af12dd2578c29af4037f0c834b4321429dd5b01"
|
||||
integrity sha512-VJYl4xSl/wqG2D5xTYncVWW+26ICV4wubwN9Gs5NrqhJtayikwCXzPL8GDsLnaLU3WwhQ8W02IinYSFJfyo34Q==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.24.4":
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.4.tgz#e48e78cdd45313b977c1390f4bfde7ab79be8871"
|
||||
integrity sha512-ku2GvtPwQfCqoPFIJCqZ8o7bJcj+Y54cZSr43hHca6jLwAiCbZdBUOrqE6y29QFajNAzzpIOwsckaTFmN6/8TA==
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@4.24.4":
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.4.tgz#a3fc8536d243fe161c796acb93eba43c250f311c"
|
||||
integrity sha512-V3nCe+eTt/W6UYNr/wGvO1fLpHUrnlirlypZfKCT1fG6hWfqhPgQV/K/mRBXBpxc0eKLIF18pIOFVPh0mqHjlg==
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@4.24.4":
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.4.tgz#e2a9d1fd56524103a6cc8a54404d9d3ebc73c454"
|
||||
integrity sha512-LTw1Dfd0mBIEqUVCxbvTE/LLo+9ZxVC9k99v1v4ahg9Aak6FpqOfNu5kRkeTAn0wphoC4JU7No1/rL+bBCEwhg==
|
||||
|
||||
"@rollup/wasm-node@^4.19.0":
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/wasm-node/-/wasm-node-4.24.4.tgz#11d78f5cc85b04e81468f245c2dc0885d06d663d"
|
||||
integrity sha512-WKJUdPcM8YAYujafY95+2EapqU3F/nwfBkXh9AfkBvWBwFhsvNJABA86Br6graRH2vRE4FBsiqjFvFWOtEO6wg==
|
||||
dependencies:
|
||||
"@types/estree" "1.0.6"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
"@shikijs/core@1.9.0":
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-1.9.0.tgz#ff717fef5e0e9882f0848272699fd8f04d6f9a07"
|
||||
@@ -71,11 +297,26 @@
|
||||
traverse "^0.6.7"
|
||||
unified "^9.2.2"
|
||||
|
||||
"@ts-morph/common@~0.22.0":
|
||||
version "0.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.22.0.tgz#8951d451622a26472fbc3a227d6c3a90e687a683"
|
||||
integrity sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==
|
||||
dependencies:
|
||||
fast-glob "^3.3.2"
|
||||
minimatch "^9.0.3"
|
||||
mkdirp "^3.0.1"
|
||||
path-browserify "^1.0.1"
|
||||
|
||||
"@tsconfig/recommended@^1.0.2":
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/recommended/-/recommended-1.0.6.tgz#217b78f9601215939d566a79d202a760ae185114"
|
||||
integrity sha512-0IKu9GHYF1NGTJiYgfWwqnOQSlnE9V9R7YohHNNf0/fj/SyOZWzdd06JFr0fLpg1Mqw0kGbYg8w5xdkSqLKM9g==
|
||||
|
||||
"@types/estree@1.0.6":
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
|
||||
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
|
||||
|
||||
"@types/json-schema@^7.0.15":
|
||||
version "7.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
|
||||
@@ -127,6 +368,16 @@ anchor-markdown-header@^0.6.0:
|
||||
dependencies:
|
||||
emoji-regex "~10.1.0"
|
||||
|
||||
ansi-regex@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
||||
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
|
||||
|
||||
ansi-regex@^6.0.1:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654"
|
||||
integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==
|
||||
|
||||
ansi-styles@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||
@@ -134,6 +385,18 @@ ansi-styles@^3.2.1:
|
||||
dependencies:
|
||||
color-convert "^1.9.0"
|
||||
|
||||
ansi-styles@^4.0.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
|
||||
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
|
||||
dependencies:
|
||||
color-convert "^2.0.1"
|
||||
|
||||
ansi-styles@^6.1.0:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
|
||||
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
|
||||
|
||||
argparse@^1.0.7:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
|
||||
@@ -178,6 +441,11 @@ arrify@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
|
||||
integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
available-typed-arrays@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846"
|
||||
@@ -185,6 +453,15 @@ available-typed-arrays@^1.0.7:
|
||||
dependencies:
|
||||
possible-typed-array-names "^1.0.0"
|
||||
|
||||
axios@^1.6.7:
|
||||
version "1.7.7"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
|
||||
integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.6"
|
||||
form-data "^4.0.0"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
bail@^1.0.0:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776"
|
||||
@@ -195,6 +472,11 @@ balanced-match@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
before-after-hook@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-3.0.2.tgz#d5665a5fa8b62294a5aa0a499f933f4a1016195d"
|
||||
integrity sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==
|
||||
|
||||
brace-expansion@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
|
||||
@@ -263,6 +545,11 @@ character-reference-invalid@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
|
||||
integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==
|
||||
|
||||
code-block-writer@^12.0.0:
|
||||
version "12.0.0"
|
||||
resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-12.0.0.tgz#4dd58946eb4234105aff7f0035977b2afdc2a770"
|
||||
integrity sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==
|
||||
|
||||
color-convert@^1.9.0:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
@@ -270,11 +557,35 @@ color-convert@^1.9.0:
|
||||
dependencies:
|
||||
color-name "1.1.3"
|
||||
|
||||
color-convert@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
|
||||
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
|
||||
dependencies:
|
||||
color-name "~1.1.4"
|
||||
|
||||
color-name@1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
|
||||
|
||||
color-name@~1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
||||
combined-stream@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
commander@^11.1.0:
|
||||
version "11.1.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906"
|
||||
integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==
|
||||
|
||||
concat-md@^0.5.1:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-md/-/concat-md-0.5.1.tgz#03c72343a5d81306aa5ae1040d6368ffbc444781"
|
||||
@@ -287,6 +598,15 @@ concat-md@^0.5.1:
|
||||
meow "^9.0.0"
|
||||
transform-markdown-links "^2.0.0"
|
||||
|
||||
cross-spawn@^7.0.0:
|
||||
version "7.0.5"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.5.tgz#910aac880ff5243da96b728bc6521a5f6c2f2f82"
|
||||
integrity sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==
|
||||
dependencies:
|
||||
path-key "^3.1.0"
|
||||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
data-view-buffer@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2"
|
||||
@@ -352,6 +672,11 @@ define-properties@^1.2.0, define-properties@^1.2.1:
|
||||
has-property-descriptors "^1.0.0"
|
||||
object-keys "^1.1.1"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
dir-glob@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
|
||||
@@ -401,6 +726,21 @@ domutils@^2.8.0:
|
||||
domelementtype "^2.2.0"
|
||||
domhandler "^4.2.0"
|
||||
|
||||
eastasianwidth@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
|
||||
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
|
||||
|
||||
emoji-regex@^9.2.2:
|
||||
version "9.2.2"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
|
||||
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
|
||||
|
||||
emoji-regex@~10.1.0:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.1.0.tgz#d50e383743c0f7a5945c47087295afc112e3cf66"
|
||||
@@ -542,7 +882,7 @@ extend@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
|
||||
|
||||
fast-glob@^3.2.9:
|
||||
fast-glob@^3.2.9, fast-glob@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
|
||||
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
|
||||
@@ -582,6 +922,11 @@ find-up@^4.1.0:
|
||||
locate-path "^5.0.0"
|
||||
path-exists "^4.0.0"
|
||||
|
||||
follow-redirects@^1.15.6:
|
||||
version "1.15.9"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
|
||||
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
|
||||
|
||||
for-each@^0.3.3:
|
||||
version "0.3.3"
|
||||
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
|
||||
@@ -589,6 +934,23 @@ for-each@^0.3.3:
|
||||
dependencies:
|
||||
is-callable "^1.1.3"
|
||||
|
||||
foreground-child@^3.1.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77"
|
||||
integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==
|
||||
dependencies:
|
||||
cross-spawn "^7.0.0"
|
||||
signal-exit "^4.0.1"
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48"
|
||||
integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
format@^0.2.0:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
|
||||
@@ -601,6 +963,11 @@ front-matter@^4.0.2:
|
||||
dependencies:
|
||||
js-yaml "^3.13.1"
|
||||
|
||||
fsevents@~2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
||||
function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
|
||||
@@ -648,6 +1015,18 @@ glob-parent@^5.1.2:
|
||||
dependencies:
|
||||
is-glob "^4.0.1"
|
||||
|
||||
glob@^10.3.10, glob@^10.3.7:
|
||||
version "10.4.5"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
|
||||
integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==
|
||||
dependencies:
|
||||
foreground-child "^3.1.0"
|
||||
jackspeak "^3.1.2"
|
||||
minimatch "^9.0.4"
|
||||
minipass "^7.1.2"
|
||||
package-json-from-dist "^1.0.0"
|
||||
path-scurry "^1.11.1"
|
||||
|
||||
globalthis@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236"
|
||||
@@ -844,6 +1223,11 @@ is-extglob@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
||||
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
|
||||
|
||||
is-fullwidth-code-point@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
|
||||
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
|
||||
|
||||
is-glob@^4.0.1:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
|
||||
@@ -931,6 +1315,20 @@ isarray@^2.0.5:
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
|
||||
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
|
||||
|
||||
isexe@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
|
||||
|
||||
jackspeak@^3.1.2:
|
||||
version "3.4.3"
|
||||
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a"
|
||||
integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==
|
||||
dependencies:
|
||||
"@isaacs/cliui" "^8.0.2"
|
||||
optionalDependencies:
|
||||
"@pkgjs/parseargs" "^0.11.0"
|
||||
|
||||
js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
@@ -978,11 +1376,21 @@ lodash.startcase@^4.4.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.startcase/-/lodash.startcase-4.4.0.tgz#9436e34ed26093ed7ffae1936144350915d9add8"
|
||||
integrity sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==
|
||||
|
||||
lodash@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
longest-streak@^2.0.0:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4"
|
||||
integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==
|
||||
|
||||
lru-cache@^10.2.0:
|
||||
version "10.4.3"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
|
||||
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||
@@ -1221,11 +1629,30 @@ micromatch@^4.0.4:
|
||||
braces "^3.0.3"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
mime-db@1.52.0:
|
||||
version "1.52.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@^2.1.12:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
dependencies:
|
||||
mime-db "1.52.0"
|
||||
|
||||
min-indent@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
|
||||
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
|
||||
|
||||
minimatch@^9.0.3:
|
||||
version "9.0.5"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
|
||||
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimatch@^9.0.4:
|
||||
version "9.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51"
|
||||
@@ -1247,6 +1674,16 @@ minimist@^1.2.6:
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
|
||||
"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
|
||||
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
|
||||
|
||||
mkdirp@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50"
|
||||
integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==
|
||||
|
||||
ms@2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
@@ -1339,6 +1776,11 @@ p-try@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
|
||||
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
|
||||
|
||||
package-json-from-dist@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505"
|
||||
integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==
|
||||
|
||||
parse-entities@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8"
|
||||
@@ -1361,16 +1803,34 @@ parse-json@^5.0.0:
|
||||
json-parse-even-better-errors "^2.3.0"
|
||||
lines-and-columns "^1.1.6"
|
||||
|
||||
path-browserify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
|
||||
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
|
||||
|
||||
path-exists@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
|
||||
integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
|
||||
|
||||
path-key@^3.1.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
|
||||
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
|
||||
|
||||
path-parse@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
|
||||
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
|
||||
|
||||
path-scurry@^1.11.1:
|
||||
version "1.11.1"
|
||||
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2"
|
||||
integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==
|
||||
dependencies:
|
||||
lru-cache "^10.2.0"
|
||||
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
|
||||
path-type@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
|
||||
@@ -1396,6 +1856,11 @@ prettier@^3.2.5:
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368"
|
||||
integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==
|
||||
|
||||
proxy-from-env@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||
|
||||
punycode.js@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
|
||||
@@ -1430,6 +1895,11 @@ read-pkg@^5.2.0:
|
||||
parse-json "^5.0.0"
|
||||
type-fest "^0.6.0"
|
||||
|
||||
readline@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/readline/-/readline-1.3.0.tgz#c580d77ef2cfc8752b132498060dc9793a7ac01c"
|
||||
integrity sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==
|
||||
|
||||
redent@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
|
||||
@@ -1503,6 +1973,40 @@ reusify@^1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
|
||||
|
||||
rimraf@^5.0.1:
|
||||
version "5.0.10"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.10.tgz#23b9843d3dc92db71f96e1a2ce92e39fd2a8221c"
|
||||
integrity sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==
|
||||
dependencies:
|
||||
glob "^10.3.7"
|
||||
|
||||
rollup@^4.5.2:
|
||||
version "4.24.4"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.24.4.tgz#fdc76918de02213c95447c9ffff5e35dddb1d058"
|
||||
integrity sha512-vGorVWIsWfX3xbcyAS+I047kFKapHYivmkaT63Smj77XwvLSJos6M1xGqZnBPFQFBRZDOcG1QnYEIxAvTr/HjA==
|
||||
dependencies:
|
||||
"@types/estree" "1.0.6"
|
||||
optionalDependencies:
|
||||
"@rollup/rollup-android-arm-eabi" "4.24.4"
|
||||
"@rollup/rollup-android-arm64" "4.24.4"
|
||||
"@rollup/rollup-darwin-arm64" "4.24.4"
|
||||
"@rollup/rollup-darwin-x64" "4.24.4"
|
||||
"@rollup/rollup-freebsd-arm64" "4.24.4"
|
||||
"@rollup/rollup-freebsd-x64" "4.24.4"
|
||||
"@rollup/rollup-linux-arm-gnueabihf" "4.24.4"
|
||||
"@rollup/rollup-linux-arm-musleabihf" "4.24.4"
|
||||
"@rollup/rollup-linux-arm64-gnu" "4.24.4"
|
||||
"@rollup/rollup-linux-arm64-musl" "4.24.4"
|
||||
"@rollup/rollup-linux-powerpc64le-gnu" "4.24.4"
|
||||
"@rollup/rollup-linux-riscv64-gnu" "4.24.4"
|
||||
"@rollup/rollup-linux-s390x-gnu" "4.24.4"
|
||||
"@rollup/rollup-linux-x64-gnu" "4.24.4"
|
||||
"@rollup/rollup-linux-x64-musl" "4.24.4"
|
||||
"@rollup/rollup-win32-arm64-msvc" "4.24.4"
|
||||
"@rollup/rollup-win32-ia32-msvc" "4.24.4"
|
||||
"@rollup/rollup-win32-x64-msvc" "4.24.4"
|
||||
fsevents "~2.3.2"
|
||||
|
||||
run-parallel@^1.1.9:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
|
||||
@@ -1561,6 +2065,18 @@ set-function-name@^2.0.1:
|
||||
functions-have-names "^1.2.3"
|
||||
has-property-descriptors "^1.0.2"
|
||||
|
||||
shebang-command@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
|
||||
integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
|
||||
dependencies:
|
||||
shebang-regex "^3.0.0"
|
||||
|
||||
shebang-regex@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
|
||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
||||
|
||||
shiki@^1.9.0:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/shiki/-/shiki-1.9.0.tgz#e4d3a044d9c746aefbea47615e83323fdc3dc361"
|
||||
@@ -1578,6 +2094,11 @@ side-channel@^1.0.4:
|
||||
get-intrinsic "^1.2.4"
|
||||
object-inspect "^1.13.1"
|
||||
|
||||
signal-exit@^4.0.1:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
|
||||
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
|
||||
|
||||
slash@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
||||
@@ -1614,6 +2135,33 @@ sprintf-js@~1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||
integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^5.0.1, string-width@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
|
||||
integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
|
||||
dependencies:
|
||||
eastasianwidth "^0.2.0"
|
||||
emoji-regex "^9.2.2"
|
||||
strip-ansi "^7.0.1"
|
||||
|
||||
string.prototype.trim@^1.2.9:
|
||||
version "1.2.9"
|
||||
resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4"
|
||||
@@ -1642,6 +2190,27 @@ string.prototype.trimstart@^1.0.8:
|
||||
define-properties "^1.2.1"
|
||||
es-object-atoms "^1.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^7.0.1:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
|
||||
integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
|
||||
dependencies:
|
||||
ansi-regex "^6.0.1"
|
||||
|
||||
strip-indent@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
|
||||
@@ -1692,6 +2261,14 @@ trough@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
|
||||
integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==
|
||||
|
||||
ts-morph@^21.0.1:
|
||||
version "21.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-21.0.1.tgz#712302a0f6e9dbf1aa8d9cf33a4386c4b18c2006"
|
||||
integrity sha512-dbDtVdEAncKctzrVZ+Nr7kHpHkv+0JDJb2MjjpBaj8bFeCkePU9rHfMklmhuLFnpeq/EJZk2IhStY6NzqgjOkg==
|
||||
dependencies:
|
||||
"@ts-morph/common" "~0.22.0"
|
||||
code-block-writer "^12.0.0"
|
||||
|
||||
type-fest@^0.18.0:
|
||||
version "0.18.1"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f"
|
||||
@@ -1841,6 +2418,11 @@ unist-util-visit-parents@^3.0.0:
|
||||
"@types/unist" "^2.0.0"
|
||||
unist-util-is "^4.0.0"
|
||||
|
||||
universal-user-agent@^7.0.0, universal-user-agent@^7.0.2:
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-7.0.2.tgz#52e7d0e9b3dc4df06cc33cb2b9fd79041a54827e"
|
||||
integrity sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==
|
||||
|
||||
update-section@^0.3.3:
|
||||
version "0.3.3"
|
||||
resolved "https://registry.yarnpkg.com/update-section/-/update-section-0.3.3.tgz#458f17820d37820dc60e20b86d94391b00123158"
|
||||
@@ -1899,6 +2481,31 @@ which-typed-array@^1.1.14, which-typed-array@^1.1.15:
|
||||
gopd "^1.0.1"
|
||||
has-tostringtag "^1.0.2"
|
||||
|
||||
which@^2.0.1:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
||||
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
|
||||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||
integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
|
||||
dependencies:
|
||||
ansi-styles "^6.1.0"
|
||||
string-width "^5.0.1"
|
||||
strip-ansi "^7.0.1"
|
||||
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
|
||||
@@ -36,6 +36,7 @@ from langgraph_sdk.schema import (
|
||||
AssistantVersion,
|
||||
CancelAction,
|
||||
Checkpoint,
|
||||
Command,
|
||||
Config,
|
||||
Cron,
|
||||
DisconnectMode,
|
||||
@@ -1174,6 +1175,7 @@ class RunsClient:
|
||||
assistant_id: str,
|
||||
*,
|
||||
input: Optional[dict] = None,
|
||||
command: Optional[Command] = None,
|
||||
stream_mode: Union[StreamMode, Sequence[StreamMode]] = "values",
|
||||
stream_subgraphs: bool = False,
|
||||
metadata: Optional[dict] = None,
|
||||
@@ -1197,6 +1199,7 @@ class RunsClient:
|
||||
assistant_id: str,
|
||||
*,
|
||||
input: Optional[dict] = None,
|
||||
command: Optional[Command] = None,
|
||||
stream_mode: Union[StreamMode, Sequence[StreamMode]] = "values",
|
||||
stream_subgraphs: bool = False,
|
||||
metadata: Optional[dict] = None,
|
||||
@@ -1217,6 +1220,7 @@ class RunsClient:
|
||||
assistant_id: str,
|
||||
*,
|
||||
input: Optional[dict] = None,
|
||||
command: Optional[Command] = None,
|
||||
stream_mode: Union[StreamMode, Sequence[StreamMode]] = "values",
|
||||
stream_subgraphs: bool = False,
|
||||
metadata: Optional[dict] = None,
|
||||
@@ -1241,6 +1245,7 @@ class RunsClient:
|
||||
assistant_id: The assistant ID or graph name to stream from.
|
||||
If using graph name, will default to first assistant created from that graph.
|
||||
input: The input to the graph.
|
||||
command: A command to execute. Cannot be combined with input.
|
||||
stream_mode: The stream mode(s) to use.
|
||||
stream_subgraphs: Whether to stream output from subgraphs.
|
||||
metadata: Metadata to assign to the run.
|
||||
@@ -1291,6 +1296,7 @@ class RunsClient:
|
||||
""" # noqa: E501
|
||||
payload = {
|
||||
"input": input,
|
||||
"command": command,
|
||||
"config": config,
|
||||
"metadata": metadata,
|
||||
"stream_mode": stream_mode,
|
||||
@@ -1324,6 +1330,7 @@ class RunsClient:
|
||||
assistant_id: str,
|
||||
*,
|
||||
input: Optional[dict] = None,
|
||||
command: Optional[Command] = None,
|
||||
stream_mode: Union[StreamMode, Sequence[StreamMode]] = "values",
|
||||
stream_subgraphs: bool = False,
|
||||
metadata: Optional[dict] = None,
|
||||
@@ -1343,6 +1350,7 @@ class RunsClient:
|
||||
assistant_id: str,
|
||||
*,
|
||||
input: Optional[dict] = None,
|
||||
command: Optional[Command] = None,
|
||||
stream_mode: Union[StreamMode, Sequence[StreamMode]] = "values",
|
||||
stream_subgraphs: bool = False,
|
||||
metadata: Optional[dict] = None,
|
||||
@@ -1363,6 +1371,7 @@ class RunsClient:
|
||||
assistant_id: str,
|
||||
*,
|
||||
input: Optional[dict] = None,
|
||||
command: Optional[Command] = None,
|
||||
stream_mode: Union[StreamMode, Sequence[StreamMode]] = "values",
|
||||
stream_subgraphs: bool = False,
|
||||
metadata: Optional[dict] = None,
|
||||
@@ -1385,6 +1394,7 @@ class RunsClient:
|
||||
assistant_id: The assistant ID or graph name to stream from.
|
||||
If using graph name, will default to first assistant created from that graph.
|
||||
input: The input to the graph.
|
||||
command: A command to execute. Cannot be combined with input.
|
||||
stream_mode: The stream mode(s) to use.
|
||||
stream_subgraphs: Whether to stream output from subgraphs.
|
||||
metadata: Metadata to assign to the run.
|
||||
@@ -1471,6 +1481,7 @@ class RunsClient:
|
||||
""" # noqa: E501
|
||||
payload = {
|
||||
"input": input,
|
||||
"command": command,
|
||||
"stream_mode": stream_mode,
|
||||
"stream_subgraphs": stream_subgraphs,
|
||||
"config": config,
|
||||
@@ -1508,6 +1519,7 @@ class RunsClient:
|
||||
assistant_id: str,
|
||||
*,
|
||||
input: Optional[dict] = None,
|
||||
command: Optional[Command] = None,
|
||||
metadata: Optional[dict] = None,
|
||||
config: Optional[Config] = None,
|
||||
checkpoint: Optional[Checkpoint] = None,
|
||||
@@ -1529,6 +1541,7 @@ class RunsClient:
|
||||
assistant_id: str,
|
||||
*,
|
||||
input: Optional[dict] = None,
|
||||
command: Optional[Command] = None,
|
||||
metadata: Optional[dict] = None,
|
||||
config: Optional[Config] = None,
|
||||
interrupt_before: Optional[Union[All, Sequence[str]]] = None,
|
||||
@@ -1547,6 +1560,7 @@ class RunsClient:
|
||||
assistant_id: str,
|
||||
*,
|
||||
input: Optional[dict] = None,
|
||||
command: Optional[Command] = None,
|
||||
metadata: Optional[dict] = None,
|
||||
config: Optional[Config] = None,
|
||||
checkpoint: Optional[Checkpoint] = None,
|
||||
@@ -1569,6 +1583,7 @@ class RunsClient:
|
||||
assistant_id: The assistant ID or graph name to run.
|
||||
If using graph name, will default to first assistant created from that graph.
|
||||
input: The input to the graph.
|
||||
command: A command to execute. Cannot be combined with input.
|
||||
metadata: Metadata to assign to the run.
|
||||
config: The configuration for the assistant.
|
||||
checkpoint: The checkpoint to resume from.
|
||||
@@ -1635,6 +1650,7 @@ class RunsClient:
|
||||
""" # noqa: E501
|
||||
payload = {
|
||||
"input": input,
|
||||
"command": command,
|
||||
"config": config,
|
||||
"metadata": metadata,
|
||||
"assistant_id": assistant_id,
|
||||
|
||||
@@ -339,3 +339,14 @@ class StreamPart(NamedTuple):
|
||||
"""The type of event for this stream part."""
|
||||
data: dict
|
||||
"""The data payload associated with the event."""
|
||||
|
||||
|
||||
class Send(TypedDict):
|
||||
node: str
|
||||
input: Optional[dict[str, Any]]
|
||||
|
||||
|
||||
class Command(TypedDict, total=False):
|
||||
send: Union[Send, Sequence[Send]]
|
||||
update: dict[str, Any]
|
||||
resume: Any
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.1.35"
|
||||
version = "0.1.36"
|
||||
description = "SDK for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
Generated
+4
-4
@@ -2,13 +2,13 @@
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
version = "2.4.2"
|
||||
version = "2.4.3"
|
||||
description = "Happy Eyeballs for asyncio"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "aiohappyeyeballs-2.4.2-py3-none-any.whl", hash = "sha256:8522691d9a154ba1145b157d6d5c15e5c692527ce6a53c5e5f9876977f6dab2f"},
|
||||
{file = "aiohappyeyeballs-2.4.2.tar.gz", hash = "sha256:4ca893e6c5c1f5bf3888b04cb5a3bee24995398efef6e0b9f747b5e89d84fd74"},
|
||||
{file = "aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572"},
|
||||
{file = "aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7331,4 +7331,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "67675531a6c4cd218c9effe87061234fbc778ff1afbb61327239b20556aa63da"
|
||||
content-hash = "738e69cf406b140217cc8c3c0f2ccb5c2027d4701bcf2a347ba4b107f700ab2a"
|
||||
|
||||
@@ -8,6 +8,7 @@ readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
aiohappyeyeballs = "2.4.3"
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
langgraph = { path = "libs/langgraph/", develop = true }
|
||||
|
||||
Reference in New Issue
Block a user