mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-29 19:29:43 +02:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3778f6113c | ||
|
|
24c13c211e | ||
|
|
2e36189c16 | ||
|
|
dac11f875c | ||
|
|
264be423f9 | ||
|
|
9a05600ff9 | ||
|
|
56dd728975 | ||
|
|
c157c956f4 | ||
|
|
dd293dad30 | ||
|
|
30811d7841 | ||
|
|
28a705b71a | ||
|
|
3b8130b96f | ||
|
|
94fc0adb05 | ||
|
|
162e96262f | ||
|
|
1bb0037450 | ||
|
|
50c53d3120 | ||
|
|
0b7b849633 | ||
|
|
678eb5cdbe | ||
|
|
bdf1215ced | ||
|
|
cba1852720 | ||
|
|
b8fafa2795 | ||
|
|
e6681bc175 | ||
|
|
a9cdb9c948 | ||
|
|
4dc8f813e7 |
@@ -39,6 +39,7 @@ jobs:
|
||||
- 'libs/checkpoint-sqlite/**'
|
||||
- 'libs/checkpoint-postgres/**'
|
||||
- 'libs/scheduler-kafka/**'
|
||||
- 'libs/prebuilt/**'
|
||||
sdk-js:
|
||||
- 'libs/sdk-js/**'
|
||||
|
||||
@@ -56,6 +57,7 @@ jobs:
|
||||
"libs/checkpoint-sqlite",
|
||||
"libs/checkpoint-postgres",
|
||||
"libs/scheduler-kafka",
|
||||
"libs/prebuilt",
|
||||
]
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
uses: ./.github/workflows/_lint.yml
|
||||
@@ -74,6 +76,7 @@ jobs:
|
||||
"libs/checkpoint",
|
||||
"libs/checkpoint-sqlite",
|
||||
"libs/checkpoint-postgres",
|
||||
"libs/prebuilt",
|
||||
]
|
||||
if: needs.changes.outputs.python == 'true'
|
||||
uses: ./.github/workflows/_test.yml
|
||||
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
"$PKG_NAME==$VERSION" \
|
||||
)
|
||||
|
||||
if [[ "$PKG_NAME" == *checkpoint* ]]; then
|
||||
if [[ "$PKG_NAME" == *checkpoint* || "$PKG_NAME" == *prebuilt* ]]; then
|
||||
# since checkpoint packages are namespace packages, import them with . convention
|
||||
# i.e. import langgraph.checkpoint or langgraph.checkpoint.sqlite
|
||||
IMPORT_NAME="$(echo "$PKG_NAME" | sed s/-/./g)"
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
matrix:
|
||||
lib-version:
|
||||
- "development"
|
||||
- "latest"
|
||||
# - "latest"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -70,7 +70,7 @@ pip install -U langgraph
|
||||
Let's build a tool-calling [ReAct-style](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#react-implementation) agent that uses a search tool!
|
||||
|
||||
```shell
|
||||
pip install langchain-anthropic
|
||||
pip install langgraph-prebuilt langchain-anthropic
|
||||
```
|
||||
|
||||
```shell
|
||||
|
||||
@@ -83,14 +83,18 @@ def generate_markdown(resolved_packages: List[ResolvedPackage], language: str) -
|
||||
resolved_packages, key=lambda p: p["weekly_downloads"] or 0, reverse=True
|
||||
)
|
||||
rows = [
|
||||
"| Name | GitHub URL | Description | Weekly Downloads |",
|
||||
"| --- | --- | --- | --- |",
|
||||
"| Name | GitHub URL | Description | Weekly Downloads | Stars |",
|
||||
"| --- | --- | --- | --- | --- |",
|
||||
]
|
||||
for package in sorted_packages:
|
||||
name = f"**{package['name']}**"
|
||||
repo_url = f"[{package['repo']}](https://github.com/{package['repo']})"
|
||||
stars_badge = (
|
||||
f"https://img.shields.io/github/stars/{package['repo']}?style=social"
|
||||
)
|
||||
stars = f""
|
||||
downloads = package["weekly_downloads"] or "-"
|
||||
row = f"| {name} | {repo_url} | {package['description']} | {downloads} |"
|
||||
row = f"| {name} | {repo_url} | {package['description']} | {downloads} | {stars}"
|
||||
rows.append(row)
|
||||
markdown_content = MARKDOWN.format(
|
||||
library_list="\n".join(rows), langgraph_url=langgraph_url
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
packages:
|
||||
- name: "trustcall"
|
||||
repo: "hinthornw/trustcall"
|
||||
description: "Tenacious tool calling built on LangGraph"
|
||||
description: "Tenacious tool calling built on LangGraph."
|
||||
- name: "breeze-agent"
|
||||
repo: "andrestorres123/breeze-agent"
|
||||
description: "A streamlined research system built inspired on STORM and built on LangGraph"
|
||||
description: "A streamlined research system built inspired on STORM and built on LangGraph."
|
||||
- name: "langgraph-supervisor"
|
||||
repo: "langchain-ai/langgraph-supervisor"
|
||||
description: "Build supervisor multi-agent systems with LangGraph"
|
||||
repo: "langchain-ai/langgraph-supervisor-py"
|
||||
description: "Build supervisor multi-agent systems with LangGraph."
|
||||
- name: "langmem"
|
||||
repo: "langchain-ai/langmem"
|
||||
description: "Build agents that learn and adapt from interactions over time."
|
||||
@@ -18,3 +18,9 @@ packages:
|
||||
- name: "open-deep-research"
|
||||
repo: "langchain-ai/open_deep_research"
|
||||
description: "Open source assistant for iterative web research and report writing."
|
||||
- name: "langgraph-swarm"
|
||||
repo: "langchain-ai/langgraph-swarm-py"
|
||||
description: "Build swarm-style multi-agent systems using LangGraph."
|
||||
- name: "delve-taxonomy-generator"
|
||||
repo: "andrestorres123/delve"
|
||||
description: "A taxonomy generator for unstructured data"
|
||||
@@ -9,6 +9,7 @@ This list of companies using LangGraph and their success stories is compiled fro
|
||||
| [AppFolio](https://www.appfolio.com/) | Real Estate | Copilot for domain-specific task | [Case study, 2024](https://blog.langchain.dev/customers-appfolio/) |
|
||||
| [Athena Intelligence](https://www.athenaintel.com/) | Software & Technology (GenAI Native) | Research & summarization | [Case study, 2024](https://blog.langchain.dev/customers-athena-intelligence/) |
|
||||
| [Captide](https://www.captide.co/) | Software & Technology (GenAI Native) | Data extraction | [Case study, 2025](https://blog.langchain.dev/how-captide-is-redefining-equity-research-with-agentic-workflows-built-on-langgraph-and-langsmith/) |
|
||||
| [Cisco Outshift](https://outshift.cisco.com/) | Software & Technology | DevOps | [Blog post, 2025](https://outshift.cisco.com/blog/build-react-agent-application-for-devops-tasks-using-rest-apis) |
|
||||
| [Elastic](https://www.elastic.co/) | Software & Technology | Copilot for domain-specific task | [Blog post, 2025](https://www.elastic.co/blog/elastic-security-generative-ai-features) |
|
||||
| [GitLab](https://about.gitlab.com/) | Software & Technology | Code generation | [Duo workflow docs](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/duo_workflow/) |
|
||||
| [Infor](https://infor.com/) | Software & Technology | GenAI embedded product experiences; customer support; copilot | [Case study, 2025](https://blog.langchain.dev/customers-infor/) |
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,12 @@
|
||||
|
||||
The LangGraph Cloud Server supports specific environment variables for configuring a deployment.
|
||||
|
||||
## `DD_API_KEY`
|
||||
|
||||
Specify `DD_API_KEY` (your [Datadog API Key](https://docs.datadoghq.com/account_management/api-app-keys/)) to automatically enable Datadog tracing for the deployment. Specify other [`DD_*` environment variables](https://ddtrace.readthedocs.io/en/stable/configuration.html) to configure the tracing instrumentation.
|
||||
|
||||
If `DD_API_KEY` is specified, the application process is wrapped in the [`ddtrace-run` command](https://ddtrace.readthedocs.io/en/stable/installation_quickstart.html). Other `DD_*` environment variables (e.g. `DD_SITE`, `DD_ENV`, `DD_SERVICE`, `DD_TRACE_ENABLED`) are typically needed to properly configure the tracing instrumentation. See [`DD_*` environment variables](https://ddtrace.readthedocs.io/en/stable/configuration.html) for more details.
|
||||
|
||||
## `LANGCHAIN_TRACING_SAMPLING_RATE`
|
||||
|
||||
Sampling rate for traces sent to LangSmith. Valid values: Any float between `0` and `1`.
|
||||
|
||||
@@ -62,3 +62,9 @@ Yes! You can use LangGraph with any LLMs. The main reason we use LLMs that suppo
|
||||
## Does LangGraph work with OSS LLMs?
|
||||
|
||||
Yes! LangGraph is totally ambivalent to what LLMs are used under the hood. The main reason we use closed LLMs in most of the tutorials is that they seamlessly support tool calling, while OSS LLMs often don't. But tool calling is not necessary (see [this section](#does-langgraph-work-with-llms-that-dont-support-tool-calling)) so you can totally use LangGraph with OSS LLMs.
|
||||
|
||||
## Can I use LangGraph Studio without logging to LangSmith
|
||||
|
||||
Yes! You can use the [development version of LangGraph Server](../tutorials/langgraph-platform/local-server.md) to run the backend locally.
|
||||
This will connect to the studio frontend hosted as part of LangSmith.
|
||||
If you set an environment variable of `LANGSMITH_TRACING=false` then no traces will be sent to LangSmith.
|
||||
@@ -310,7 +310,7 @@ graph.add_conditional_edges(START, routing_function, {True: "node_b", False: "no
|
||||
|
||||
## `Send`
|
||||
|
||||
By default, `Nodes` and `Edges` are defined ahead of time and operate on the same shared state. However, there can be cases where the exact edges are not known ahead of time and/or you may want different versions of `State` to exist at the same time. A common example of this is with `map-reduce` design patterns. In this design pattern, a first node may generate a list of objects, and you may want to apply some other node to all those objects. The number of objects may be unknown ahead of time (meaning the number of edges may not be known) and the input `State` to the downstream `Node` should be different (one for each generated object).
|
||||
By default, `Nodes` and `Edges` are defined ahead of time and operate on the same shared state. However, there can be cases where the exact edges are not known ahead of time and/or you may want different versions of `State` to exist at the same time. A common example of this is with [map-reduce](https://langchain-ai.github.io/langgraph/how-tos/map-reduce/) design patterns. In this design pattern, a first node may generate a list of objects, and you may want to apply some other node to all those objects. The number of objects may be unknown ahead of time (meaning the number of edges may not be known) and the input `State` to the downstream `Node` should be different (one for each generated object).
|
||||
|
||||
To support this design pattern, LangGraph supports returning [`Send`][langgraph.types.Send] objects from conditional edges. `Send` takes two arguments: first is the name of the node, and second is the state to pass to that node.
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain-anthropic"
|
||||
"%pip install -U langgraph langgraph-prebuilt langchain-anthropic"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install --quiet -U langgraph langchain_anthropic"
|
||||
"%pip install --quiet -U langgraph langgraph-prebuilt langchain_anthropic"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain-openai"
|
||||
"%pip install -U langgraph langgraph-prebuilt langchain-openai"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain-openai"
|
||||
"%pip install -U langgraph langgraph-prebuilt langchain-openai"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain-openai"
|
||||
"%pip install -U langgraph langgraph-prebuilt langchain-openai"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain-openai"
|
||||
"%pip install -U langgraph langgraph-prebuilt langchain-openai"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain-openai"
|
||||
"%pip install -U langgraph langgraph-prebuilt langchain-openai"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -71,7 +71,7 @@ If you navigate to `localhost:2024/hello` in your browser (2024 is the default d
|
||||
|
||||
## Deploying
|
||||
|
||||
You can deploy this app as-is to the managed langgraph cloud or to your self-hsoted platform.
|
||||
You can deploy this app as-is to the managed langgraph cloud or to your self-hosted platform.
|
||||
|
||||
## Next steps
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain-openai langchain"
|
||||
"%pip install -U langgraph langgraph-prebuilt langchain-openai langchain"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# %%capture --no-stderr\n",
|
||||
"# %pip install -U langgraph langchain-anthropic"
|
||||
"# %pip install -U langgraph langgraph-prebuilt langchain-anthropic"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain-anthropic"
|
||||
"%pip install -U langgraph langgraph-prebuilt langchain-anthropic"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain-anthropic"
|
||||
"%pip install -U langgraph langgraph-prebuilt langchain-anthropic"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain-anthropic"
|
||||
"%pip install -U langgraph langgraph-prebuilt langchain-anthropic"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install --quiet -U langgraph langchain_anthropic"
|
||||
"%pip install --quiet -U langgraph langgraph-prebuilt langchain_anthropic"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install --quiet -U langgraph langchain-openai"
|
||||
"%pip install --quiet -U langgraph langgraph-prebuilt langchain-openai"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U pymongo langgraph langgraph-checkpoint-mongodb"
|
||||
"%pip install -U pymongo langgraph langgraph-prebuilt langgraph-checkpoint-mongodb"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -44,8 +44,7 @@
|
||||
"...\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"!!! info \"Setup\"",
|
||||
"\n",
|
||||
"!!! info \"Setup\"\n",
|
||||
" You need to run `.setup()` once on your checkpointer to initialize the database before you can use it."
|
||||
]
|
||||
},
|
||||
@@ -70,7 +69,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U psycopg psycopg-pool langgraph langgraph-checkpoint-postgres"
|
||||
"%pip install -U psycopg psycopg-pool langgraph langgraph-prebuilt langgraph-checkpoint-postgres"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U redis langgraph langchain_openai"
|
||||
"%pip install -U redis langgraph langgraph-prebuilt langchain_openai"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain-openai"
|
||||
"%pip install -U langgraph langgraph-prebuilt langchain-openai"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install --quiet -U langgraph langchain_openai"
|
||||
"%pip install --quiet -U langgraph langgraph-prebuilt langchain_openai"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain-openai langchain-community"
|
||||
"%pip install -U langgraph langgraph-prebuilt langchain-openai langchain-community"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain-openai"
|
||||
"%pip install -U langgraph langgraph-prebuilt langchain-openai"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain-openai"
|
||||
"%pip install -U langgraph langgraph-prebuilt langchain-openai"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain_community langchain_anthropic langchain_experimental"
|
||||
"%pip install -U langgraph langgraph-prebuilt langchain_community langchain_anthropic langchain_experimental"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph langchain_community langchain_anthropic langchain_experimental"
|
||||
"%pip install -U langgraph langgraph-prebuilt langchain_community langchain_anthropic langchain_experimental"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langchain_community langchain_anthropic langchain_experimental matplotlib langgraph"
|
||||
"%pip install -U langchain_community langchain_anthropic langchain_experimental matplotlib langgraph langgraph-prebuilt"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install --quiet -U langgraph langchain-community langchain-openai tavily-python"
|
||||
"%pip install --quiet -U langgraph langgraph-prebuilt langchain-community langchain-openai tavily-python"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -184,7 +184,7 @@ As noted in the [Anthropic blog](https://www.anthropic.com/research/building-eff
|
||||
|
||||
See our lesson on Prompt Chaining [here](https://github.com/langchain-ai/langchain-academy/blob/main/module-1/chain.ipynb).
|
||||
|
||||
=== "Functional API (beta)"
|
||||
=== "Functional API"
|
||||
|
||||
```python
|
||||
from langgraph.func import entrypoint, task
|
||||
@@ -335,7 +335,7 @@ With parallelization, LLMs work simultaneously on a task:
|
||||
|
||||
See our lesson on parallelization [here](https://github.com/langchain-ai/langchain-academy/blob/main/module-1/simple-graph.ipynb).
|
||||
|
||||
=== "Functional API (beta)"
|
||||
=== "Functional API"
|
||||
|
||||
```python
|
||||
@task
|
||||
@@ -524,7 +524,7 @@ Routing classifies an input and directs it to a followup task. As noted in the [
|
||||
|
||||
[Here](https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_adaptive_rag_local/) is RAG workflow that routes questions. See our video [here](https://www.youtube.com/watch?v=bq1Plo2RhYI).
|
||||
|
||||
=== "Functional API (beta)"
|
||||
=== "Functional API"
|
||||
|
||||
```python
|
||||
from typing_extensions import Literal
|
||||
@@ -761,7 +761,7 @@ With orchestrator-worker, an orchestrator breaks down a task and delegates each
|
||||
[Here](https://github.com/langchain-ai/report-mAIstro) is a project that uses orchestrator-worker for report planning and writing. See our video [here](https://www.youtube.com/watch?v=wSxZ7yFbbas).
|
||||
|
||||
|
||||
=== "Functional API (beta)"
|
||||
=== "Functional API"
|
||||
|
||||
```python
|
||||
from typing import List
|
||||
@@ -952,7 +952,7 @@ In the evaluator-optimizer workflow, one LLM call generates a response while ano
|
||||
|
||||
[Here](https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_adaptive_rag_local/) is a RAG workflow that grades answers for hallucinations or errors. See our video [here](https://www.youtube.com/watch?v=bq1Plo2RhYI).
|
||||
|
||||
=== "Functional API (beta)"
|
||||
=== "Functional API"
|
||||
|
||||
```python
|
||||
# Schema for structured output to use in evaluation
|
||||
@@ -1161,7 +1161,7 @@ llm_with_tools = llm.bind_tools(tools)
|
||||
|
||||
[Here](https://github.com/langchain-ai/memory-agent) is a project that uses a tool calling agent to create / store long-term memories.
|
||||
|
||||
=== "Functional API (beta)"
|
||||
=== "Functional API"
|
||||
|
||||
```python
|
||||
from langgraph.graph import add_messages
|
||||
@@ -1270,4 +1270,4 @@ LangGraph provides several ways to stream workflow / agent outputs or intermedia
|
||||
|
||||
### Deployment
|
||||
|
||||
LangGraph provides an easy on-ramp for deployment, observability, and evaluation. See [module 6](https://github.com/langchain-ai/langchain-academy/tree/main/module-6) of LangChain Academy.
|
||||
LangGraph provides an easy on-ramp for deployment, observability, and evaluation. See [module 6](https://github.com/langchain-ai/langchain-academy/tree/main/module-6) of LangChain Academy.
|
||||
|
||||
Generated
+54
-38
@@ -169,15 +169,15 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "anthropic"
|
||||
version = "0.45.2"
|
||||
version = "0.47.2"
|
||||
description = "The official Python library for the anthropic API"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["test"]
|
||||
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
|
||||
files = [
|
||||
{file = "anthropic-0.45.2-py3-none-any.whl", hash = "sha256:ecd746f7274451dfcb7e1180571ead624c7e1195d1d46cb7c70143d2aedb4d35"},
|
||||
{file = "anthropic-0.45.2.tar.gz", hash = "sha256:32a18b9ecd12c91b2be4cae6ca2ab46a06937b5aa01b21308d97a6d29794fb5e"},
|
||||
{file = "anthropic-0.47.2-py3-none-any.whl", hash = "sha256:61b712a56308fce69f04d92ba0230ab2bc187b5bce17811d400843a8976bb67f"},
|
||||
{file = "anthropic-0.47.2.tar.gz", hash = "sha256:452f4ca0c56ffab8b6ce9928bf8470650f88106a7001b250895eb65c54cfa44c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1299,7 +1299,7 @@ version = "0.7.1"
|
||||
description = "XML bomb protection for Python stdlib modules"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
groups = ["docs", "test"]
|
||||
groups = ["docs"]
|
||||
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
|
||||
files = [
|
||||
{file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
|
||||
@@ -3288,21 +3288,20 @@ together = ["langchain-together"]
|
||||
|
||||
[[package]]
|
||||
name = "langchain-anthropic"
|
||||
version = "0.2.4"
|
||||
version = "0.3.8"
|
||||
description = "An integration package connecting AnthropicMessages and LangChain"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["test"]
|
||||
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
|
||||
files = [
|
||||
{file = "langchain_anthropic-0.2.4-py3-none-any.whl", hash = "sha256:bcb6c2d0df4a67aff52816621079d6e743b260911caccf313a72b33b7edece6f"},
|
||||
{file = "langchain_anthropic-0.2.4.tar.gz", hash = "sha256:0382d4c7b5236839b703f7b72b3e06de4bb5be99104b193f719adbe34c49562b"},
|
||||
{file = "langchain_anthropic-0.3.8-py3-none-any.whl", hash = "sha256:05a70f51500d3c4e0f3e463730e193a25b6244e06b3bda3d7b2ec21d83d081ae"},
|
||||
{file = "langchain_anthropic-0.3.8.tar.gz", hash = "sha256:1932977b8105744739ffdcb39861b041b73ae93846d0896a775fcea9a29e4b2b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anthropic = ">=0.30.0,<1"
|
||||
defusedxml = ">=0.7.1,<0.8.0"
|
||||
langchain-core = ">=0.3.15,<0.4.0"
|
||||
anthropic = ">=0.47.0,<1"
|
||||
langchain-core = ">=0.3.39,<1.0.0"
|
||||
pydantic = ">=2.7.4,<3.0.0"
|
||||
|
||||
[[package]]
|
||||
@@ -3357,15 +3356,15 @@ tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<10"
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.34"
|
||||
version = "0.3.40"
|
||||
description = "Building applications with LLMs through composability"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["docs", "test"]
|
||||
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
|
||||
files = [
|
||||
{file = "langchain_core-0.3.34-py3-none-any.whl", hash = "sha256:a057ebeddd2158d3be14bde341b25640ddf958b6989bd6e47160396f5a8202ae"},
|
||||
{file = "langchain_core-0.3.34.tar.gz", hash = "sha256:26504cf1e8e6c310adad907b890d4e3c147581cfa7434114f6dc1134fe4bc6d3"},
|
||||
{file = "langchain_core-0.3.40-py3-none-any.whl", hash = "sha256:9f31358741f10a13db8531e8288b8a5ae91904018c5c2e6f739d6645a98fca03"},
|
||||
{file = "langchain_core-0.3.40.tar.gz", hash = "sha256:893a238b38491967c804662c1ec7c3e6ebaf223d1125331249c3cf3862ff2746"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3474,19 +3473,19 @@ ollama = ">=0.4.4,<1"
|
||||
|
||||
[[package]]
|
||||
name = "langchain-openai"
|
||||
version = "0.3.4"
|
||||
version = "0.3.7"
|
||||
description = "An integration package connecting OpenAI and LangChain"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["test"]
|
||||
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
|
||||
files = [
|
||||
{file = "langchain_openai-0.3.4-py3-none-any.whl", hash = "sha256:58d0c014620eb92f4f46ff9daf584c2a7794896b1379eb85ad7be8d9f3493b61"},
|
||||
{file = "langchain_openai-0.3.4.tar.gz", hash = "sha256:c6645745a1d1bf19f21ea6fa473a746bd464053ff57ce563215e6165a0c4b9f1"},
|
||||
{file = "langchain_openai-0.3.7-py3-none-any.whl", hash = "sha256:0aefc7bdf8e7398d41e09c4313cace816df6438f2aa93d34f79523487310f0da"},
|
||||
{file = "langchain_openai-0.3.7.tar.gz", hash = "sha256:b8b51a3aaa1cc3bda060651ea41145f7728219e8a7150b5404fb1e8446de9cef"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
langchain-core = ">=0.3.34,<1.0.0"
|
||||
langchain-core = ">=0.3.39,<1.0.0"
|
||||
openai = ">=1.58.1,<2.0.0"
|
||||
tiktoken = ">=0.7,<1"
|
||||
|
||||
@@ -3508,17 +3507,17 @@ langchain-core = ">=0.3.34,<1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "0.2.71"
|
||||
version = "0.3.0"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
optional = false
|
||||
python-versions = ">=3.9.0,<4.0"
|
||||
groups = ["docs", "test"]
|
||||
groups = ["docs"]
|
||||
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
|
||||
files = []
|
||||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
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,!=0.3.15,!=0.3.16,!=0.3.17,!=0.3.18,!=0.3.19,!=0.3.20,!=0.3.21,!=0.3.22"
|
||||
langchain-core = ">=0.1,<0.4"
|
||||
langgraph-checkpoint = "^2.0.10"
|
||||
langgraph-sdk = "^0.1.42"
|
||||
|
||||
@@ -3528,7 +3527,7 @@ url = "../libs/langgraph"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.13"
|
||||
version = "2.0.16"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -3547,26 +3546,25 @@ url = "../libs/checkpoint"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-mongodb"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
description = "Library with a MongoDB implementation of LangGraph checkpoint saver."
|
||||
optional = false
|
||||
python-versions = "<4.0.0,>=3.9.0"
|
||||
python-versions = ">=3.9"
|
||||
groups = ["test"]
|
||||
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
|
||||
files = [
|
||||
{file = "langgraph_checkpoint_mongodb-0.1.0-py3-none-any.whl", hash = "sha256:52f20956b36e0275ff805a1eea1db4c1a7e5e0ffe0a1ade65969004fa1654703"},
|
||||
{file = "langgraph_checkpoint_mongodb-0.1.0.tar.gz", hash = "sha256:3165c134ad5c82a3fe02fef04c81dcd48a3f5d031e07a9d1cb84457241f76793"},
|
||||
{file = "langgraph_checkpoint_mongodb-0.1.1-py3-none-any.whl", hash = "sha256:1ff2c3cb2a9139c38ea9cf398659b8b32d6bbfcc4999713b62014431477c5ac5"},
|
||||
{file = "langgraph_checkpoint_mongodb-0.1.1.tar.gz", hash = "sha256:350d347b0458fb7977231ac1295095bef512458ee0debe09fd394d913b8d89d3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
langgraph = ">=0.2.38,<0.3.0"
|
||||
langgraph-checkpoint = ">=2.0.0,<3.0.0"
|
||||
langgraph-checkpoint = ">=2.0.0"
|
||||
motor = ">3.5.0"
|
||||
pymongo = ">=4.9.0,<4.10.0"
|
||||
pymongo = ">=4.9,<4.12"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "2.0.14"
|
||||
version = "2.0.15"
|
||||
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -3576,7 +3574,7 @@ files = []
|
||||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
langgraph-checkpoint = "^2.0.10"
|
||||
langgraph-checkpoint = "^2.0.15"
|
||||
orjson = ">=3.10.1"
|
||||
psycopg = "^3.2.0"
|
||||
psycopg-pool = "^3.2.0"
|
||||
@@ -3587,7 +3585,7 @@ url = "../libs/checkpoint-postgres"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "2.0.4"
|
||||
version = "2.0.5"
|
||||
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
|
||||
optional = false
|
||||
python-versions = "^3.9.0"
|
||||
@@ -3597,20 +3595,40 @@ files = []
|
||||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
aiosqlite = "^0.20.0"
|
||||
langgraph-checkpoint = "^2.0.10"
|
||||
aiosqlite = ">=0.20,<0.22"
|
||||
langgraph-checkpoint = "^2.0.15"
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
url = "../libs/checkpoint-sqlite"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "1.0.0"
|
||||
description = "Library with high-level APIs for creating and executing LangGraph agents and tools."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
groups = ["docs"]
|
||||
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
|
||||
files = []
|
||||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
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,!=0.3.15,!=0.3.16,!=0.3.17,!=0.3.18,!=0.3.19,!=0.3.20,!=0.3.21,!=0.3.22"
|
||||
langgraph = ">=0.3,<0.4"
|
||||
langgraph-checkpoint = "^2.0.10"
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
url = "../libs/prebuilt"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.1.51"
|
||||
version = "0.1.53"
|
||||
description = "SDK for interacting with LangGraph API"
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
groups = ["docs", "test"]
|
||||
groups = ["docs"]
|
||||
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
|
||||
files = []
|
||||
develop = true
|
||||
@@ -5939,7 +5957,6 @@ python-versions = ">=3.8"
|
||||
groups = ["test"]
|
||||
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
|
||||
files = [
|
||||
{file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"},
|
||||
{file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"},
|
||||
]
|
||||
|
||||
@@ -5952,7 +5969,6 @@ python-versions = ">=3.8"
|
||||
groups = ["test"]
|
||||
markers = "python_version <= \"3.11\" or python_version >= \"3.12\""
|
||||
files = [
|
||||
{file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"},
|
||||
{file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"},
|
||||
]
|
||||
|
||||
@@ -8634,4 +8650,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "06debb82135affdb2baf1fdcc028c062c236121508d787588cd0de1db2da11e4"
|
||||
content-hash = "ac9af57c6abaddd1f181551a7bb8194ef3e4491391a0f2dc71417d68e85cb5b3"
|
||||
|
||||
+3
-2
@@ -13,6 +13,7 @@ hub = "^3.0.1"
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
langgraph = { path = "../libs/langgraph/", develop = true }
|
||||
langgraph-prebuilt = {path = "../libs/prebuilt", develop = true}
|
||||
langgraph-checkpoint = { path = "../libs/checkpoint/", develop = true }
|
||||
langgraph-checkpoint-sqlite = { path = "../libs/checkpoint-sqlite", develop = true }
|
||||
langgraph-checkpoint-postgres = { path = "../libs/checkpoint-postgres", develop = true }
|
||||
@@ -40,8 +41,8 @@ langchain-cohere = "^0.4.2"
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
langchain = "^0.3.8"
|
||||
langchain-openai = "^0.3.0"
|
||||
langchain-anthropic = "^0.2.1"
|
||||
langchain-openai = "^0.3.7"
|
||||
langchain-anthropic = "^0.3.8"
|
||||
langchain-nomic = "^0.1.3"
|
||||
langchain-fireworks = "^0.2.0"
|
||||
langchain-community = "^0.3.0"
|
||||
|
||||
Generated
+8
-8
@@ -1,23 +1,23 @@
|
||||
# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiosqlite"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
description = "asyncio bridge to the standard sqlite3 module"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"},
|
||||
{file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"},
|
||||
{file = "aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0"},
|
||||
{file = "aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing_extensions = ">=4.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["attribution (==1.7.0)", "black (==24.2.0)", "coverage[toml] (==7.4.1)", "flake8 (==7.0.0)", "flake8-bugbear (==24.2.6)", "flit (==3.9.0)", "mypy (==1.8.0)", "ufmt (==2.3.0)", "usort (==1.0.8.post1)"]
|
||||
docs = ["sphinx (==7.2.6)", "sphinx-mdinclude (==0.5.3)"]
|
||||
dev = ["attribution (==1.7.1)", "black (==24.3.0)", "build (>=1.2)", "coverage[toml] (==7.6.10)", "flake8 (==7.0.0)", "flake8-bugbear (==24.12.12)", "flit (==3.10.1)", "mypy (==1.14.1)", "ufmt (==2.5.1)", "usort (==1.0.8.post1)"]
|
||||
docs = ["sphinx (==8.1.3)", "sphinx-mdinclude (==0.6.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
@@ -1043,4 +1043,4 @@ watchmedo = ["PyYAML (>=3.10)"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.9.0"
|
||||
content-hash = "e6d3ca9bce723c05f4c5ae9dc4bee872f7581b7763680b34112f1d280f5a9b0a"
|
||||
content-hash = "21896b8d3d283d95bc3988aa93f06faf5c47dadc2a8822e5a35672b9cb054693"
|
||||
|
||||
@@ -11,7 +11,7 @@ packages = [{ include = "langgraph" }]
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0"
|
||||
langgraph-checkpoint = "^2.0.15"
|
||||
aiosqlite = "^0.20.0"
|
||||
aiosqlite = ">=0.20,<0.22"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.6.2"
|
||||
|
||||
@@ -70,7 +70,7 @@ pip install -U langgraph
|
||||
Let's build a tool-calling [ReAct-style](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#react-implementation) agent that uses a search tool!
|
||||
|
||||
```shell
|
||||
pip install langchain-anthropic
|
||||
pip install langgraph-prebuilt langchain-anthropic
|
||||
```
|
||||
|
||||
```shell
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
from typing import Any, Callable, Sequence, Union
|
||||
|
||||
from langchain_core.load.serializable import Serializable
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from langchain_core.tools import BaseTool
|
||||
from langchain_core.tools import tool as create_tool
|
||||
|
||||
from langgraph._api.deprecation import deprecated
|
||||
from langgraph.utils.runnable import RunnableCallable
|
||||
|
||||
INVALID_TOOL_MSG_TEMPLATE = (
|
||||
"{requested_tool_name} is not a valid tool, "
|
||||
"try one of [{available_tool_names_str}]."
|
||||
)
|
||||
|
||||
|
||||
@deprecated("0.2.0", "langgraph.prebuilt.ToolNode", removal="0.3.0")
|
||||
class ToolInvocationInterface:
|
||||
"""Interface for invoking a tool.
|
||||
|
||||
Attributes:
|
||||
tool (str): The name of the tool to invoke.
|
||||
tool_input (Union[str, dict]): The input to pass to the tool.
|
||||
|
||||
"""
|
||||
|
||||
tool: str
|
||||
tool_input: Union[str, dict]
|
||||
|
||||
|
||||
@deprecated("0.2.0", "langgraph.prebuilt.ToolNode", removal="0.3.0")
|
||||
class ToolInvocation(Serializable):
|
||||
"""Information about how to invoke a tool.
|
||||
|
||||
Attributes:
|
||||
tool (str): The name of the Tool to execute.
|
||||
tool_input (Union[str, dict]): The input to pass in to the Tool.
|
||||
|
||||
Examples:
|
||||
Basic usage:
|
||||
```pycon
|
||||
>>> invocation = ToolInvocation(
|
||||
... tool="search",
|
||||
... tool_input="What is the capital of France?"
|
||||
... )
|
||||
```
|
||||
"""
|
||||
|
||||
tool: str
|
||||
tool_input: Union[str, dict]
|
||||
|
||||
|
||||
@deprecated("0.2.0", "langgraph.prebuilt.ToolNode", removal="0.3.0")
|
||||
class ToolExecutor(RunnableCallable):
|
||||
"""Executes a tool invocation.
|
||||
|
||||
Args:
|
||||
tools (Sequence[BaseTool]): A sequence of tools that can be invoked.
|
||||
invalid_tool_msg_template (str, optional): The template for the error message
|
||||
when an invalid tool is requested. Defaults to INVALID_TOOL_MSG_TEMPLATE.
|
||||
|
||||
Examples:
|
||||
Basic usage:
|
||||
|
||||
```pycon
|
||||
>>> from langchain_core.tools import tool
|
||||
>>> from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation
|
||||
...
|
||||
...
|
||||
>>> @tool
|
||||
... def search(query: str) -> str:
|
||||
... \"\"\"Search engine.\"\"\"
|
||||
... return f"Searching for: {query}"
|
||||
...
|
||||
...
|
||||
>>> tools = [search]
|
||||
>>> executor = ToolExecutor(tools)
|
||||
...
|
||||
>>> invocation = ToolInvocation(tool="search", tool_input="What is the capital of France?")
|
||||
>>> result = executor.invoke(invocation)
|
||||
>>> print(result)
|
||||
"Searching for: What is the capital of France?"
|
||||
```
|
||||
Handling invalid tool:
|
||||
|
||||
```pycon
|
||||
>>> invocation = ToolInvocation(
|
||||
... tool="nonexistent", tool_input="What is the capital of France?"
|
||||
... )
|
||||
>>> result = executor.invoke(invocation)
|
||||
>>> print(result)
|
||||
"nonexistent is not a valid tool, try one of [search]."
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tools: Sequence[Union[BaseTool, Callable]],
|
||||
*,
|
||||
invalid_tool_msg_template: str = INVALID_TOOL_MSG_TEMPLATE,
|
||||
) -> None:
|
||||
super().__init__(self._execute, afunc=self._aexecute, trace=False)
|
||||
tools_ = [
|
||||
tool if isinstance(tool, BaseTool) else create_tool(tool) for tool in tools
|
||||
]
|
||||
self.tools = tools_
|
||||
self.tool_map = {t.name: t for t in tools_}
|
||||
self.invalid_tool_msg_template = invalid_tool_msg_template
|
||||
|
||||
def _execute(
|
||||
self, tool_invocation: ToolInvocationInterface, config: RunnableConfig
|
||||
) -> Any:
|
||||
if tool_invocation.tool not in self.tool_map:
|
||||
return self.invalid_tool_msg_template.format(
|
||||
requested_tool_name=tool_invocation.tool,
|
||||
available_tool_names_str=", ".join([t.name for t in self.tools]),
|
||||
)
|
||||
else:
|
||||
tool = self.tool_map[tool_invocation.tool]
|
||||
output = tool.invoke(tool_invocation.tool_input, config)
|
||||
return output
|
||||
|
||||
async def _aexecute(
|
||||
self, tool_invocation: ToolInvocationInterface, config: RunnableConfig
|
||||
) -> Any:
|
||||
if tool_invocation.tool not in self.tool_map:
|
||||
return self.invalid_tool_msg_template.format(
|
||||
requested_tool_name=tool_invocation.tool,
|
||||
available_tool_names_str=", ".join([t.name for t in self.tools]),
|
||||
)
|
||||
else:
|
||||
tool = self.tool_map[tool_invocation.tool]
|
||||
output = await tool.ainvoke(tool_invocation.tool_input, config)
|
||||
return output
|
||||
@@ -971,6 +971,12 @@ class Pregel(PregelProtocol):
|
||||
raise ValueError(f"Subgraph {recast} not found")
|
||||
|
||||
config = merge_configs(self.config, config) if self.config else config
|
||||
if self.checkpointer is True:
|
||||
ns = cast(str, config[CONF][CONFIG_KEY_CHECKPOINT_NS])
|
||||
config = merge_configs(
|
||||
config, {CONF: {CONFIG_KEY_CHECKPOINT_NS: recast_checkpoint_ns(ns)}}
|
||||
)
|
||||
|
||||
saved = checkpointer.get_tuple(config)
|
||||
return self._prepare_state_snapshot(
|
||||
config,
|
||||
@@ -1004,6 +1010,12 @@ class Pregel(PregelProtocol):
|
||||
raise ValueError(f"Subgraph {recast} not found")
|
||||
|
||||
config = merge_configs(self.config, config) if self.config else config
|
||||
if self.checkpointer is True:
|
||||
ns = cast(str, config[CONF][CONFIG_KEY_CHECKPOINT_NS])
|
||||
config = merge_configs(
|
||||
config, {CONF: {CONFIG_KEY_CHECKPOINT_NS: recast_checkpoint_ns(ns)}}
|
||||
)
|
||||
|
||||
saved = await checkpointer.aget_tuple(config)
|
||||
return await self._aprepare_state_snapshot(
|
||||
config,
|
||||
@@ -1911,9 +1923,7 @@ class Pregel(PregelProtocol):
|
||||
# set up subgraph checkpointing
|
||||
if self.checkpointer is True:
|
||||
ns = cast(str, config[CONF][CONFIG_KEY_CHECKPOINT_NS])
|
||||
config[CONF][CONFIG_KEY_CHECKPOINT_NS] = NS_SEP.join(
|
||||
part.split(NS_END)[0] for part in ns.split(NS_SEP)
|
||||
)
|
||||
config[CONF][CONFIG_KEY_CHECKPOINT_NS] = recast_checkpoint_ns(ns)
|
||||
# set up messages stream mode
|
||||
if "messages" in stream_modes:
|
||||
run_manager.inheritable_handlers.append(
|
||||
@@ -2201,9 +2211,7 @@ class Pregel(PregelProtocol):
|
||||
# set up subgraph checkpointing
|
||||
if self.checkpointer is True:
|
||||
ns = cast(str, config[CONF][CONFIG_KEY_CHECKPOINT_NS])
|
||||
config[CONF][CONFIG_KEY_CHECKPOINT_NS] = NS_SEP.join(
|
||||
part.split(NS_END)[0] for part in ns.split(NS_SEP)
|
||||
)
|
||||
config[CONF][CONFIG_KEY_CHECKPOINT_NS] = recast_checkpoint_ns(ns)
|
||||
# set up messages stream mode
|
||||
if "messages" in stream_modes:
|
||||
run_manager.inheritable_handlers.append(
|
||||
|
||||
@@ -127,6 +127,16 @@ class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
|
||||
tuple(cast(str, metadata["langgraph_checkpoint_ns"]).split(NS_SEP)),
|
||||
metadata,
|
||||
)
|
||||
if isinstance(inputs, dict):
|
||||
for key, value in inputs.items():
|
||||
if isinstance(value, BaseMessage):
|
||||
if value.id is not None:
|
||||
self.seen.add(value.id)
|
||||
elif isinstance(value, Sequence) and not isinstance(value, str):
|
||||
for item in value:
|
||||
if isinstance(item, BaseMessage):
|
||||
if item.id is not None:
|
||||
self.seen.add(item.id)
|
||||
|
||||
def on_chain_end(
|
||||
self,
|
||||
|
||||
@@ -32,11 +32,17 @@ def validate_graph(
|
||||
|
||||
for chan in subscribed_channels:
|
||||
if chan not in channels:
|
||||
raise ValueError(f"Subscribed channel '{chan}' not in 'channels'")
|
||||
raise ValueError(
|
||||
f"Subscribed channel '{chan}' not "
|
||||
f"in known channels: '{repr(sorted(channels))[:100]}'"
|
||||
)
|
||||
|
||||
if isinstance(input_channels, str):
|
||||
if input_channels not in channels:
|
||||
raise ValueError(f"Input channel '{input_channels}' not in 'channels'")
|
||||
raise ValueError(
|
||||
f"Input channel '{input_channels}' not "
|
||||
f"in known channels: '{repr(sorted(channels))[:100]}'"
|
||||
)
|
||||
if input_channels not in subscribed_channels:
|
||||
raise ValueError(
|
||||
f"Input channel {input_channels} is not subscribed to by any node"
|
||||
@@ -44,10 +50,13 @@ def validate_graph(
|
||||
else:
|
||||
for chan in input_channels:
|
||||
if chan not in channels:
|
||||
raise ValueError(f"Input channel '{chan}' not in 'channels'")
|
||||
raise ValueError(
|
||||
f"Input channel '{chan}' not in '{repr(sorted(channels))[:100]}'"
|
||||
)
|
||||
if all(chan not in subscribed_channels for chan in input_channels):
|
||||
raise ValueError(
|
||||
f"None of the input channels {input_channels} are subscribed to by any node"
|
||||
f"None of the input channels {input_channels} "
|
||||
f"are subscribed to by any node"
|
||||
)
|
||||
|
||||
all_output_channels = set[str]()
|
||||
@@ -62,7 +71,10 @@ def validate_graph(
|
||||
|
||||
for chan in all_output_channels:
|
||||
if chan not in channels:
|
||||
raise ValueError(f"Output channel '{chan}' not in 'channels'")
|
||||
raise ValueError(
|
||||
f"Output channel '{chan}' not "
|
||||
f"in known channels: '{repr(sorted(channels))[:100]}'"
|
||||
)
|
||||
|
||||
if interrupt_after_nodes != "*":
|
||||
for n in interrupt_after_nodes:
|
||||
|
||||
Generated
+28
-9
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiosqlite"
|
||||
@@ -1348,7 +1348,7 @@ typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.10"
|
||||
version = "2.0.16"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -1366,7 +1366,7 @@ url = "../checkpoint"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "2.0.12"
|
||||
version = "2.0.15"
|
||||
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -1375,7 +1375,7 @@ files = []
|
||||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
langgraph-checkpoint = "^2.0.10"
|
||||
langgraph-checkpoint = "^2.0.15"
|
||||
orjson = ">=3.10.1"
|
||||
psycopg = "^3.2.0"
|
||||
psycopg-pool = "^3.2.0"
|
||||
@@ -1386,7 +1386,7 @@ url = "../checkpoint-postgres"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "2.0.3"
|
||||
version = "2.0.5"
|
||||
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
|
||||
optional = false
|
||||
python-versions = "^3.9.0"
|
||||
@@ -1395,16 +1395,35 @@ files = []
|
||||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
aiosqlite = "^0.20.0"
|
||||
langgraph-checkpoint = "^2.0.10"
|
||||
aiosqlite = ">=0.20,<0.22"
|
||||
langgraph-checkpoint = "^2.0.15"
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
url = "../checkpoint-sqlite"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "0.1.0"
|
||||
description = "Library with high-level APIs for creating and executing LangGraph agents and tools."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
groups = ["dev"]
|
||||
files = []
|
||||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
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,!=0.3.15,!=0.3.16,!=0.3.17,!=0.3.18,!=0.3.19,!=0.3.20,!=0.3.21,!=0.3.22"
|
||||
langgraph = ">=0.3,<0.4"
|
||||
langgraph-checkpoint = "^2.0.10"
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
url = "../prebuilt"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.1.51"
|
||||
version = "0.1.53"
|
||||
description = "SDK for interacting with LangGraph API"
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -3491,4 +3510,4 @@ type = ["pytest-mypy"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.9.0,<4.0"
|
||||
content-hash = "caf943b02b6913c05d15c37fda6d216669f789e2a059b7e8e2490b2bdcd23e0e"
|
||||
content-hash = "967adb1fe0f48b76a53b79c8604768b2ce74042f88d1791f57ea1ed0a0ede0ad"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph"
|
||||
version = "0.2.74"
|
||||
version = "0.3.0"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -9,7 +9,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,!=0.3.15,!=0.3.16,!=0.3.17,!=0.3.18,!=0.3.19,!=0.3.20,!=0.3.21,!=0.3.22"
|
||||
langchain-core = ">=0.1,<0.4"
|
||||
langgraph-checkpoint = "^2.0.10"
|
||||
langgraph-sdk = "^0.1.42"
|
||||
|
||||
@@ -26,6 +26,7 @@ ruff = "^0.6.2"
|
||||
jupyter = "^1.0.0"
|
||||
pytest-xdist = {extras = ["psutil"], version = "^3.6.1"}
|
||||
pytest-repeat = "^0.9.3"
|
||||
langgraph-prebuilt = {path = "../prebuilt", develop = true}
|
||||
langgraph-checkpoint = {path = "../checkpoint", develop = true}
|
||||
langgraph-checkpoint-sqlite = {path = "../checkpoint-sqlite", develop = true}
|
||||
langgraph-checkpoint-postgres = {path = "../checkpoint-postgres", develop = true}
|
||||
|
||||
@@ -3371,6 +3371,59 @@ def test_subgraph_checkpoint_true(
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_subgraph_checkpoint_true_interrupt(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
) -> None:
|
||||
checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name)
|
||||
|
||||
# Define subgraph
|
||||
class SubgraphState(TypedDict):
|
||||
# note that none of these keys are shared with the parent graph state
|
||||
bar: str
|
||||
baz: str
|
||||
|
||||
def subgraph_node_1(state: SubgraphState):
|
||||
baz_value = interrupt("Provide baz value")
|
||||
return {"baz": baz_value}
|
||||
|
||||
def subgraph_node_2(state: SubgraphState):
|
||||
return {"bar": state["bar"] + state["baz"]}
|
||||
|
||||
subgraph_builder = StateGraph(SubgraphState)
|
||||
subgraph_builder.add_node(subgraph_node_1)
|
||||
subgraph_builder.add_node(subgraph_node_2)
|
||||
subgraph_builder.add_edge(START, "subgraph_node_1")
|
||||
subgraph_builder.add_edge("subgraph_node_1", "subgraph_node_2")
|
||||
subgraph = subgraph_builder.compile(checkpointer=True)
|
||||
|
||||
class ParentState(TypedDict):
|
||||
foo: str
|
||||
|
||||
def node_1(state: ParentState):
|
||||
return {"foo": "hi! " + state["foo"]}
|
||||
|
||||
def node_2(state: ParentState):
|
||||
response = subgraph.invoke({"bar": state["foo"]})
|
||||
return {"foo": response["bar"]}
|
||||
|
||||
builder = StateGraph(ParentState)
|
||||
builder.add_node("node_1", node_1)
|
||||
builder.add_node("node_2", node_2)
|
||||
builder.add_edge(START, "node_1")
|
||||
builder.add_edge("node_1", "node_2")
|
||||
|
||||
checkpointer = MemorySaver()
|
||||
graph = builder.compile(checkpointer=checkpointer)
|
||||
config = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
assert graph.invoke({"foo": "foo"}, config) == {"foo": "hi! foo"}
|
||||
assert graph.get_state(config, subgraphs=True).tasks[0].state.values == {
|
||||
"bar": "hi! foo"
|
||||
}
|
||||
assert graph.invoke(Command(resume="baz"), config) == {"foo": "hi! foobaz"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_stream_subgraphs_during_execution(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
@@ -6588,3 +6641,101 @@ def test_get_stream_writer() -> None:
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def test_stream_messages_dedupe_inputs() -> None:
|
||||
from langchain_core.messages import AIMessage
|
||||
|
||||
def call_model(state):
|
||||
return {"messages": AIMessage("hi", id="1")}
|
||||
|
||||
def route(state):
|
||||
return Command(goto="node_2", graph=Command.PARENT)
|
||||
|
||||
subgraph = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node(call_model)
|
||||
.add_node(route)
|
||||
.add_edge(START, "call_model")
|
||||
.add_edge("call_model", "route")
|
||||
.compile()
|
||||
)
|
||||
|
||||
graph = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("node_1", subgraph)
|
||||
.add_node("node_2", lambda state: state)
|
||||
.add_edge(START, "node_1")
|
||||
.compile()
|
||||
)
|
||||
|
||||
chunks = [
|
||||
chunk
|
||||
for ns, chunk in graph.stream(
|
||||
{"messages": "hi"}, stream_mode="messages", subgraphs=True
|
||||
)
|
||||
]
|
||||
|
||||
assert len(chunks) == 1
|
||||
assert chunks[0][0] == AIMessage("hi", id="1")
|
||||
assert chunks[0][1]["langgraph_node"] == "call_model"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
def test_stream_messages_dedupe_state(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
) -> None:
|
||||
from langchain_core.messages import AIMessage
|
||||
|
||||
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
|
||||
to_emit = [AIMessage("bye", id="1"), AIMessage("bye again", id="2")]
|
||||
|
||||
def call_model(state):
|
||||
return {"messages": to_emit.pop(0)}
|
||||
|
||||
def route(state):
|
||||
return Command(goto="node_2", graph=Command.PARENT)
|
||||
|
||||
subgraph = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node(call_model)
|
||||
.add_node(route)
|
||||
.add_edge(START, "call_model")
|
||||
.add_edge("call_model", "route")
|
||||
.compile()
|
||||
)
|
||||
|
||||
graph = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("node_1", subgraph)
|
||||
.add_node("node_2", lambda state: state)
|
||||
.add_edge(START, "node_1")
|
||||
.compile(checkpointer=checkpointer)
|
||||
)
|
||||
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
chunks = [
|
||||
chunk
|
||||
for ns, chunk in graph.stream(
|
||||
{"messages": "hi"}, thread1, stream_mode="messages", subgraphs=True
|
||||
)
|
||||
]
|
||||
|
||||
assert len(chunks) == 1
|
||||
assert chunks[0][0] == AIMessage("bye", id="1")
|
||||
assert chunks[0][1]["langgraph_node"] == "call_model"
|
||||
|
||||
chunks = [
|
||||
chunk
|
||||
for ns, chunk in graph.stream(
|
||||
{"messages": "hi again"},
|
||||
thread1,
|
||||
stream_mode="messages",
|
||||
subgraphs=True,
|
||||
)
|
||||
]
|
||||
|
||||
assert len(chunks) == 1
|
||||
assert chunks[0][0] == AIMessage("bye again", id="2")
|
||||
assert chunks[0][1]["langgraph_node"] == "call_model"
|
||||
|
||||
@@ -7282,9 +7282,7 @@ async def test_multiple_subgraphs_mixed_state_graph(
|
||||
|
||||
@NEEDS_CONTEXTVARS
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_multiple_subgraphs_checkpointer(
|
||||
request: pytest.FixtureRequest, checkpointer_name: str
|
||||
) -> None:
|
||||
async def test_multiple_subgraphs_checkpointer(checkpointer_name: str) -> None:
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
|
||||
class SubgraphState(TypedDict):
|
||||
@@ -7513,3 +7511,99 @@ async def test_tags_stream_mode_messages() -> None:
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
async def test_stream_messages_dedupe_inputs() -> None:
|
||||
from langchain_core.messages import AIMessage
|
||||
|
||||
async def call_model(state):
|
||||
return {"messages": AIMessage("hi", id="1")}
|
||||
|
||||
async def route(state):
|
||||
return Command(goto="node_2", graph=Command.PARENT)
|
||||
|
||||
subgraph = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node(call_model)
|
||||
.add_node(route)
|
||||
.add_edge(START, "call_model")
|
||||
.add_edge("call_model", "route")
|
||||
.compile()
|
||||
)
|
||||
|
||||
graph = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("node_1", subgraph)
|
||||
.add_node("node_2", lambda state: state)
|
||||
.add_edge(START, "node_1")
|
||||
.compile()
|
||||
)
|
||||
|
||||
chunks = [
|
||||
chunk
|
||||
async for ns, chunk in graph.astream(
|
||||
{"messages": "hi"}, stream_mode="messages", subgraphs=True
|
||||
)
|
||||
]
|
||||
|
||||
assert len(chunks) == 1
|
||||
assert chunks[0][0] == AIMessage("hi", id="1")
|
||||
assert chunks[0][1]["langgraph_node"] == "call_model"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
|
||||
async def test_stream_messages_dedupe_state(checkpointer_name: str) -> None:
|
||||
async with awith_checkpointer(checkpointer_name) as checkpointer:
|
||||
from langchain_core.messages import AIMessage
|
||||
|
||||
to_emit = [AIMessage("bye", id="1"), AIMessage("bye again", id="2")]
|
||||
|
||||
async def call_model(state):
|
||||
return {"messages": to_emit.pop(0)}
|
||||
|
||||
async def route(state):
|
||||
return Command(goto="node_2", graph=Command.PARENT)
|
||||
|
||||
subgraph = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node(call_model)
|
||||
.add_node(route)
|
||||
.add_edge(START, "call_model")
|
||||
.add_edge("call_model", "route")
|
||||
.compile()
|
||||
)
|
||||
|
||||
graph = (
|
||||
StateGraph(MessagesState)
|
||||
.add_node("node_1", subgraph)
|
||||
.add_node("node_2", lambda state: state)
|
||||
.add_edge(START, "node_1")
|
||||
.compile(checkpointer=checkpointer)
|
||||
)
|
||||
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
|
||||
chunks = [
|
||||
chunk
|
||||
async for ns, chunk in graph.astream(
|
||||
{"messages": "hi"}, thread1, stream_mode="messages", subgraphs=True
|
||||
)
|
||||
]
|
||||
|
||||
assert len(chunks) == 1
|
||||
assert chunks[0][0] == AIMessage("bye", id="1")
|
||||
assert chunks[0][1]["langgraph_node"] == "call_model"
|
||||
|
||||
chunks = [
|
||||
chunk
|
||||
async for ns, chunk in graph.astream(
|
||||
{"messages": "hi again"},
|
||||
thread1,
|
||||
stream_mode="messages",
|
||||
subgraphs=True,
|
||||
)
|
||||
]
|
||||
|
||||
assert len(chunks) == 1
|
||||
assert chunks[0][0] == AIMessage("bye again", id="2")
|
||||
assert chunks[0][1]["langgraph_node"] == "call_model"
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 LangChain, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,78 @@
|
||||
.PHONY: all format lint test test_watch integration_tests spell_check spell_fix benchmark profile
|
||||
|
||||
# Default target executed when no arguments are given to make.
|
||||
all: help
|
||||
|
||||
######################
|
||||
# TESTING AND COVERAGE
|
||||
######################
|
||||
|
||||
start-postgres:
|
||||
docker compose -f tests/compose-postgres.yml up -V --force-recreate --wait --remove-orphans
|
||||
|
||||
stop-postgres:
|
||||
docker compose -f tests/compose-postgres.yml down -v
|
||||
|
||||
TEST ?= .
|
||||
|
||||
test:
|
||||
make start-postgres && poetry run pytest $(TEST); \
|
||||
EXIT_CODE=$$?; \
|
||||
make stop-postgres; \
|
||||
exit $$EXIT_CODE
|
||||
|
||||
test_watch:
|
||||
make start-postgres && poetry run ptw $(TEST); \
|
||||
EXIT_CODE=$$?; \
|
||||
make stop-postgres; \
|
||||
exit $$EXIT_CODE
|
||||
|
||||
######################
|
||||
# LINTING AND FORMATTING
|
||||
######################
|
||||
|
||||
# Define a variable for Python and notebook files.
|
||||
PYTHON_FILES=.
|
||||
MYPY_CACHE=.mypy_cache
|
||||
lint format: PYTHON_FILES=.
|
||||
lint_diff format_diff: PYTHON_FILES=$(shell git diff --name-only --relative --diff-filter=d main . | grep -E '\.py$$|\.ipynb$$')
|
||||
lint_package: PYTHON_FILES=langgraph
|
||||
lint_tests: PYTHON_FILES=tests
|
||||
lint_tests: MYPY_CACHE=.mypy_cache_test
|
||||
|
||||
lint lint_diff lint_package lint_tests:
|
||||
poetry run ruff check .
|
||||
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff
|
||||
[ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES)
|
||||
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE)
|
||||
[ "$(PYTHON_FILES)" = "" ] || poetry run mypy langgraph --cache-dir $(MYPY_CACHE)
|
||||
|
||||
format format_diff:
|
||||
poetry run ruff format $(PYTHON_FILES)
|
||||
poetry run ruff check --select I --fix $(PYTHON_FILES)
|
||||
|
||||
spell_check:
|
||||
poetry run codespell --toml pyproject.toml
|
||||
|
||||
spell_fix:
|
||||
poetry run codespell --toml pyproject.toml -w
|
||||
|
||||
|
||||
######################
|
||||
# HELP
|
||||
######################
|
||||
|
||||
help:
|
||||
@echo '===================='
|
||||
@echo '-- DOCUMENTATION --'
|
||||
|
||||
@echo '-- LINTING --'
|
||||
@echo 'format - run code formatters'
|
||||
@echo 'lint - run linters'
|
||||
@echo 'spell_check - run codespell on the project'
|
||||
@echo 'spell_fix - run codespell on the project and fix the errors'
|
||||
@echo '-- TESTS --'
|
||||
@echo 'coverage - run unit tests and generate coverage report'
|
||||
@echo 'test - run unit tests'
|
||||
@echo 'test TEST_FILE=<test_file> - run all tests in file'
|
||||
@echo 'test_watch - run unit tests in watch mode'
|
||||
@@ -0,0 +1,120 @@
|
||||
# LangGraph Prebuilt
|
||||
|
||||
This library defines high-level APIs for creating and executing LangGraph agents and tools.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install langgraph-prebuilt
|
||||
```
|
||||
|
||||
## Agents
|
||||
|
||||
`langgraph-prebuilt` provides an [implementation](https://langchain-ai.github.io/langgraph/reference/prebuilt/#langgraph.prebuilt.chat_agent_executor.create_react_agent) of a tool-calling [ReAct-style](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#react-implementation) agent - `create_react_agent`:
|
||||
|
||||
```bash
|
||||
pip install langchain-anthropic
|
||||
```
|
||||
|
||||
```python
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langgraph.prebuilt import create_react_agent
|
||||
|
||||
# Define the tools for the agent to use
|
||||
def search(query: str):
|
||||
"""Call to surf the web."""
|
||||
# This is a placeholder, but don't tell the LLM that...
|
||||
if "sf" in query.lower() or "san francisco" in query.lower():
|
||||
return "It's 60 degrees and foggy."
|
||||
return "It's 90 degrees and sunny."
|
||||
|
||||
tools = [search]
|
||||
model = ChatAnthropic(model="claude-3-7-sonnet-latest")
|
||||
|
||||
app = create_react_agent(model, tools)
|
||||
# run the agent
|
||||
app.invoke(
|
||||
{"messages": [{"role": "user", "content": "what is the weather in sf"}]},
|
||||
)
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
### ToolNode
|
||||
|
||||
`langgraph-prebuilt` provides an [implementation](https://langchain-ai.github.io/langgraph/reference/prebuilt/#langgraph.prebuilt.tool_node.ToolNode) of a node that executes tool calls - `ToolNode`:
|
||||
|
||||
```python
|
||||
from langgraph.prebuilt import ToolNode
|
||||
from langchain_core.messages import AIMessage
|
||||
|
||||
def search(query: str):
|
||||
"""Call to surf the web."""
|
||||
# This is a placeholder, but don't tell the LLM that...
|
||||
if "sf" in query.lower() or "san francisco" in query.lower():
|
||||
return "It's 60 degrees and foggy."
|
||||
return "It's 90 degrees and sunny."
|
||||
|
||||
tool_node = ToolNode([search])
|
||||
tool_calls = [{"name": "search", "args": {"query": "what is the weather in sf"}, "id": "1"}]
|
||||
ai_message = AIMessage(content="", tool_calls=tool_calls)
|
||||
# execute tool call
|
||||
tool_node.invoke({"messages": [ai_message]})
|
||||
```
|
||||
|
||||
### ValidationNode
|
||||
|
||||
`langgraph-prebuilt` provides an [implementation](https://langchain-ai.github.io/langgraph/reference/prebuilt/#langgraph.prebuilt.tool_validator.ValidationNode) of a node that validates tool calls against a pydantic schema - `ValidationNode`:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, field_validator
|
||||
from langgraph.prebuilt import ValidationNode
|
||||
from langchain_core.messages import AIMessage
|
||||
|
||||
|
||||
class SelectNumber(BaseModel):
|
||||
a: int
|
||||
|
||||
@field_validator("a")
|
||||
def a_must_be_meaningful(cls, v):
|
||||
if v != 37:
|
||||
raise ValueError("Only 37 is allowed")
|
||||
return v
|
||||
|
||||
validation_node = ValidationNode([SelectNumber])
|
||||
validation_node.invoke({
|
||||
"messages": [AIMessage("", tool_calls=[{"name": "SelectNumber", "args": {"a": 42}, "id": "1"}])]
|
||||
})
|
||||
```
|
||||
|
||||
## Agent Inbox
|
||||
|
||||
The library contains schemas for using the [Agent Inbox](https://github.com/langchain-ai/agent-inbox) with LangGraph agents. Learn more about how to use Agent Inbox [here](https://github.com/langchain-ai/agent-inbox#interrupts).
|
||||
|
||||
```python
|
||||
from langgraph.types import interrupt
|
||||
from langgraph.prebuilt.interrupt import HumanInterrupt, HumanResponse
|
||||
|
||||
def my_graph_function():
|
||||
# Extract the last tool call from the `messages` field in the state
|
||||
tool_call = state["messages"][-1].tool_calls[0]
|
||||
# Create an interrupt
|
||||
request: HumanInterrupt = {
|
||||
"action_request": {
|
||||
"action": tool_call['name'],
|
||||
"args": tool_call['args']
|
||||
},
|
||||
"config": {
|
||||
"allow_ignore": True,
|
||||
"allow_respond": True,
|
||||
"allow_edit": False,
|
||||
"allow_accept": False
|
||||
},
|
||||
"description": _generate_email_markdown(state) # Generate a detailed markdown description.
|
||||
}
|
||||
# Send the interrupt request inside a list, and extract the first response
|
||||
response = interrupt([request])[0]
|
||||
if response['type'] == "response":
|
||||
# Do something with the response
|
||||
...
|
||||
```
|
||||
-3
@@ -1,7 +1,6 @@
|
||||
"""langgraph.prebuilt exposes a higher-level API for creating and executing agents and tools."""
|
||||
|
||||
from langgraph.prebuilt.chat_agent_executor import create_react_agent
|
||||
from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation
|
||||
from langgraph.prebuilt.tool_node import (
|
||||
InjectedState,
|
||||
InjectedStore,
|
||||
@@ -12,8 +11,6 @@ from langgraph.prebuilt.tool_validator import ValidationNode
|
||||
|
||||
__all__ = [
|
||||
"create_react_agent",
|
||||
"ToolExecutor",
|
||||
"ToolInvocation",
|
||||
"ToolNode",
|
||||
"tools_condition",
|
||||
"ValidationNode",
|
||||
+6
-42
@@ -32,7 +32,6 @@ from langgraph.graph import END, StateGraph
|
||||
from langgraph.graph.graph import CompiledGraph
|
||||
from langgraph.graph.message import add_messages
|
||||
from langgraph.managed import IsLastStep, RemainingSteps
|
||||
from langgraph.prebuilt.tool_executor import ToolExecutor
|
||||
from langgraph.prebuilt.tool_node import ToolNode
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import Checkpointer, Send
|
||||
@@ -68,13 +67,6 @@ StateSchemaType = Type[StateSchema]
|
||||
|
||||
PROMPT_RUNNABLE_NAME = "Prompt"
|
||||
|
||||
MessagesModifier = Union[
|
||||
SystemMessage,
|
||||
str,
|
||||
Callable[[Sequence[BaseMessage]], LanguageModelInput],
|
||||
Runnable[Sequence[BaseMessage], LanguageModelInput],
|
||||
]
|
||||
|
||||
Prompt = Union[
|
||||
SystemMessage,
|
||||
str,
|
||||
@@ -119,43 +111,20 @@ def _get_prompt_runnable(prompt: Optional[Prompt]) -> Runnable:
|
||||
return prompt_runnable
|
||||
|
||||
|
||||
def _convert_messages_modifier_to_prompt(
|
||||
messages_modifier: MessagesModifier,
|
||||
) -> Prompt:
|
||||
prompt: Prompt
|
||||
if isinstance(messages_modifier, (str, SystemMessage)):
|
||||
return messages_modifier
|
||||
elif callable(messages_modifier):
|
||||
|
||||
def prompt(state: AgentState) -> Sequence[BaseMessage]:
|
||||
return messages_modifier(state["messages"])
|
||||
|
||||
return prompt
|
||||
elif isinstance(messages_modifier, Runnable):
|
||||
prompt = (lambda state: state["messages"]) | messages_modifier
|
||||
return prompt
|
||||
raise ValueError(
|
||||
f"Got unexpected type for `messages_modifier`: {type(messages_modifier)}"
|
||||
)
|
||||
|
||||
|
||||
def _convert_modifier_to_prompt(func: F) -> F:
|
||||
"""Decorator that converts state_modifier/messages_modifier kwargs to prompt kwarg."""
|
||||
"""Decorator that converts state_modifier kwarg to prompt kwarg."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
prompt = kwargs.get("prompt")
|
||||
state_modifier = kwargs.pop("state_modifier", None)
|
||||
messages_modifier = kwargs.pop("messages_modifier", None)
|
||||
if sum(p is not None for p in (prompt, state_modifier, messages_modifier)) > 1:
|
||||
if sum(p is not None for p in (prompt, state_modifier)) > 1:
|
||||
raise ValueError(
|
||||
"Expected only one of prompt, state_modifier, or messages_modifier, got multiple values"
|
||||
"Expected only one of (prompt, state_modifier), got multiple values"
|
||||
)
|
||||
|
||||
if state_modifier is not None:
|
||||
prompt = state_modifier
|
||||
elif messages_modifier is not None:
|
||||
prompt = _convert_messages_modifier_to_prompt(messages_modifier)
|
||||
|
||||
kwargs["prompt"] = prompt
|
||||
return func(*args, **kwargs)
|
||||
@@ -244,7 +213,7 @@ def _validate_chat_history(
|
||||
@_convert_modifier_to_prompt
|
||||
def create_react_agent(
|
||||
model: Union[str, LanguageModelLike],
|
||||
tools: Union[ToolExecutor, Sequence[BaseTool], ToolNode],
|
||||
tools: Union[Sequence[BaseTool], ToolNode],
|
||||
*,
|
||||
prompt: Optional[Prompt] = None,
|
||||
response_format: Optional[
|
||||
@@ -264,7 +233,7 @@ def create_react_agent(
|
||||
|
||||
Args:
|
||||
model: The `LangChain` chat model that supports tool calling.
|
||||
tools: A list of tools, a ToolExecutor, or a ToolNode instance.
|
||||
tools: A list of tools or a ToolNode instance.
|
||||
If an empty list is provided, the agent will consist of a single LLM node without tool calling.
|
||||
prompt: An optional prompt for the LLM. Can take a few different forms:
|
||||
|
||||
@@ -273,8 +242,6 @@ def create_react_agent(
|
||||
- Callable: This function should take in full graph state and the output is then passed to the language model.
|
||||
- Runnable: This runnable should take in full graph state and the output is then passed to the language model.
|
||||
|
||||
!!! Note
|
||||
Prior to `v0.2.68`, the prompt was set using `state_modifier` / `messages_modifier` parameters.
|
||||
response_format: An optional schema for the final agent output.
|
||||
|
||||
If provided, output will be formatted to match the given schema and returned in the 'structured_response' state key.
|
||||
@@ -617,10 +584,7 @@ def create_react_agent(
|
||||
else AgentState
|
||||
)
|
||||
|
||||
if isinstance(tools, ToolExecutor):
|
||||
tool_classes: Sequence[BaseTool] = tools.tools
|
||||
tool_node = ToolNode(tool_classes)
|
||||
elif isinstance(tools, ToolNode):
|
||||
if isinstance(tools, ToolNode):
|
||||
tool_classes = list(tools.tools_by_name.values())
|
||||
tool_node = tools
|
||||
else:
|
||||
+12
-7
@@ -29,6 +29,7 @@ from langchain_core.runnables import (
|
||||
)
|
||||
from langchain_core.runnables.config import get_executor_for_config
|
||||
from langchain_core.tools import BaseTool, create_schema_from_function
|
||||
from langchain_core.utils.pydantic import is_basemodel_subclass
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
from pydantic.v1 import ValidationError as ValidationErrorV1
|
||||
@@ -78,7 +79,7 @@ class ValidationNode(RunnableCallable):
|
||||
>>> from typing_extensions import TypedDict
|
||||
...
|
||||
>>> from langchain_anthropic import ChatAnthropic
|
||||
>>> from pydantic import BaseModel, validator
|
||||
>>> from pydantic import BaseModel, field_validator
|
||||
...
|
||||
>>> from langgraph.graph import END, START, StateGraph
|
||||
>>> from langgraph.prebuilt import ValidationNode
|
||||
@@ -88,18 +89,15 @@ class ValidationNode(RunnableCallable):
|
||||
>>> class SelectNumber(BaseModel):
|
||||
... a: int
|
||||
...
|
||||
... @validator("a")
|
||||
... @field_validator("a")
|
||||
... def a_must_be_meaningful(cls, v):
|
||||
... if v != 37:
|
||||
... raise ValueError("Only 37 is allowed")
|
||||
... return v
|
||||
...
|
||||
...
|
||||
>>> class State(TypedDict):
|
||||
... messages: Annotated[list, add_messages]
|
||||
...
|
||||
>>> builder = StateGraph(State)
|
||||
>>> llm = ChatAnthropic(model="claude-3-haiku-20240307").bind_tools([SelectNumber])
|
||||
>>> builder = StateGraph(Annotated[list, add_messages])
|
||||
>>> llm = ChatAnthropic(model="claude-3-5-haiku-latest").bind_tools([SelectNumber])
|
||||
>>> builder.add_node("model", llm)
|
||||
>>> builder.add_node("validation", ValidationNode([SelectNumber]))
|
||||
>>> builder.add_edge(START, "model")
|
||||
@@ -177,6 +175,13 @@ class ValidationNode(RunnableCallable):
|
||||
raise ValueError(
|
||||
f"Tool {schema.name} does not have an args_schema defined."
|
||||
)
|
||||
elif not isinstance(
|
||||
schema.args_schema, type
|
||||
) or not is_basemodel_subclass(schema.args_schema):
|
||||
raise ValueError(
|
||||
"Validation node only works with tools that have a pydantic BaseModel args_schema. "
|
||||
f"Got {schema.name} with args_schema: {schema.args_schema}."
|
||||
)
|
||||
self.schemas_by_name[schema.name] = schema.args_schema
|
||||
elif isinstance(schema, type) and issubclass(
|
||||
schema, (BaseModel, BaseModelV1)
|
||||
Generated
+1475
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,63 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "0.1.0"
|
||||
description = "Library with high-level APIs for creating and executing LangGraph agents and tools."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
repository = "https://www.github.com/langchain-ai/langgraph"
|
||||
packages = [{ include = "langgraph" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0,<4.0"
|
||||
langgraph = ">=0.3,<0.4"
|
||||
langgraph-checkpoint = "^2.0.10"
|
||||
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,!=0.3.15,!=0.3.16,!=0.3.17,!=0.3.18,!=0.3.19,!=0.3.20,!=0.3.21,!=0.3.22"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.6.2"
|
||||
codespell = "^2.2.0"
|
||||
pytest = "^7.2.1"
|
||||
pytest-asyncio = "^0.21.1"
|
||||
pytest-mock = "^3.11.1"
|
||||
pytest-watcher = "^0.4.1"
|
||||
mypy = "^1.10.0"
|
||||
langgraph = {path = "../langgraph", develop = true}
|
||||
langgraph-checkpoint = {path = "../checkpoint", develop = true}
|
||||
langgraph-checkpoint-sqlite = {path = "../checkpoint-sqlite", develop = true}
|
||||
langgraph-checkpoint-postgres = {path = "../checkpoint-postgres", develop = true}
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
# --strict-markers will raise errors on unknown marks.
|
||||
# https://docs.pytest.org/en/7.1.x/how-to/mark.html#raising-errors-on-unknown-marks
|
||||
#
|
||||
# https://docs.pytest.org/en/7.1.x/reference/reference.html
|
||||
# --strict-config any warnings encountered while parsing the `pytest`
|
||||
# section of the configuration file raise errors.
|
||||
addopts = "--strict-markers --strict-config --durations=5 -vv"
|
||||
asyncio_mode = "auto"
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.ruff]
|
||||
lint.select = [ "E", "F", "I", "TID251" ]
|
||||
lint.ignore = [ "E501" ]
|
||||
|
||||
[tool.pytest-watcher]
|
||||
now = true
|
||||
delay = 0.1
|
||||
runner_args = ["--ff", "-v", "--tb", "short"]
|
||||
patterns = ["*.py"]
|
||||
|
||||
[tool.mypy]
|
||||
# https://mypy.readthedocs.io/en/stable/config_file.html
|
||||
disallow_untyped_defs = "True"
|
||||
explicit_package_bases = "True"
|
||||
warn_no_return = "False"
|
||||
warn_unused_ignores = "True"
|
||||
warn_redundant_casts = "True"
|
||||
allow_redefinition = "True"
|
||||
disable_error_code = "typeddict-item, return-value"
|
||||
@@ -0,0 +1,86 @@
|
||||
import re
|
||||
from typing import Any, Sequence, Union
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
|
||||
class FloatBetween(float):
|
||||
def __new__(cls, min_value: float, max_value: float) -> Self:
|
||||
return super().__new__(cls, min_value)
|
||||
|
||||
def __init__(self, min_value: float, max_value: float) -> None:
|
||||
super().__init__()
|
||||
self.min_value = min_value
|
||||
self.max_value = max_value
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return (
|
||||
isinstance(other, float)
|
||||
and other >= self.min_value
|
||||
and other <= self.max_value
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((float(self), self.min_value, self.max_value))
|
||||
|
||||
|
||||
class AnyStr(str):
|
||||
def __init__(self, prefix: Union[str, re.Pattern] = "") -> None:
|
||||
super().__init__()
|
||||
self.prefix = prefix
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, str) and (
|
||||
other.startswith(self.prefix)
|
||||
if isinstance(self.prefix, str)
|
||||
else self.prefix.match(other)
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((str(self), self.prefix))
|
||||
|
||||
|
||||
class AnyDict(dict):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, dict) or len(self) != len(other):
|
||||
return False
|
||||
for k, v in self.items():
|
||||
if kk := next((kk for kk in other if kk == k), None):
|
||||
if v == other[kk]:
|
||||
continue
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
class AnyVersion:
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return isinstance(other, (str, int, float))
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(str(self))
|
||||
|
||||
|
||||
class UnsortedSequence:
|
||||
def __init__(self, *values: Any) -> None:
|
||||
self.seq = values
|
||||
|
||||
def __eq__(self, value: object) -> bool:
|
||||
return (
|
||||
isinstance(value, Sequence)
|
||||
and len(self.seq) == len(value)
|
||||
and all(a in value for a in self.seq)
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(frozenset(self.seq))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return repr(self.seq)
|
||||
@@ -0,0 +1,17 @@
|
||||
name: langgraph-tests
|
||||
services:
|
||||
postgres-test:
|
||||
image: postgres:16
|
||||
ports:
|
||||
- "5442:5432"
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
healthcheck:
|
||||
test: pg_isready -U postgres
|
||||
start_period: 10s
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
interval: 60s
|
||||
start_interval: 1s
|
||||
@@ -0,0 +1,448 @@
|
||||
import sys
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncIterator, Optional
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
from langchain_core import __version__ as core_version
|
||||
from packaging import version
|
||||
from psycopg import AsyncConnection, Connection
|
||||
from psycopg_pool import AsyncConnectionPool, ConnectionPool
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver
|
||||
from langgraph.checkpoint.postgres import PostgresSaver, ShallowPostgresSaver
|
||||
from langgraph.checkpoint.postgres.aio import (
|
||||
AsyncPostgresSaver,
|
||||
AsyncShallowPostgresSaver,
|
||||
)
|
||||
from langgraph.checkpoint.sqlite import SqliteSaver
|
||||
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
from langgraph.store.postgres import AsyncPostgresStore, PostgresStore
|
||||
|
||||
pytest.register_assert_rewrite("tests.memory_assert")
|
||||
|
||||
DEFAULT_POSTGRES_URI = "postgres://postgres:postgres@localhost:5442/"
|
||||
# TODO: fix this once core is released
|
||||
IS_LANGCHAIN_CORE_030_OR_GREATER = version.parse(core_version) >= version.parse(
|
||||
"0.3.0.dev0"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def anyio_backend():
|
||||
return "asyncio"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def deterministic_uuids(mocker: MockerFixture) -> MockerFixture:
|
||||
side_effect = (
|
||||
UUID(f"00000000-0000-4000-8000-{i:012}", version=4) for i in range(10000)
|
||||
)
|
||||
return mocker.patch("uuid.uuid4", side_effect=side_effect)
|
||||
|
||||
|
||||
# checkpointer fixtures
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_memory():
|
||||
from tests.memory_assert import MemorySaverAssertImmutable
|
||||
|
||||
yield MemorySaverAssertImmutable()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_sqlite():
|
||||
with SqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_sqlite_aio():
|
||||
async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer:
|
||||
yield checkpointer
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_postgres():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
with PostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_postgres_shallow():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
with ShallowPostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_postgres_pipe():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
with PostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
checkpointer.setup()
|
||||
# setup can't run inside pipeline because of implicit transaction
|
||||
with checkpointer.conn.pipeline() as pipe:
|
||||
checkpointer.pipe = pipe
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def checkpointer_postgres_pool():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
with ConnectionPool(
|
||||
DEFAULT_POSTGRES_URI + database, max_size=10, kwargs={"autocommit": True}
|
||||
) as pool:
|
||||
checkpointer = PostgresSaver(pool)
|
||||
checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncPostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
await checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio_shallow():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncShallowPostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
await checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio_pipe():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncPostgresSaver.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as checkpointer:
|
||||
await checkpointer.setup()
|
||||
# setup can't run inside pipeline because of implicit transaction
|
||||
async with checkpointer.conn.pipeline() as pipe:
|
||||
checkpointer.pipe = pipe
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _checkpointer_postgres_aio_pool():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield checkpointer
|
||||
async with AsyncConnectionPool(
|
||||
DEFAULT_POSTGRES_URI + database, max_size=10, kwargs={"autocommit": True}
|
||||
) as pool:
|
||||
checkpointer = AsyncPostgresSaver(pool)
|
||||
await checkpointer.setup()
|
||||
yield checkpointer
|
||||
finally:
|
||||
# drop unique db
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def awith_checkpointer(
|
||||
checkpointer_name: Optional[str],
|
||||
) -> AsyncIterator[BaseCheckpointSaver]:
|
||||
if checkpointer_name is None:
|
||||
yield None
|
||||
elif checkpointer_name == "memory":
|
||||
from tests.memory_assert import MemorySaverAssertImmutable
|
||||
|
||||
yield MemorySaverAssertImmutable()
|
||||
elif checkpointer_name == "sqlite_aio":
|
||||
async with _checkpointer_sqlite_aio() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_aio":
|
||||
async with _checkpointer_postgres_aio() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_aio_shallow":
|
||||
async with _checkpointer_postgres_aio_shallow() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_aio_pipe":
|
||||
async with _checkpointer_postgres_aio_pipe() as checkpointer:
|
||||
yield checkpointer
|
||||
elif checkpointer_name == "postgres_aio_pool":
|
||||
async with _checkpointer_postgres_aio_pool() as checkpointer:
|
||||
yield checkpointer
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown checkpointer: {checkpointer_name}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _store_postgres_aio():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as store:
|
||||
await store.setup()
|
||||
yield store
|
||||
finally:
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _store_postgres_aio_pipe():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database
|
||||
) as store:
|
||||
await store.setup() # Run in its own transaction
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database, pipeline=True
|
||||
) as store:
|
||||
yield store
|
||||
finally:
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _store_postgres_aio_pool():
|
||||
if sys.version_info < (3, 10):
|
||||
pytest.skip("Async Postgres tests require Python 3.10+")
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
async with AsyncPostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database,
|
||||
pool_config={"max_size": 10},
|
||||
) as store:
|
||||
await store.setup()
|
||||
yield store
|
||||
finally:
|
||||
async with await AsyncConnection.connect(
|
||||
DEFAULT_POSTGRES_URI, autocommit=True
|
||||
) as conn:
|
||||
await conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def store_postgres():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield store
|
||||
with PostgresStore.from_conn_string(DEFAULT_POSTGRES_URI + database) as store:
|
||||
store.setup()
|
||||
yield store
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def store_postgres_pipe():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield store
|
||||
with PostgresStore.from_conn_string(DEFAULT_POSTGRES_URI + database) as store:
|
||||
store.setup() # Run in its own transaction
|
||||
with PostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database, pipeline=True
|
||||
) as store:
|
||||
yield store
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def store_postgres_pool():
|
||||
database = f"test_{uuid4().hex[:16]}"
|
||||
# create unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"CREATE DATABASE {database}")
|
||||
try:
|
||||
# yield store
|
||||
with PostgresStore.from_conn_string(
|
||||
DEFAULT_POSTGRES_URI + database, pool_config={"max_size": 10}
|
||||
) as store:
|
||||
store.setup()
|
||||
yield store
|
||||
finally:
|
||||
# drop unique db
|
||||
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
|
||||
conn.execute(f"DROP DATABASE {database}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def store_in_memory():
|
||||
yield InMemoryStore()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def awith_store(store_name: Optional[str]) -> AsyncIterator[BaseStore]:
|
||||
if store_name is None:
|
||||
yield None
|
||||
elif store_name == "in_memory":
|
||||
yield InMemoryStore()
|
||||
elif store_name == "postgres_aio":
|
||||
async with _store_postgres_aio() as store:
|
||||
yield store
|
||||
elif store_name == "postgres_aio_pipe":
|
||||
async with _store_postgres_aio_pipe() as store:
|
||||
yield store
|
||||
elif store_name == "postgres_aio_pool":
|
||||
async with _store_postgres_aio_pool() as store:
|
||||
yield store
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown store {store_name}")
|
||||
|
||||
|
||||
ALL_CHECKPOINTERS_SYNC = [
|
||||
"memory",
|
||||
"sqlite",
|
||||
"postgres",
|
||||
"postgres_pipe",
|
||||
"postgres_pool",
|
||||
"postgres_shallow",
|
||||
]
|
||||
|
||||
ALL_CHECKPOINTERS_ASYNC = [
|
||||
"memory",
|
||||
"sqlite_aio",
|
||||
"postgres_aio",
|
||||
"postgres_aio_pipe",
|
||||
"postgres_aio_pool",
|
||||
"postgres_aio_shallow",
|
||||
]
|
||||
@@ -0,0 +1,134 @@
|
||||
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
|
||||
|
||||
from langgraph.checkpoint.base import (
|
||||
ChannelVersions,
|
||||
Checkpoint,
|
||||
CheckpointMetadata,
|
||||
CheckpointTuple,
|
||||
SerializerProtocol,
|
||||
copy_checkpoint,
|
||||
)
|
||||
from langgraph.checkpoint.memory import InMemorySaver, PersistentDict
|
||||
|
||||
|
||||
class NoopSerializer(SerializerProtocol):
|
||||
def loads_typed(self, data: tuple[str, bytes]) -> Any:
|
||||
return data[1]
|
||||
|
||||
def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
|
||||
return "type", obj
|
||||
|
||||
|
||||
class MemorySaverAssertImmutable(InMemorySaver):
|
||||
storage_for_copies: defaultdict[str, dict[str, dict[str, Checkpoint]]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
serde: Optional[SerializerProtocol] = None,
|
||||
put_sleep: Optional[float] = None,
|
||||
) -> None:
|
||||
_, 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,
|
||||
config: dict,
|
||||
checkpoint: Checkpoint,
|
||||
metadata: CheckpointMetadata,
|
||||
new_versions: ChannelVersions,
|
||||
) -> None:
|
||||
if self.put_sleep:
|
||||
import time
|
||||
|
||||
time.sleep(self.put_sleep)
|
||||
# assert checkpoint hasn't been modified since last written
|
||||
thread_id = config["configurable"]["thread_id"]
|
||||
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
||||
if saved := super().get(config):
|
||||
assert (
|
||||
self.serde.loads_typed(
|
||||
self.storage_for_copies[thread_id][checkpoint_ns][saved["id"]]
|
||||
)
|
||||
== saved
|
||||
)
|
||||
self.storage_for_copies[thread_id][checkpoint_ns][checkpoint["id"]] = (
|
||||
self.serde.dumps_typed(copy_checkpoint(checkpoint))
|
||||
)
|
||||
# call super to write checkpoint
|
||||
return super().put(config, checkpoint, metadata, new_versions)
|
||||
|
||||
|
||||
class MemorySaverAssertCheckpointMetadata(InMemorySaver):
|
||||
"""This custom checkpointer is for verifying that a run's configurable
|
||||
fields are merged with the previous checkpoint config for each step in
|
||||
the run. This is the desired behavior. Because the checkpointer's (a)put()
|
||||
method is called for each step, the implementation of this checkpointer
|
||||
should produce a side effect that can be asserted.
|
||||
"""
|
||||
|
||||
def put(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
checkpoint: Checkpoint,
|
||||
metadata: CheckpointMetadata,
|
||||
new_versions: ChannelVersions,
|
||||
) -> None:
|
||||
"""The implementation of put() merges config["configurable"] (a run's
|
||||
configurable fields) with the metadata field. The state of the
|
||||
checkpoint metadata can be asserted to confirm that the run's
|
||||
configurable fields were merged with the previous checkpoint config.
|
||||
"""
|
||||
configurable = config["configurable"].copy()
|
||||
|
||||
# remove checkpoint_id to make testing simpler
|
||||
checkpoint_id = configurable.pop("checkpoint_id", None)
|
||||
thread_id = config["configurable"]["thread_id"]
|
||||
checkpoint_ns = config["configurable"]["checkpoint_ns"]
|
||||
self.storage[thread_id][checkpoint_ns].update(
|
||||
{
|
||||
checkpoint["id"]: (
|
||||
self.serde.dumps_typed(checkpoint),
|
||||
# merge configurable fields and metadata
|
||||
self.serde.dumps_typed({**configurable, **metadata}),
|
||||
checkpoint_id,
|
||||
)
|
||||
}
|
||||
)
|
||||
return {
|
||||
"configurable": {
|
||||
"thread_id": config["configurable"]["thread_id"],
|
||||
"checkpoint_id": checkpoint["id"],
|
||||
}
|
||||
}
|
||||
|
||||
async def aput(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
checkpoint: Checkpoint,
|
||||
metadata: CheckpointMetadata,
|
||||
new_versions: ChannelVersions,
|
||||
) -> RunnableConfig:
|
||||
return await asyncio.get_running_loop().run_in_executor(
|
||||
None, self.put, config, checkpoint, metadata, new_versions
|
||||
)
|
||||
|
||||
|
||||
class MemorySaverNoPending(InMemorySaver):
|
||||
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
|
||||
result = super().get_tuple(config)
|
||||
if result:
|
||||
return CheckpointTuple(result.config, result.checkpoint, result.metadata)
|
||||
return result
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Redefined messages as a work-around for pydantic issue with AnyStr.
|
||||
|
||||
The code below creates version of pydantic models
|
||||
that will work in unit tests with AnyStr as id field
|
||||
Please note that the `id` field is assigned AFTER the model is created
|
||||
to workaround an issue with pydantic ignoring the __eq__ method on
|
||||
subclassed strings.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.documents import Document
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage, ToolMessage
|
||||
|
||||
from tests.any_str import AnyStr
|
||||
|
||||
|
||||
def _AnyIdDocument(**kwargs: Any) -> Document:
|
||||
"""Create a document with an id field."""
|
||||
message = Document(**kwargs)
|
||||
message.id = AnyStr()
|
||||
return message
|
||||
|
||||
|
||||
def _AnyIdAIMessage(**kwargs: Any) -> AIMessage:
|
||||
"""Create ai message with an any id field."""
|
||||
message = AIMessage(**kwargs)
|
||||
message.id = AnyStr()
|
||||
return message
|
||||
|
||||
|
||||
def _AnyIdAIMessageChunk(**kwargs: Any) -> AIMessageChunk:
|
||||
"""Create ai message with an any id field."""
|
||||
message = AIMessageChunk(**kwargs)
|
||||
message.id = AnyStr()
|
||||
return message
|
||||
|
||||
|
||||
def _AnyIdHumanMessage(**kwargs: Any) -> HumanMessage:
|
||||
"""Create a human message with an any id field."""
|
||||
message = HumanMessage(**kwargs)
|
||||
message.id = AnyStr()
|
||||
return message
|
||||
|
||||
|
||||
def _AnyIdToolMessage(**kwargs: Any) -> ToolMessage:
|
||||
"""Create a tool message with an any id field."""
|
||||
message = ToolMessage(**kwargs)
|
||||
message.id = AnyStr()
|
||||
return message
|
||||
@@ -0,0 +1,98 @@
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
Sequence,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
|
||||
from langchain_core.callbacks import CallbackManagerForLLMRun
|
||||
from langchain_core.language_models import BaseChatModel, LanguageModelInput
|
||||
from langchain_core.messages import (
|
||||
AIMessage,
|
||||
BaseMessage,
|
||||
ToolCall,
|
||||
)
|
||||
from langchain_core.outputs import ChatGeneration, ChatResult
|
||||
from langchain_core.runnables import Runnable, RunnableLambda
|
||||
from langchain_core.tools import BaseTool
|
||||
from pydantic import BaseModel
|
||||
|
||||
from langgraph.prebuilt.chat_agent_executor import StructuredResponse
|
||||
|
||||
|
||||
class FakeToolCallingModel(BaseChatModel):
|
||||
tool_calls: Optional[list[list[ToolCall]]] = None
|
||||
structured_response: Optional[StructuredResponse] = None
|
||||
index: int = 0
|
||||
tool_style: Literal["openai", "anthropic"] = "openai"
|
||||
|
||||
def _generate(
|
||||
self,
|
||||
messages: List[BaseMessage],
|
||||
stop: Optional[List[str]] = None,
|
||||
run_manager: Optional[CallbackManagerForLLMRun] = None,
|
||||
**kwargs: Any,
|
||||
) -> ChatResult:
|
||||
"""Top Level call"""
|
||||
messages_string = "-".join([m.content for m in messages])
|
||||
tool_calls = (
|
||||
self.tool_calls[self.index % len(self.tool_calls)]
|
||||
if self.tool_calls
|
||||
else []
|
||||
)
|
||||
message = AIMessage(
|
||||
content=messages_string, id=str(self.index), tool_calls=tool_calls.copy()
|
||||
)
|
||||
self.index += 1
|
||||
return ChatResult(generations=[ChatGeneration(message=message)])
|
||||
|
||||
@property
|
||||
def _llm_type(self) -> str:
|
||||
return "fake-tool-call-model"
|
||||
|
||||
def with_structured_output(
|
||||
self, schema: Type[BaseModel]
|
||||
) -> Runnable[LanguageModelInput, StructuredResponse]:
|
||||
if self.structured_response is None:
|
||||
raise ValueError("Structured response is not set")
|
||||
|
||||
return RunnableLambda(lambda x: self.structured_response)
|
||||
|
||||
def bind_tools(
|
||||
self,
|
||||
tools: Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]],
|
||||
**kwargs: Any,
|
||||
) -> Runnable[LanguageModelInput, BaseMessage]:
|
||||
if len(tools) == 0:
|
||||
raise ValueError("Must provide at least one tool")
|
||||
|
||||
tool_dicts = []
|
||||
for tool in tools:
|
||||
if not isinstance(tool, BaseTool):
|
||||
raise TypeError(
|
||||
"Only BaseTool is supported by FakeToolCallingModel.bind_tools"
|
||||
)
|
||||
|
||||
# NOTE: this is a simplified tool spec for testing purposes only
|
||||
if self.tool_style == "openai":
|
||||
tool_dicts.append(
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool.name,
|
||||
},
|
||||
}
|
||||
)
|
||||
elif self.tool_style == "anthropic":
|
||||
tool_dicts.append(
|
||||
{
|
||||
"name": tool.name,
|
||||
}
|
||||
)
|
||||
|
||||
return self.bind(tools=tool_dicts)
|
||||
+120
-1279
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,81 @@
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from langchain_core.messages import AIMessage
|
||||
from langchain_core.tools import tool as dec_tool
|
||||
from pydantic import BaseModel
|
||||
from pydantic.v1 import BaseModel as BaseModelV1
|
||||
|
||||
from langgraph.prebuilt import ValidationNode
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
def my_function(some_val: int, some_other_val: str) -> str:
|
||||
return f"{some_val} - {some_other_val}"
|
||||
|
||||
|
||||
class MyModel(BaseModel):
|
||||
some_val: int
|
||||
some_other_val: str
|
||||
|
||||
|
||||
class MyModelV1(BaseModelV1):
|
||||
some_val: int
|
||||
some_other_val: str
|
||||
|
||||
|
||||
@dec_tool
|
||||
def my_tool(some_val: int, some_other_val: str) -> str:
|
||||
"""Cool."""
|
||||
return f"{some_val} - {some_other_val}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"tool_schema",
|
||||
[
|
||||
my_function,
|
||||
MyModel,
|
||||
MyModelV1,
|
||||
my_tool,
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("use_message_key", [True, False])
|
||||
async def test_validation_node(tool_schema: Any, use_message_key: bool):
|
||||
validation_node = ValidationNode([tool_schema])
|
||||
tool_name = getattr(tool_schema, "name", getattr(tool_schema, "__name__", None))
|
||||
inputs = [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": tool_name,
|
||||
"args": {"some_val": 1, "some_other_val": "foo"},
|
||||
"id": "some 0",
|
||||
},
|
||||
{
|
||||
"name": tool_name,
|
||||
# Wrong type for some_val
|
||||
"args": {"some_val": "bar", "some_other_val": "foo"},
|
||||
"id": "some 1",
|
||||
},
|
||||
],
|
||||
),
|
||||
]
|
||||
if use_message_key:
|
||||
inputs = {"messages": inputs}
|
||||
result = await validation_node.ainvoke(inputs)
|
||||
if use_message_key:
|
||||
result = result["messages"]
|
||||
|
||||
def check_results(messages: list):
|
||||
assert len(messages) == 2
|
||||
assert all(m.type == "tool" for m in messages)
|
||||
assert not messages[0].additional_kwargs.get("is_error")
|
||||
assert messages[1].additional_kwargs.get("is_error")
|
||||
|
||||
check_results(result)
|
||||
result_sync = validation_node.invoke(inputs)
|
||||
if use_message_key:
|
||||
result_sync = result_sync["messages"]
|
||||
check_results(result_sync)
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@langchain/langgraph-sdk",
|
||||
"version": "0.0.46",
|
||||
"version": "0.0.45",
|
||||
"description": "Client library for interacting with the LangGraph API",
|
||||
"type": "module",
|
||||
"packageManager": "yarn@1.22.19",
|
||||
|
||||
Reference in New Issue
Block a user