Compare commits

..
Author SHA1 Message Date
Vadym BardaandGitHub 3778f6113c prebuilt: release 0.1.0 (#3610) 2025-02-26 18:48:03 -05:00
Vadym BardaandGitHub 24c13c211e langgraph: separate prebuilt into a standalone package (#3589) 2025-02-26 18:33:07 -05:00
Vadym BardaandGitHub 2e36189c16 langgraph: release 0.2.76 (#3609) 2025-02-26 16:41:28 -05:00
Vadym BardaandGitHub dac11f875c langgraph: fix get_state(subgraphs=True) for checkpointer=True (#3607) 2025-02-26 20:43:46 +00:00
andrestorres123andGitHub 264be423f9 docs: Add Delve Taxonomy Generator package to the Prebuilt Agents (#3606) 2025-02-26 19:03:51 +00:00
HackHuangandGitHub 9a05600ff9 docs(concepts) : Add a tutorial link for map-reduce (#3577)
Update `low_level.md`: Add a tutorial link for map-reduce, now it's
perfect !!!
2025-02-26 12:23:02 -05:00
Eugene YurtsevandGitHub 56dd728975 docs: remove one more beta (#3601) 2025-02-26 12:22:22 -05:00
Eugene YurtsevandGitHub c157c956f4 docs: prebuilt add github stars (#3603) 2025-02-26 12:22:08 -05:00
Vadym BardaandGitHub dd293dad30 langgraph: release 0.2.75 (#3602) 2025-02-26 11:47:16 -05:00
Vadym BardaandGitHub 30811d7841 langgraph: add py.typed files to modules (#3600) 2025-02-26 16:41:51 +00:00
Vadym BardaandGitHub 28a705b71a docs: update requirements (#3598) 2025-02-26 10:35:16 -05:00
Vadym BardaandGitHub 3b8130b96f docs: add swarm (#3597) 2025-02-26 09:59:29 -05:00
Harrison ChaseandGitHub 94fc0adb05 faq about no langsmith (#3596) 2025-02-26 09:43:28 -05:00
Nuno CamposandGitHub 162e96262f langgraph: stream_mode=messages should not emit input or state messages (#3591)
- any messages seen in inputs in on_chain_start should not be emitted
2025-02-25 19:09:57 -08:00
Nuno Campos 1bb0037450 langgraph: stream_mode=messages should not emit input or state messages
- any messages seen in inputs in on_chain_start should not be emitted
2025-02-25 19:00:33 -08:00
Nuno CamposandGitHub 50c53d3120 checkpoint-sqlite: update aiosqlite bounds (#3540)
aiosqlite 0.21.0 was released on Feb 2

It seems to be a maintenance release:
https://github.com/omnilib/aiosqlite/blob/main/CHANGELOG.md#v0210
2025-02-25 16:56:10 -08:00
Nuno CamposandGitHub 0b7b849633 Update adopters.md to include Cisco Outshift (#3573) 2025-02-25 16:47:52 -08:00
Nuno CamposandGitHub 678eb5cdbe pregel: update validation error messages (#3517)
OK not including as well, but this was useful for troubleshooting as a
new user (especially before there were any examples)
2025-02-25 16:45:57 -08:00
Andrew NguonlyandGitHub bdf1215ced docs: Add docs for DD_API_KEY env var (#3585) 2025-02-25 11:24:59 -08:00
William FHandGitHub cba1852720 Fix typo (#3583) 2025-02-25 18:19:48 +00:00
jessicaouandGitHub b8fafa2795 Update adopters.md to include Cisco Outshift 2025-02-24 18:34:38 -08:00
Theodore NiandGitHub e6681bc175 Merge branch 'main' into update-aiosqlite-bounds 2025-02-22 06:16:58 -06:00
Theodore Ni a9cdb9c948 checkpoint-sqlite: update aiosqlite bounds 2025-02-20 14:51:51 -08:00
Eugene Yurtsev 4dc8f813e7 x 2025-02-19 16:16:14 -05:00
80 changed files with 4322 additions and 2604 deletions
+3
View File
@@ -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
+1 -1
View File
@@ -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)"
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
matrix:
lib-version:
- "development"
- "latest"
# - "latest"
steps:
- uses: actions/checkout@v4
+1 -1
View File
@@ -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"![GitHub stars]({stars_badge})"
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
+10 -4
View File
@@ -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"
+1
View File
@@ -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
+6
View File
@@ -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`.
+6
View File
@@ -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.
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -78,7 +78,7 @@
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-anthropic"
"%pip install -U langgraph langgraph-prebuilt langchain-anthropic"
]
},
{
+1 -1
View File
@@ -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"
]
},
{
+1 -1
View File
@@ -68,7 +68,7 @@
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-openai"
"%pip install -U langgraph langgraph-prebuilt langchain-openai"
]
},
{
+1 -1
View File
@@ -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"
]
},
{
+1 -1
View File
@@ -57,7 +57,7 @@
"outputs": [],
"source": [
"%%capture --no-stderr\n",
"%pip install -U langgraph langchain-anthropic"
"%pip install -U langgraph langgraph-prebuilt langchain-anthropic"
]
},
{
+1 -1
View File
@@ -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"
]
},
{
+1 -1
View File
@@ -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"
]
},
{
+2 -3
View File
@@ -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"
]
},
{
+1 -1
View File
@@ -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"
]
},
{
+1 -1
View File
@@ -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"
]
},
{
+1 -1
View File
@@ -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"
]
},
{
+7 -7
View File
@@ -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.
+54 -38
View File
@@ -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
View File
@@ -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"
+8 -8
View File
@@ -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"
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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
+14 -6
View File
@@ -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,
+17 -5
View File
@@ -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:
+28 -9
View File
@@ -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"
+3 -2
View File
@@ -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}
+151
View File
@@ -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"
+97 -3
View File
@@ -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"
+21
View File
@@ -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.
+78
View File
@@ -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'
+120
View File
@@ -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
...
```
@@ -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",
@@ -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:
@@ -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)
+1475
View File
File diff suppressed because it is too large Load Diff
+63
View File
@@ -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"
View File
+86
View File
@@ -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)
+17
View File
@@ -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
+448
View File
@@ -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",
]
+134
View File
@@ -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
+50
View File
@@ -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
+98
View File
@@ -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)
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 -1
View File
@@ -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",