Compare commits

...
Author SHA1 Message Date
William FHandGitHub 208cd4d70e Add default TTL in store & CLI (#3786) 2025-03-12 06:14:58 -07:00
Ben BurnsandGitHub 1352e58133 chore(langgraph): add functional api test for multiple task interrupts (#3790)
While working on langchain-ai/langgraphjs#984 I ported the test I was
debugging over to python so I could compare behavior. Figured I might as
well add it to this codebase, as I don't think we had this particular
case covered previously.
2025-03-12 18:50:24 +13:00
William Fu-Hinthorn f1162ac898 Bump 2025-03-11 20:22:58 -07:00
William Fu-Hinthorn 4de8443c5c Add default TTL in store & CLI 2025-03-11 20:15:38 -07:00
Nuno CamposandGitHub 96dc39aeab 0.3.7 2025-03-11 19:46:43 -07:00
Nuno CamposandGitHub 316f8410fa Avoid validating pydantic state models when we can (#3782)
- When a pydantic input schema isued but dict input is passed in
validate it once after running hidden START node. If the input is an
instance of the input model we skip validation altogether
- When entering each node we need to create a standalone instance of the
state class, but we can now skip validation, as it's now run once
elsewhere
2025-03-11 18:23:05 -07:00
Nuno Campos 1d3926af27 Fix kafka 2025-03-11 18:13:36 -07:00
Nuno Campos e566ed4b3f Fix py 3.9
- isclass and issubclass disagree on whether something like list[str] is a class
2025-03-11 17:51:13 -07:00
Nuno Campos 14c2241853 Lint 2025-03-11 17:44:24 -07:00
Nuno Campos 2c908f1557 Avoid validating pydantic state models when we can
- When a pydantic input schema isued but dict input is passed in validate it once after running hidden START node. If the input is an instance of the input model we skip validation altogether
- When entering each node we need to create a standalone instance of the state class, but we can now skip validation, as it's now run once elsewhere
2025-03-11 17:32:54 -07:00
William FHandGitHub 5005d1c004 Default store ttl config (#3781) 2025-03-11 17:18:16 -07:00
Vadym BardaandGitHub 02a46c45c8 langgraph: support subgraphs with a single node (#3780) 2025-03-12 00:09:35 +00:00
William Fu-Hinthorn 852a129881 Default store ttl config 2025-03-11 15:58:58 -07:00
84c956bc8c Add llms.txt (#3765)
Co-authored-by: Lance Martin <lance@langchain.dev>
2025-03-11 18:01:50 +00:00
Eugene YurtsevandGitHub b86e6b82f2 ci: add poetry check --lock to test workflow (#3777) 2025-03-11 17:42:37 +00:00
b1de5be334 docs: Fix version badge by linking it to PyPi instead of shield (#3766)
Currently the version badge showing langgraph version as PyPi shield
image is linking to the shield image. It would be more intuitive to link
it to PyPi.

---------

Co-authored-by: vbarda <vadym@langchain.dev>
2025-03-11 16:21:24 +00:00
David DuongandGitHub 0751428422 feat(sdk-js): cleanup types for ui payloads (#3773) 2025-03-11 17:04:12 +01:00
Tat Dat Duong b16f05405b Bump to 0.0.53 2025-03-11 16:59:56 +01:00
Vadym BardaandGitHub ca8d92421a langgraph: release 0.3.6 (#3775) 2025-03-11 11:34:36 -04:00
7aa9d3fd00 langgraph: use input schema from conditional edge (#2516)
Currently we ignore the input schema in the branch and instead use the
input schema from the previous node (or overall graph schema)

This change makes the input schema to branches respected. This means
that if you try to pass extra keys and they're NOT in the input schema,
you will receive an error. If you don't provide an annotation in the
router, it will fall back to the previous node's input schema / full
graph state schema

Alternative solution is to just ignore the input schema in the router
altogether (including ignoring the schema from previous node / full
graph), but personally I find it more confusing.

---------

Co-authored-by: Nuno Campos <nuno@langchain.dev>
2025-03-11 11:33:28 -04:00
Tat Dat Duong 857fd3578f feat(sdk-js): cleanup types for ui payloads 2025-03-11 15:02:42 +01:00
William FHandGitHub 3a4af1e573 chore(deps): bump axios from 1.7.7 to 1.8.2 in /libs/sdk-js (#3740)
Bumps [axios](https://github.com/axios/axios) from 1.7.7 to 1.8.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/axios/axios/releases">axios's
releases</a>.</em></p>
<blockquote>
<h2>Release v1.8.2</h2>
<h2>Release notes:</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>http-adapter:</strong> add allowAbsoluteUrls to path
building (<a
href="https://redirect.github.com/axios/axios/issues/6810">#6810</a>)
(<a
href="https://github.com/axios/axios/commit/fb8eec214ce7744b5ca787f2c3b8339b2f54b00f">fb8eec2</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a href="https://github.com/lexcorp16"
title="+1/-1 ([#6810](https://github.com/axios/axios/issues/6810)
)">Fasoro-Joseph Alexander</a></li>
</ul>
<h2>Release v1.8.1</h2>
<h2>Release notes:</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>utils:</strong> move <code>generateString</code> to platform
utils to avoid importing crypto module into client builds; (<a
href="https://redirect.github.com/axios/axios/issues/6789">#6789</a>)
(<a
href="https://github.com/axios/axios/commit/36a5a620bec0b181451927f13ac85b9888b86cec">36a5a62</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a
href="https://github.com/DigitalBrainJS" title="+51/-47
([#6789](https://github.com/axios/axios/issues/6789) )">Dmitriy
Mozgovoy</a></li>
</ul>
<h2>Release v1.8.0</h2>
<h2>Release notes:</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>examples:</strong> application crashed when navigating
examples in browser (<a
href="https://redirect.github.com/axios/axios/issues/5938">#5938</a>)
(<a
href="https://github.com/axios/axios/commit/1260ded634ec101dd5ed05d3b70f8e8f899dba6c">1260ded</a>)</li>
<li>missing word in SUPPORT_QUESTION.yml (<a
href="https://redirect.github.com/axios/axios/issues/6757">#6757</a>)
(<a
href="https://github.com/axios/axios/commit/1f890b13f2c25a016f3c84ae78efb769f244133e">1f890b1</a>)</li>
<li><strong>utils:</strong> replace getRandomValues with crypto module
(<a
href="https://redirect.github.com/axios/axios/issues/6788">#6788</a>)
(<a
href="https://github.com/axios/axios/commit/23a25af0688d1db2c396deb09229d2271cc24f6c">23a25af</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>Add config for ignoring absolute URLs (<a
href="https://redirect.github.com/axios/axios/issues/5902">#5902</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/6192">#6192</a>)
(<a
href="https://github.com/axios/axios/commit/32c7bcc0f233285ba27dec73a4b1e81fb7a219b3">32c7bcc</a>)</li>
</ul>
<h3>Reverts</h3>
<ul>
<li>Revert &quot;chore: expose fromDataToStream to be consumable (<a
href="https://redirect.github.com/axios/axios/issues/6731">#6731</a>)&quot;
(<a
href="https://redirect.github.com/axios/axios/issues/6732">#6732</a>)
(<a
href="https://github.com/axios/axios/commit/1317261125e9c419fe9f126867f64d28f9c1efda">1317261</a>),
closes <a
href="https://redirect.github.com/axios/axios/issues/6731">#6731</a> <a
href="https://redirect.github.com/axios/axios/issues/6732">#6732</a></li>
</ul>
<h3>BREAKING CHANGES</h3>
<ul>
<li>
<p>code relying on the above will now combine the URLs instead of prefer
request URL</p>
</li>
<li>
<p>feat: add config option for allowing absolute URLs</p>
</li>
<li>
<p>fix: add default value for allowAbsoluteUrls in buildFullPath</p>
</li>
<li>
<p>fix: typo in flow control when setting allowAbsoluteUrls</p>
</li>
</ul>
<h3>Contributors to this release</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/axios/axios/blob/v1.x/CHANGELOG.md">axios's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/axios/axios/compare/v1.8.1...v1.8.2">1.8.2</a>
(2025-03-07)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>http-adapter:</strong> add allowAbsoluteUrls to path
building (<a
href="https://redirect.github.com/axios/axios/issues/6810">#6810</a>)
(<a
href="https://github.com/axios/axios/commit/fb8eec214ce7744b5ca787f2c3b8339b2f54b00f">fb8eec2</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a href="https://github.com/lexcorp16"
title="+1/-1 ([#6810](https://github.com/axios/axios/issues/6810)
)">Fasoro-Joseph Alexander</a></li>
</ul>
<h2><a
href="https://github.com/axios/axios/compare/v1.8.0...v1.8.1">1.8.1</a>
(2025-02-26)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>utils:</strong> move <code>generateString</code> to platform
utils to avoid importing crypto module into client builds; (<a
href="https://redirect.github.com/axios/axios/issues/6789">#6789</a>)
(<a
href="https://github.com/axios/axios/commit/36a5a620bec0b181451927f13ac85b9888b86cec">36a5a62</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a
href="https://github.com/DigitalBrainJS" title="+51/-47
([#6789](https://github.com/axios/axios/issues/6789) )">Dmitriy
Mozgovoy</a></li>
</ul>
<h1><a
href="https://github.com/axios/axios/compare/v1.7.9...v1.8.0">1.8.0</a>
(2025-02-25)</h1>
<h3>Bug Fixes</h3>
<ul>
<li><strong>examples:</strong> application crashed when navigating
examples in browser (<a
href="https://redirect.github.com/axios/axios/issues/5938">#5938</a>)
(<a
href="https://github.com/axios/axios/commit/1260ded634ec101dd5ed05d3b70f8e8f899dba6c">1260ded</a>)</li>
<li>missing word in SUPPORT_QUESTION.yml (<a
href="https://redirect.github.com/axios/axios/issues/6757">#6757</a>)
(<a
href="https://github.com/axios/axios/commit/1f890b13f2c25a016f3c84ae78efb769f244133e">1f890b1</a>)</li>
<li><strong>utils:</strong> replace getRandomValues with crypto module
(<a
href="https://redirect.github.com/axios/axios/issues/6788">#6788</a>)
(<a
href="https://github.com/axios/axios/commit/23a25af0688d1db2c396deb09229d2271cc24f6c">23a25af</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>Add config for ignoring absolute URLs (<a
href="https://redirect.github.com/axios/axios/issues/5902">#5902</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/6192">#6192</a>)
(<a
href="https://github.com/axios/axios/commit/32c7bcc0f233285ba27dec73a4b1e81fb7a219b3">32c7bcc</a>)</li>
</ul>
<h3>Reverts</h3>
<ul>
<li>Revert &quot;chore: expose fromDataToStream to be consumable (<a
href="https://redirect.github.com/axios/axios/issues/6731">#6731</a>)&quot;
(<a
href="https://redirect.github.com/axios/axios/issues/6732">#6732</a>)
(<a
href="https://github.com/axios/axios/commit/1317261125e9c419fe9f126867f64d28f9c1efda">1317261</a>),
closes <a
href="https://redirect.github.com/axios/axios/issues/6731">#6731</a> <a
href="https://redirect.github.com/axios/axios/issues/6732">#6732</a></li>
</ul>
<h3>BREAKING CHANGES</h3>
<ul>
<li>
<p>code relying on the above will now combine the URLs instead of prefer
request URL</p>
</li>
<li>
<p>feat: add config option for allowing absolute URLs</p>
</li>
<li>
<p>fix: add default value for allowAbsoluteUrls in buildFullPath</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/axios/axios/commit/a9f7689b0c4b6d68c7f587c3aa376860da509d94"><code>a9f7689</code></a>
chore(release): v1.8.2 (<a
href="https://redirect.github.com/axios/axios/issues/6812">#6812</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/fb8eec214ce7744b5ca787f2c3b8339b2f54b00f"><code>fb8eec2</code></a>
fix(http-adapter): add allowAbsoluteUrls to path building (<a
href="https://redirect.github.com/axios/axios/issues/6810">#6810</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/98120457559e573024862e2925d56295a965ad7e"><code>9812045</code></a>
chore(sponsor): update sponsor block (<a
href="https://redirect.github.com/axios/axios/issues/6804">#6804</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/72acf759373ef4e211d5299818d19e50e08c02f8"><code>72acf75</code></a>
chore(sponsor): update sponsor block (<a
href="https://redirect.github.com/axios/axios/issues/6794">#6794</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/2e64afdff5c41e38284a6fb8312f2745072513a1"><code>2e64afd</code></a>
chore(release): v1.8.1 (<a
href="https://redirect.github.com/axios/axios/issues/6800">#6800</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/36a5a620bec0b181451927f13ac85b9888b86cec"><code>36a5a62</code></a>
fix(utils): move <code>generateString</code> to platform utils to avoid
importing crypto...</li>
<li><a
href="https://github.com/axios/axios/commit/cceb7b1e154fbf294135c93d3f91921643bbe49f"><code>cceb7b1</code></a>
chore(release): v1.8.0 (<a
href="https://redirect.github.com/axios/axios/issues/6795">#6795</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/23a25af0688d1db2c396deb09229d2271cc24f6c"><code>23a25af</code></a>
fix(utils): replace getRandomValues with crypto module (<a
href="https://redirect.github.com/axios/axios/issues/6788">#6788</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/32c7bcc0f233285ba27dec73a4b1e81fb7a219b3"><code>32c7bcc</code></a>
feat: Add config for ignoring absolute URLs (<a
href="https://redirect.github.com/axios/axios/issues/5902">#5902</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/6192">#6192</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/4a3e26cf65bb040b7eb4577d5fd62199b0f3d017"><code>4a3e26c</code></a>
chore(config): adjust rollup config to preserve license header to
minified Ja...</li>
<li>Additional commits viewable in <a
href="https://github.com/axios/axios/compare/v1.7.7...v1.8.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=axios&package-manager=npm_and_yarn&previous-version=1.7.7&new-version=1.8.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/langchain-ai/langgraph/network/alerts).

</details>
2025-03-10 17:50:20 -07:00
37344124e1 Fix updated_at timestamp loading (#3767)
Co-authored-by: Mohammad Mohtashim <45242107+keenborder786@users.noreply.github.com>
2025-03-10 22:20:12 +00:00
David DuongandGitHub d0f4db6ddd feat(sdk-js): use fetchClient from client in gen ui (#3761) 2025-03-10 17:48:25 +01:00
Vadym BardaandGitHub a9800aab87 checkpoint-sqlite: release 2.0.6 (#3763) 2025-03-10 11:25:38 -04:00
Vadym BardaandGitHub 9bf3fc2d0f checkpoint-sqlite: commit transactions in AsyncSqliteSaver.aput_writes (#3762) 2025-03-10 15:14:20 +00:00
Tat Dat Duong a6e4bd93ff Bump to 0.0.52 2025-03-10 15:02:06 +01:00
Tat Dat Duong 0e9c41f480 feat(sdk-js): use fetchClient from client in gen ui 2025-03-10 13:59:00 +01:00
David DuongandGitHub d4368cfa97 feat(sdk-js): api improvements for gen ui (#3760)
- merge `typedUi.create` and `typedUi.write` into `typedUi.push`
- Add mutate function in `onCustomEvent`
2025-03-10 13:31:07 +01:00
Tat Dat Duong 3808302309 Bump to 0.0.51 2025-03-10 13:25:55 +01:00
Tat Dat Duong 25019450e2 feat(sdk-js): api improvements for gen ui
- merge `typedUi.create` and `typedUi.write` into `typedUi.push`
- Add mutate function in `onCustomEvent`
2025-03-09 10:26:50 +01:00
dependabot[bot]andGitHub 263eab9f76 chore(deps): bump axios from 1.7.7 to 1.8.2 in /libs/sdk-js
Bumps [axios](https://github.com/axios/axios) from 1.7.7 to 1.8.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.7...v1.8.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-07 19:23:09 +00:00
39 changed files with 1156 additions and 293 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ jobs:
if: steps.changed-files.outputs.all
shell: bash
working-directory: ${{ inputs.working-directory }}
run: poetry lock --check
run: poetry check --lock
- name: Install dependencies
if: steps.changed-files.outputs.all
+6
View File
@@ -39,6 +39,12 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_RO_TOKEN }}
- name: Check Lock
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
poetry check --lock
- name: Install dependencies
shell: bash
working-directory: ${{ inputs.working-directory }}
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦜🕸️LangGraph
![Version](https://img.shields.io/pypi/v/langgraph)
[![Version](https://img.shields.io/pypi/v/langgraph.svg)](https://pypi.org/project/langgraph/)
[![Downloads](https://static.pepy.tech/badge/langgraph/month)](https://pepy.tech/project/langgraph)
[![Open Issues](https://img.shields.io/github/issues-raw/langchain-ai/langgraph)](https://github.com/langchain-ai/langgraph/issues)
[![Docs](https://img.shields.io/badge/docs-latest-blue)](https://langchain-ai.github.io/langgraph/)
+191
View File
@@ -0,0 +1,191 @@
# LangGraph
## Quickstart
These guides are designed to help you get started with LangGraph.
- [LangGraph Quickstart](https://langchain-ai.github.io/langgraph/tutorials/introduction/): Build a chatbot that can use tools and keep track of conversation history. Add human-in-the-loop capabilities and explore how time-travel works.
- [Common Workflows](https://langchain-ai.github.io/langgraph/tutorials/workflows/): Overview of the most common workflows using LLMs implemented with LangGraph.
- [LangGraph Server Quickstart](https://langchain-ai.github.io/langgraph/tutorials/langgraph-platform/local-server/): Launch a LangGraph server locally and interact with it using REST API and LangGraph Studio Web UI.
- [Deploy with LangGraph Cloud Quickstart](https://langchain-ai.github.io/langgraph/cloud/quick_start/): Deploy a LangGraph app using LangGraph Cloud.
## Concepts
These guides provide explanations of the key concepts behind the LangGraph framework.
- [Why LangGraph?](https://langchain-ai.github.io/langgraph/concepts/high_level/): Motivation for LangGraph, a library for building agentic applications with LLMs.
- [LangGraph Glossary](https://langchain-ai.github.io/langgraph/concepts/low_level/): LangGraph workflows are designed as graphs, with nodes representing different components and edges representing the flow of information between them. This guide provides an overview of the key concepts associated with LangGraph graph primitives.
- [Common Agentic Patterns](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/): An agent uses an LLM to pick its own control flow to solve more complex problems! Agents are a key building block in many LLM applications. This guide explains the different types of agent architectures and how they can be used to control the flow of an application.
- [Multi-Agent Systems](https://langchain-ai.github.io/langgraph/concepts/multi_agent/): Complex LLM applications can often be broken down into multiple agents, each responsible for a different part of the application. This guide explains common patterns for building multi-agent systems.
- [Breakpoints](https://langchain-ai.github.io/langgraph/concepts/breakpoints/): Breakpoints allow pausing the execution of a graph at specific points. Breakpoints allow stepping through graph execution for debugging purposes.
- [Human-in-the-Loop](https://langchain-ai.github.io/langgraph/concepts/human_in_the_loop/): Explains different ways of integrating human feedback into a LangGraph application.
- [Time Travel](https://langchain-ai.github.io/langgraph/concepts/time-travel/): Time travel allows you to replay past actions in your LangGraph application to explore alternative paths and debug issues.
- [Persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/): LangGraph has a built-in persistence layer, implemented through checkpointers. This persistence layer helps to support powerful capabilities like human-in-the-loop, memory, time travel, and fault-tolerance.
- [Memory](https://langchain-ai.github.io/langgraph/concepts/memory/): Memory in AI applications refers to the ability to process, store, and effectively recall information from past interactions. With memory, your agents can learn from feedback and adapt to users' preferences.
- [Streaming](https://langchain-ai.github.io/langgraph/concepts/streaming/): Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
- [Functional API](https://langchain-ai.github.io/langgraph/concepts/functional_api/): `@entrypoint` and `@task` decorators that allow you to add LangGraph functionality to an existing codebase.
- [Durable Execution](https://langchain-ai.github.io/langgraph/concepts/durable_execution/): LangGraph's built-in [persistence](https://langchain-ai.github.io/langgraph/concepts/persistence/) layer provides durable execution for workflows, ensuring that the state of each execution step is saved to a durable store.
- [Pregel](https://langchain-ai.github.io/langgraph/concepts/pregel/): Pregel is LangGraph's runtime, which is responsible for managing the execution of LangGraph applications.
- [FAQ](https://langchain-ai.github.io/langgraph/concepts/faq/): Frequently asked questions about LangGraph.
## How-tos
Here youll find answers to “How do I...?” types of questions.
These guides are **goal-oriented** and concrete.
They're meant to help you complete a specific task.
### Graph API Basics
- [How to update graph state from nodes](https://langchain-ai.github.io/langgraph/how-tos/state-reducers/)
- [How to create a sequence of steps](https://langchain-ai.github.io/langgraph/how-tos/sequence/)
- [How to create branches for parallel execution](https://langchain-ai.github.io/langgraph/how-tos/branching/)
- [How to create and control loops with recursion limits](https://langchain-ai.github.io/langgraph/how-tos/recursion-limit/)
- [How to visualize your graph](https://langchain-ai.github.io/langgraph/how-tos/visualization/)
### Fine-grained Control
These guides demonstrate LangGraph features that grant fine-grained control over the execution of your graph.
- [How to create map-reduce branches for parallel execution](https://langchain-ai.github.io/langgraph/how-tos/map-reduce/)
- [How to update state and jump to nodes in graphs and subgraphs](https://langchain-ai.github.io/langgraph/how-tos/command/)
- [How to add runtime configuration to your graph](https://langchain-ai.github.io/langgraph/how-tos/configuration/)
- [How to add node retries](https://langchain-ai.github.io/langgraph/how-tos/node-retries/)
- [How to return state before hitting recursion limit](https://langchain-ai.github.io/langgraph/how-tos/return-when-recursion-limit-hits/)
### Persistence
Persistence makes it easy to persist state across graph runs (per-thread persistence) and across threads (cross-thread persistence).
These how-to guides show how to add persistence to your graph.
- [How to add thread-level persistence to your graph](https://langchain-ai.github.io/langgraph/how-tos/persistence/)
- [How to add thread-level persistence to a subgraph](https://langchain-ai.github.io/langgraph/how-tos/subgraph-persistence/)
- [How to add cross-thread persistence to your graph](https://langchain-ai.github.io/langgraph/how-tos/cross-thread-persistence/)
- [How to use Postgres checkpointer for persistence](https://langchain-ai.github.io/langgraph/how-tos/persistence_postgres/)
- [How to use MongoDB checkpointer for persistence](https://langchain-ai.github.io/langgraph/how-tos/persistence_mongodb/)
- [How to create a custom checkpointer using Redis](https://langchain-ai.github.io/langgraph/how-tos/persistence_redis/)
See the below guides for how-to add persistence to your workflow using the [Functional API](https://langchain-ai.github.io/langgraph/concepts/functional_api/):
- [How to add thread-level persistence (functional API)](https://langchain-ai.github.io/langgraph/how-tos/persistence-functional/)
- [How to add cross-thread persistence (functional API)](https://langchain-ai.github.io/langgraph/how-tos/cross-thread-persistence-functional/)
### Memory
LangGraph makes it easy to manage conversation memory in your graph. These how-to guides show how to implement different strategies for that.
- [How to manage conversation history](https://langchain-ai.github.io/langgraph/how-tos/memory/manage-conversation-history/)
- [How to delete messages](https://langchain-ai.github.io/langgraph/how-tos/memory/delete-messages/)
- [How to add summary conversation memory](https://langchain-ai.github.io/langgraph/how-tos/memory/add-summary-conversation-history/)
- [How to add long-term memory (cross-thread)](https://langchain-ai.github.io/langgraph/how-tos/memory/cross-thread-persistence/)
- [How to use semantic search for long-term memory](https://langchain-ai.github.io/langgraph/how-tos/memory/semantic-search/)
### Human-in-the-loop
Human-in-the-loop functionality allows you to involve humans in the decision-making process of your graph.
These how-to guides show how to implement human-in-the-loop workflows in your graph.
- [How to wait for user input](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/wait-user-input/): A basic example that shows how to implement a human-in-the-loop workflow in your graph using the `interrupt` function.
- [How to review tool calls](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/review-tool-calls/): Incorporate human-in-the-loop for reviewing/editing/accepting tool call requests before they executed using the `interrupt` function.
- [How to add static breakpoints](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/breakpoints/): Use for debugging purposes. For human-in-the-loop workflows, we recommend the [`interrupt` function](https://langchain-ai.github.io/langgraph/reference/types/#langgraph.types.interrupt) instead.
- [How to edit graph state](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/edit-graph-state/): Edit graph state using `graph.update_state` method. Use this if implementing a **human-in-the-loop** workflow via **static breakpoints**.
See the below guides for how-to implement human-in-the-loop workflows with the Functional API.
- [How to wait for user input (Functional API)](https://langchain-ai.github.io/langgraph/how-tos/wait-user-input-functional/)
- [How to review tool calls (Functional API)](https://langchain-ai.github.io/langgraph/how-tos/review-tool-calls-functional/)
### Time Travel
[Time travel](https://langchain-ai.github.io/langgraph/concepts/time-travel/) allows you to replay past actions in your LangGraph application to explore alternative paths and debug issues. These how-to guides show how to use time travel in your graph.
- [How to view and update past graph state](https://langchain-ai.github.io/langgraph/how-tos/time-travel/)
### Streaming
[Streaming](https://langchain-ai.github.io/langgraph/concepts/streaming/) is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
- [How to stream](https://langchain-ai.github.io/langgraph/how-tos/streaming/)
- [How to stream LLM tokens](https://langchain-ai.github.io/langgraph/how-tos/streaming-tokens/)
- [How to stream LLM tokens from specific nodes](https://langchain-ai.github.io/langgraph/how-tos/streaming-specific-nodes/)
- [How to stream data from within a tool](https://langchain-ai.github.io/langgraph/how-tos/streaming-events-from-within-tools/)
- [How to stream from subgraphs](https://langchain-ai.github.io/langgraph/how-tos/streaming-subgraphs/)
- [How to disable streaming for models that don't support it](https://langchain-ai.github.io/langgraph/how-tos/disable-streaming/)
### Tool calling
[Tool calling](https://python.langchain.com/docs/concepts/tool_calling/) is a type of [chat model](https://python.langchain.com/docs/concepts/chat_models/) API.
It accepts tool schemas, along with messages, as input and returns invocations of those tools as part of the output message.
These how-to guides show common patterns for tool calling with LangGraph:
- [How to call tools using ToolNode](https://langchain-ai.github.io/langgraph/how-tos/tool-calling/)
- [How to handle tool calling errors](https://langchain-ai.github.io/langgraph/how-tos/tool-calling-errors/)
- [How to pass runtime values to tools](https://langchain-ai.github.io/langgraph/how-tos/pass-run-time-values-to-tools/)
- [How to pass config to tools](https://langchain-ai.github.io/langgraph/how-tos/pass-config-to-tools/)
- [How to update graph state from tools](https://langchain-ai.github.io/langgraph/how-tos/update-state-from-tools/)
- [How to handle large numbers of tools](https://langchain-ai.github.io/langgraph/how-tos/many-tools/)
### Subgraphs
Subgraphs allow you to reuse an existing graph from another graph.
These how-to guides show how to use subgraphs:
- [How to use subgraphs](https://langchain-ai.github.io/langgraph/how-tos/subgraph/)
- [How to view and update state in subgraphs](https://langchain-ai.github.io/langgraph/how-tos/subgraphs-manage-state/)
- [How to transform inputs and outputs of a subgraph](https://langchain-ai.github.io/langgraph/how-tos/subgraph-transform-state/)
### Multi-agent
Multi-agent systems are useful to break down complex LLM applications into multiple agents, each responsible for a different part of the application.
These how-to guides show how to implement multi-agent systems in LangGraph:
- [How to implement handoffs between agents](https://langchain-ai.github.io/langgraph/how-tos/agent-handoffs/)
- [How to build a multi-agent network](https://langchain-ai.github.io/langgraph/how-tos/multi-agent-network/)
- [How to add multi-turn conversation in a multi-agent application](https://langchain-ai.github.io/langgraph/how-tos/multi-agent-multi-turn-convo/)
### State Management
- [How to use Pydantic model as graph state](https://langchain-ai.github.io/langgraph/how-tos/state-model/)
- [How to define input/output schema for your graph](https://langchain-ai.github.io/langgraph/how-tos/input_output_schema/)
- [How to pass private state between nodes inside the graph](https://langchain-ai.github.io/langgraph/how-tos/pass_private_state/)
### Other
- [How to run graph asynchronously](https://langchain-ai.github.io/langgraph/how-tos/async/)
- [How to force tool-calling agent to structure output](https://langchain-ai.github.io/langgraph/how-tos/react-agent-structured-output/)
- [How to pass custom LangSmith run ID for graph runs](https://langchain-ai.github.io/langgraph/how-tos/run-id-langsmith/)
- [How to integrate LangGraph with AutoGen, CrewAI, and other frameworks](https://langchain-ai.github.io/langgraph/how-tos/autogen-integration/)
## Use cases
Explore practical implementations tailored for specific scenarios:
### Chatbots
- [Customer Support](https://langchain-ai.github.io/langgraph/tutorials/customer-support/customer-support/): Build a multi-functional support bot for flights, hotels, and car rentals.
- [Prompt Generation from User Requirements](https://langchain-ai.github.io/langgraph/tutorials/chatbots/information-gather-prompting/): Build an information gathering chatbot.
- [Code Assistant](https://langchain-ai.github.io/langgraph/tutorials/code_assistant/langgraph_code_assistant/): Build a code analysis and generation assistant.
### RAG
- [Agentic RAG](https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_agentic_rag/): Use an agent to figure out how to retrieve the most relevant information before using the retrieved information to answer the user's question.
- [Adaptive RAG](https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_adaptive_rag/): Adaptive RAG is a strategy for RAG that unites (1) query analysis with (2) active / self-corrective RAG. Implementation of: https://arxiv.org/abs/2403.14403
- For a version that uses a local LLM: [Adaptive RAG using local LLMs](https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_adaptive_rag_local/)
- [Corrective RAG](https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_crag/): Uses an LLM to grade the quality of the retrieved information from the given source, and if the quality is low, it will try to retrieve the information from another source. Implementation of: https://arxiv.org/pdf/2401.15884.pdf
- For a version that uses a local LLM: [Corrective RAG using local LLMs](https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_crag_local/)
- [Self-RAG](https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_self_rag/): Self-RAG is a strategy for RAG that incorporates self-reflection / self-grading on retrieved documents and generations. Implementation of https://arxiv.org/abs/2310.11511.
- For a version that uses a local LLM: [Self-RAG using local LLMs](https://langchain-ai.github.io/langgraph/tutorials/rag/langgraph_self_rag_local/)
- [SQL Agent](https://langchain-ai.github.io/langgraph/tutorials/sql-agent/): Build a SQL agent that can answer questions about a SQL database.
### Multi-Agent Systems
- [Network](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/multi-agent-collaboration/): Enable two or more agents to collaborate on a task
- [Supervisor](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/agent_supervisor/): Use an LLM to orchestrate and delegate to individual agents
- [Hierarchical Teams](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/hierarchical_agent_teams/): Orchestrate nested teams of agents to solve problems
@@ -39,6 +39,7 @@ from langgraph.store.base import (
Result,
SearchItem,
SearchOp,
TTLConfig,
ensure_embeddings,
get_text_at_path,
tokenize_path,
@@ -622,6 +623,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]
] = None,
index: Optional[PostgresIndexConfig] = None,
ttl: Optional[TTLConfig] = None,
) -> None:
super().__init__()
self._deserializer = deserializer
@@ -634,6 +636,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
self.embeddings, self.index_config = _ensure_index_config(self.index_config)
else:
self.embeddings = None
self.ttl_config = ttl
@classmethod
@contextmanager
@@ -530,6 +530,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
for idx, (channel, value) in enumerate(writes)
],
)
await self.conn.commit()
def get_next_version(self, current: Optional[str], channel: ChannelProtocol) -> str:
"""Generate the next version ID for a channel.
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-sqlite"
version = "2.0.5"
version = "2.0.6"
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
+143 -19
View File
@@ -11,9 +11,19 @@ Core types:
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Any, Iterable, Literal, NamedTuple, Optional, TypedDict, Union, cast
from typing import (
Any,
Iterable,
Literal,
NamedTuple,
Optional,
TypedDict,
Union,
cast,
)
from langchain_core.embeddings import Embeddings
from typing_extensions import override
from langgraph.store.base.embed import (
AEmbeddingsFunc,
@@ -24,6 +34,20 @@ from langgraph.store.base.embed import (
)
class NotProvided:
"""Sentinel singleton."""
def __bool__(self) -> Literal[False]:
return False
@override
def __repr__(self) -> str:
return "NOT_GIVEN"
NOT_PROVIDED = NotProvided()
class Item:
"""Represents a stored item with metadata.
@@ -59,7 +83,7 @@ class Item:
else created_at
)
self.updated_at = (
datetime.fromisoformat(cast(str, created_at))
datetime.fromisoformat(cast(str, updated_at))
if isinstance(updated_at, str)
else updated_at
)
@@ -496,6 +520,25 @@ class InvalidNamespaceError(ValueError):
"""Provided namespace is invalid."""
class TTLConfig(TypedDict, total=False):
"""Configuration for TTL (time-to-live) behavior in the store."""
refresh_on_read: bool
"""Default behavior for refreshing TTLs on read operations (GET and SEARCH).
If True, TTLs will be refreshed on read operations (get/search) by default.
This can be overridden per-operation by explicitly setting refresh_ttl.
Defaults to True if not configured.
"""
default_ttl: Optional[float]
"""Default TTL (time-to-live) in minutes for new items.
If provided, new items will expire after this many minutes after their last access.
The expiration timer refreshes on both read and write operations.
Defaults to None (no expiration).
"""
class IndexConfig(TypedDict, total=False):
"""Configuration for indexing documents for semantic search in the store.
@@ -640,7 +683,8 @@ class BaseStore(ABC):
Subclasses must explicitly set `supports_ttl = True` to enable this feature.
"""
supports_ttl = False
supports_ttl: bool = False
ttl_config: Optional[TTLConfig] = None
__slots__ = ("__weakref__",)
@@ -669,7 +713,11 @@ class BaseStore(ABC):
"""
def get(
self, namespace: tuple[str, ...], key: str, *, refresh_ttl: bool = True
self,
namespace: tuple[str, ...],
key: str,
*,
refresh_ttl: Optional[bool] = None,
) -> Optional[Item]:
"""Retrieve a single item.
@@ -677,12 +725,15 @@ class BaseStore(ABC):
namespace: Hierarchical path for the item.
key: Unique identifier within the namespace.
refresh_ttl: Whether to refresh TTLs for the returned item.
If None (default), uses the store's default refresh_ttl setting.
If no TTL is specified, this argument is ignored.
Returns:
The retrieved item or None if not found.
"""
return self.batch([GetOp(namespace, str(key), refresh_ttl)])[0]
return self.batch(
[GetOp(namespace, str(key), _ensure_refresh(self.ttl_config, refresh_ttl))]
)[0]
def search(
self,
@@ -693,7 +744,7 @@ class BaseStore(ABC):
filter: Optional[dict[str, Any]] = None,
limit: int = 10,
offset: int = 0,
refresh_ttl: bool = True,
refresh_ttl: Optional[bool] = None,
) -> list[SearchItem]:
"""Search for items within a namespace prefix.
@@ -743,7 +794,16 @@ class BaseStore(ABC):
and requires proper embedding configuration.
"""
return self.batch(
[SearchOp(namespace_prefix, filter, limit, offset, query, refresh_ttl)]
[
SearchOp(
namespace_prefix,
filter,
limit,
offset,
query,
_ensure_refresh(self.ttl_config, refresh_ttl),
)
]
)[0]
def put(
@@ -753,7 +813,7 @@ class BaseStore(ABC):
value: dict[str, Any],
index: Optional[Union[Literal[False], list[str]]] = None,
*,
ttl: Optional[float] = None,
ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED,
) -> None:
"""Store or update an item in the store.
@@ -806,12 +866,22 @@ class BaseStore(ABC):
```
"""
_validate_namespace(namespace)
if ttl is not None and not self.supports_ttl:
if ttl not in (NOT_PROVIDED, None) and not self.supports_ttl:
raise NotImplementedError(
f"TTL is not supported by {self.__class__.__name__}. "
f"Use a store implementation that supports TTL or set ttl=None."
)
self.batch([PutOp(namespace, str(key), value, index=index, ttl=ttl)])
self.batch(
[
PutOp(
namespace,
str(key),
value,
index=index,
ttl=_ensure_ttl(self.ttl_config, ttl),
)
]
)
def delete(self, namespace: tuple[str, ...], key: str) -> None:
"""Delete an item.
@@ -876,7 +946,11 @@ class BaseStore(ABC):
return self.batch([op])[0]
async def aget(
self, namespace: tuple[str, ...], key: str, *, refresh_ttl: bool = True
self,
namespace: tuple[str, ...],
key: str,
*,
refresh_ttl: Optional[bool] = None,
) -> Optional[Item]:
"""Asynchronously retrieve a single item.
@@ -887,7 +961,17 @@ class BaseStore(ABC):
Returns:
The retrieved item or None if not found.
"""
return (await self.abatch([GetOp(namespace, str(key), refresh_ttl)]))[0]
return (
await self.abatch(
[
GetOp(
namespace,
str(key),
_ensure_refresh(self.ttl_config, refresh_ttl),
)
]
)
)[0]
async def asearch(
self,
@@ -898,7 +982,7 @@ class BaseStore(ABC):
filter: Optional[dict[str, Any]] = None,
limit: int = 10,
offset: int = 0,
refresh_ttl: bool = True,
refresh_ttl: Optional[bool] = None,
) -> list[SearchItem]:
"""Asynchronously search for items within a namespace prefix.
@@ -909,8 +993,8 @@ class BaseStore(ABC):
limit: Maximum number of items to return.
offset: Number of items to skip before returning results.
refresh_ttl: Whether to refresh TTLs for the returned items.
Defaults to True. If no TTL is specified, this argument
is ignored.
If None (default), uses the store's TTLConfig.refresh_default setting.
If TTLConfig is not provided or no TTL is specified, this argument is ignored.
Returns:
List of items matching the search criteria.
@@ -950,7 +1034,16 @@ class BaseStore(ABC):
"""
return (
await self.abatch(
[SearchOp(namespace_prefix, filter, limit, offset, query, refresh_ttl)]
[
SearchOp(
namespace_prefix,
filter,
limit,
offset,
query,
_ensure_refresh(self.ttl_config, refresh_ttl),
)
]
)
)[0]
@@ -961,7 +1054,7 @@ class BaseStore(ABC):
value: dict[str, Any],
index: Optional[Union[Literal[False], list[str]]] = None,
*,
ttl: Optional[float] = None,
ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED,
) -> None:
"""Asynchronously store or update an item in the store.
@@ -1022,12 +1115,22 @@ class BaseStore(ABC):
```
"""
_validate_namespace(namespace)
if ttl is not None and not self.supports_ttl:
if ttl not in (NOT_PROVIDED, None) and not self.supports_ttl:
raise NotImplementedError(
f"TTL is not supported by {self.__class__.__name__}. "
f"Use a store implementation that supports TTL or set ttl=None."
)
await self.abatch([PutOp(namespace, str(key), value, index=index, ttl=ttl)])
await self.abatch(
[
PutOp(
namespace,
str(key),
value,
index=index,
ttl=_ensure_ttl(self.ttl_config, ttl),
)
]
)
async def adelete(self, namespace: tuple[str, ...], key: str) -> None:
"""Asynchronously delete an item.
@@ -1116,6 +1219,27 @@ def _validate_namespace(namespace: tuple[str, ...]) -> None:
)
def _ensure_refresh(
ttl_config: Optional[TTLConfig], refresh_ttl: Optional[bool] = None
) -> bool:
if refresh_ttl is not None:
return refresh_ttl
if ttl_config is not None:
return ttl_config.get("refresh_on_read", True)
return True
def _ensure_ttl(
ttl_config: Optional[TTLConfig],
ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED,
) -> Optional[float]:
if ttl is NOT_PROVIDED:
if ttl_config:
return ttl_config.get("default_ttl")
return None
return ttl
__all__ = [
"BaseStore",
"Item",
+45 -10
View File
@@ -5,17 +5,21 @@ from collections.abc import Iterable
from typing import Any, Callable, Literal, Optional, TypeVar, Union
from langgraph.store.base import (
NOT_PROVIDED,
BaseStore,
GetOp,
Item,
ListNamespacesOp,
MatchCondition,
NamespacePath,
NotProvided,
Op,
PutOp,
Result,
SearchItem,
SearchOp,
_ensure_refresh,
_ensure_ttl,
_validate_namespace,
)
@@ -65,11 +69,24 @@ class AsyncBatchedBaseStore(BaseStore):
pass
async def aget(
self, namespace: tuple[str, ...], key: str, *, refresh_ttl: bool = True
self,
namespace: tuple[str, ...],
key: str,
*,
refresh_ttl: Optional[bool] = None,
) -> Optional[Item]:
assert not self._task.done()
fut = self._loop.create_future()
self._aqueue.put_nowait((fut, GetOp(namespace, key, refresh_ttl=refresh_ttl)))
self._aqueue.put_nowait(
(
fut,
GetOp(
namespace,
key,
refresh_ttl=_ensure_refresh(self.ttl_config, refresh_ttl),
),
)
)
return await fut
async def asearch(
@@ -81,7 +98,7 @@ class AsyncBatchedBaseStore(BaseStore):
filter: Optional[dict[str, Any]] = None,
limit: int = 10,
offset: int = 0,
refresh_ttl: bool = True,
refresh_ttl: Optional[bool] = None,
) -> list[SearchItem]:
assert not self._task.done()
fut = self._loop.create_future()
@@ -94,7 +111,7 @@ class AsyncBatchedBaseStore(BaseStore):
limit,
offset,
query,
refresh_ttl=refresh_ttl,
refresh_ttl=_ensure_refresh(self.ttl_config, refresh_ttl),
),
)
)
@@ -107,12 +124,19 @@ class AsyncBatchedBaseStore(BaseStore):
value: dict[str, Any],
index: Optional[Union[Literal[False], list[str]]] = None,
*,
ttl: Optional[float] = None,
ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED,
) -> None:
assert not self._task.done()
_validate_namespace(namespace)
fut = self._loop.create_future()
self._aqueue.put_nowait((fut, PutOp(namespace, key, value, index, ttl=ttl)))
self._aqueue.put_nowait(
(
fut,
PutOp(
namespace, key, value, index, ttl=_ensure_ttl(self.ttl_config, ttl)
),
)
)
return await fut
async def adelete(
@@ -157,7 +181,11 @@ class AsyncBatchedBaseStore(BaseStore):
@_check_loop
def get(
self, namespace: tuple[str, ...], key: str, *, refresh_ttl: bool = True
self,
namespace: tuple[str, ...],
key: str,
*,
refresh_ttl: Optional[bool] = None,
) -> Optional[Item]:
return asyncio.run_coroutine_threadsafe(
self.aget(namespace, key=key, refresh_ttl=refresh_ttl), self._loop
@@ -173,7 +201,7 @@ class AsyncBatchedBaseStore(BaseStore):
filter: Optional[dict[str, Any]] = None,
limit: int = 10,
offset: int = 0,
refresh_ttl: bool = True,
refresh_ttl: Optional[bool] = None,
) -> list[SearchItem]:
return asyncio.run_coroutine_threadsafe(
self.asearch(
@@ -195,11 +223,18 @@ class AsyncBatchedBaseStore(BaseStore):
value: dict[str, Any],
index: Optional[Union[Literal[False], list[str]]] = None,
*,
ttl: Optional[float] = None,
ttl: Union[Optional[float], "NotProvided"] = NOT_PROVIDED,
) -> None:
_validate_namespace(namespace)
asyncio.run_coroutine_threadsafe(
self.aput(namespace, key=key, value=value, index=index, ttl=ttl), self._loop
self.aput(
namespace,
key=key,
value=value,
index=index,
ttl=_ensure_ttl(self.ttl_config, ttl),
),
self._loop,
).result()
@_check_loop
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint"
version = "2.0.18"
version = "2.0.19"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
license = "MIT"
+1 -1
View File
@@ -130,7 +130,7 @@ def test_serde_jsonplus() -> None:
key="my-key",
namespace=("a", "name", " "),
created_at=datetime(2024, 9, 24, 17, 29, 10, 128397),
updated_at=datetime(2024, 9, 24, 17, 29, 10, 128397),
updated_at=datetime(2024, 9, 24, 17, 29, 11, 128397),
),
}
+25
View File
@@ -11,6 +11,24 @@ MIN_NODE_VERSION = "20"
MIN_PYTHON_VERSION = "3.11"
class TTLConfig(TypedDict, total=False):
"""Configuration for TTL (time-to-live) behavior in the store."""
refresh_on_read: bool
"""Default behavior for refreshing TTLs on read operations (GET and SEARCH).
If True, TTLs will be refreshed on read operations (get/search) by default.
This can be overridden per-operation by explicitly setting refresh_ttl.
Defaults to True if not configured.
"""
default_ttl: Optional[float]
"""Optional. Default TTL (time-to-live) in minutes for new items.
If provided, all new items will have this TTL unless explicitly overridden.
If omitted, items will have no TTL by default.
"""
class IndexConfig(TypedDict, total=False):
"""Configuration for indexing documents for semantic search in the store.
@@ -79,6 +97,13 @@ class StoreConfig(TypedDict, total=False):
If omitted, no vector index is initialized.
"""
ttl: Optional[TTLConfig]
"""Optional. Defines the TTL (time-to-live) behavior configuration.
If provided, the store will apply TTL settings according to the configuration.
If omitted, no TTL behavior is configured.
"""
class SecurityConfig(TypedDict, total=False):
"""Configuration for OpenAPI security definitions and requirements.
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-cli"
version = "0.1.75"
version = "0.1.76"
description = "CLI for interacting with LangGraph API"
authors = []
license = "MIT"
+32
View File
@@ -397,6 +397,17 @@
}
],
"description": "Optional. Defines the vector-based semantic search configuration.\n\n- Generate embeddings according to `index.embed`\n- Enforce the embedding dimension given by `index.dims`\n- Embed only specified JSON fields (if any) from `index.fields`\n\nIf omitted, no vector index is initialized.\n"
},
"ttl": {
"anyOf": [
{
"$ref": "#/$defs/TTLConfig"
},
{
"type": "null"
}
],
"description": "Optional. Defines the TTL (time-to-live) behavior configuration.\n\nIf provided, the store will apply TTL settings according to the configuration.\nIf omitted, no TTL behavior is configured.\n"
}
},
"required": []
@@ -430,6 +441,27 @@
}
},
"required": []
},
"TTLConfig": {
"title": "TTLConfig",
"description": "Configuration for TTL (time-to-live) behavior in the store.",
"type": "object",
"properties": {
"default_ttl": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"refresh_on_read": {
"type": "boolean"
}
},
"required": []
}
},
"title": "LangGraph CLI Configuration",
+32
View File
@@ -397,6 +397,17 @@
}
],
"description": "Optional. Defines the vector-based semantic search configuration.\n\n- Generate embeddings according to `index.embed`\n- Enforce the embedding dimension given by `index.dims`\n- Embed only specified JSON fields (if any) from `index.fields`\n\nIf omitted, no vector index is initialized.\n"
},
"ttl": {
"anyOf": [
{
"$ref": "#/$defs/TTLConfig"
},
{
"type": "null"
}
],
"description": "Optional. Defines the TTL (time-to-live) behavior configuration.\n\nIf provided, the store will apply TTL settings according to the configuration.\nIf omitted, no TTL behavior is configured.\n"
}
},
"required": []
@@ -430,6 +441,27 @@
}
},
"required": []
},
"TTLConfig": {
"title": "TTLConfig",
"description": "Configuration for TTL (time-to-live) behavior in the store.",
"type": "object",
"properties": {
"default_ttl": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
]
},
"refresh_on_read": {
"type": "boolean"
}
},
"required": []
}
},
"title": "LangGraph CLI Configuration",
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦜🕸️LangGraph
![Version](https://img.shields.io/pypi/v/langgraph)
[![Version](https://img.shields.io/pypi/v/langgraph.svg)](https://pypi.org/project/langgraph/)
[![Downloads](https://static.pepy.tech/badge/langgraph/month)](https://pepy.tech/project/langgraph)
[![Open Issues](https://img.shields.io/github/issues-raw/langchain-ai/langgraph)](https://github.com/langchain-ai/langgraph/issues)
[![Docs](https://img.shields.io/badge/docs-latest-blue)](https://langchain-ai.github.io/langgraph/)
+215
View File
@@ -0,0 +1,215 @@
import asyncio
from inspect import (
isfunction,
ismethod,
signature,
)
from types import FunctionType
from typing import (
Any,
Awaitable,
Callable,
Hashable,
Literal,
NamedTuple,
Optional,
Sequence,
Type,
Union,
cast,
get_args,
get_origin,
get_type_hints,
)
from langchain_core.runnables import (
Runnable,
RunnableConfig,
RunnableLambda,
)
from langgraph.constants import END, START
from langgraph.errors import InvalidUpdateError
from langgraph.pregel.write import ChannelWrite
from langgraph.types import Send
from langgraph.utils.runnable import (
RunnableCallable,
)
def _get_branch_path_input_schema(
path: Union[
Callable[..., Union[Hashable, list[Hashable]]],
Callable[..., Awaitable[Union[Hashable, list[Hashable]]]],
Runnable[Any, Union[Hashable, list[Hashable]]],
],
) -> Optional[Type[Any]]:
input = None
# detect input schema annotation in the branch callable
try:
callable_: Optional[
Union[
Callable[..., Union[Hashable, list[Hashable]]],
Callable[..., Awaitable[Union[Hashable, list[Hashable]]]],
]
] = None
if isinstance(path, (RunnableCallable, RunnableLambda)):
if isfunction(path.func) or ismethod(path.func):
callable_ = path.func
elif (callable_method := getattr(path.func, "__call__", None)) and ismethod(
callable_method
):
callable_ = callable_method
elif isfunction(path.afunc) or ismethod(path.afunc):
callable_ = path.afunc
elif (
callable_method := getattr(path.afunc, "__call__", None)
) and ismethod(callable_method):
callable_ = callable_method
elif callable(path):
callable_ = path
if callable_ is not None and (hints := get_type_hints(callable_)):
first_parameter_name = next(
iter(signature(cast(FunctionType, callable_)).parameters.keys())
)
if input_hint := hints.get(first_parameter_name):
if isinstance(input_hint, type) and get_type_hints(input_hint):
input = input_hint
except (TypeError, StopIteration):
pass
return input
class Branch(NamedTuple):
path: Runnable[Any, Union[Hashable, list[Hashable]]]
ends: Optional[dict[Hashable, str]]
then: Optional[str] = None
input_schema: Optional[Type[Any]] = None
@classmethod
def from_path(
cls,
path: Runnable[Any, Union[Hashable, list[Hashable]]],
path_map: Optional[Union[dict[Hashable, str], list[str]]],
then: Optional[str] = None,
infer_schema: bool = False,
) -> "Branch":
# coerce path_map to a dictionary
path_map_: Optional[dict[Hashable, str]] = None
try:
if isinstance(path_map, dict):
path_map_ = path_map.copy()
elif isinstance(path_map, list):
path_map_ = {name: name for name in path_map}
else:
# find func
func: Optional[Callable] = None
if isinstance(path, (RunnableCallable, RunnableLambda)):
func = path.func or path.afunc
if func is not None:
# find callable method
if (cal := getattr(path, "__call__", None)) and ismethod(cal):
func = cal
# get the return type
if rtn_type := get_type_hints(func).get("return"):
if get_origin(rtn_type) is Literal:
path_map_ = {name: name for name in get_args(rtn_type)}
except Exception:
pass
# infer input schema
input_schema = _get_branch_path_input_schema(path) if infer_schema else None
# create branch
return cls(path=path, ends=path_map_, then=then, input_schema=input_schema)
def run(
self,
writer: Callable[
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
],
reader: Optional[Callable[[RunnableConfig], Any]] = None,
) -> RunnableCallable:
return ChannelWrite.register_writer(
RunnableCallable(
func=self._route,
afunc=self._aroute,
writer=writer,
reader=reader,
name=None,
trace=False,
)
)
def _route(
self,
input: Any,
config: RunnableConfig,
*,
reader: Optional[Callable[[RunnableConfig], Any]],
writer: Callable[
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
],
) -> Runnable:
if reader:
value = reader(config)
# passthrough additional keys from node to branch
# only doable when using dict states
if (
isinstance(value, dict)
and isinstance(input, dict)
and self.input_schema is None
):
value = {**input, **value}
else:
value = input
result = self.path.invoke(value, config)
return self._finish(writer, input, result, config)
async def _aroute(
self,
input: Any,
config: RunnableConfig,
*,
reader: Optional[Callable[[RunnableConfig], Any]],
writer: Callable[
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
],
) -> Runnable:
if reader:
value = await asyncio.to_thread(reader, config)
# passthrough additional keys from node to branch
# only doable when using dict states
if (
isinstance(value, dict)
and isinstance(input, dict)
and self.input_schema is None
):
value = {**input, **value}
else:
value = input
result = await self.path.ainvoke(value, config)
return self._finish(writer, input, result, config)
def _finish(
self,
writer: Callable[
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
],
input: Any,
result: Any,
config: RunnableConfig,
) -> Union[Runnable, Any]:
if not isinstance(result, (list, tuple)):
result = [result]
if self.ends:
destinations: Sequence[Union[Send, str]] = [
r if isinstance(r, Send) else self.ends[r] for r in result
]
else:
destinations = cast(Sequence[Union[Send, str]], result)
if any(dest is None or dest == START for dest in destinations):
raise ValueError("Branch did not return a valid destination")
if any(p.node == END for p in destinations if isinstance(p, Send)):
raise InvalidUpdateError("Cannot send a packet to the END node")
return writer(destinations, config) or input
+5 -117
View File
@@ -1,4 +1,3 @@
import asyncio
import logging
from collections import defaultdict
from typing import (
@@ -6,15 +5,11 @@ from typing import (
Awaitable,
Callable,
Hashable,
Literal,
NamedTuple,
Optional,
Sequence,
Union,
cast,
get_args,
get_origin,
get_type_hints,
overload,
)
@@ -34,12 +29,12 @@ from langgraph.constants import (
TAG_HIDDEN,
Send,
)
from langgraph.errors import InvalidUpdateError
from langgraph.graph.branch import Branch
from langgraph.pregel import Channel, Pregel
from langgraph.pregel.read import PregelNode
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
from langgraph.types import All, Checkpointer
from langgraph.utils.runnable import RunnableCallable, RunnableLike, coerce_to_runnable
from langgraph.utils.runnable import RunnableLike, coerce_to_runnable
logger = logging.getLogger(__name__)
@@ -50,95 +45,6 @@ class NodeSpec(NamedTuple):
ends: Optional[Union[tuple[str, ...], dict[str, str]]] = EMPTY_SEQ
class Branch(NamedTuple):
path: Runnable[Any, Union[Hashable, list[Hashable]]]
ends: Optional[dict[Hashable, str]]
then: Optional[str] = None
def run(
self,
writer: Callable[
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
],
reader: Optional[Callable[[RunnableConfig], Any]] = None,
) -> RunnableCallable:
return ChannelWrite.register_writer(
RunnableCallable(
func=self._route,
afunc=self._aroute,
writer=writer,
reader=reader,
name=None,
trace=False,
)
)
def _route(
self,
input: Any,
config: RunnableConfig,
*,
reader: Optional[Callable[[RunnableConfig], Any]],
writer: Callable[
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
],
) -> Runnable:
if reader:
value = reader(config)
# passthrough additional keys from node to branch
# only doable when using dict states
if isinstance(value, dict) and isinstance(input, dict):
value = {**input, **value}
else:
value = input
result = self.path.invoke(value, config)
return self._finish(writer, input, result, config)
async def _aroute(
self,
input: Any,
config: RunnableConfig,
*,
reader: Optional[Callable[[RunnableConfig], Any]],
writer: Callable[
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
],
) -> Runnable:
if reader:
value = await asyncio.to_thread(reader, config)
# passthrough additional keys from node to branch
# only doable when using dict states
if isinstance(value, dict) and isinstance(input, dict):
value = {**input, **value}
else:
value = input
result = await self.path.ainvoke(value, config)
return self._finish(writer, input, result, config)
def _finish(
self,
writer: Callable[
[Sequence[Union[str, Send]], RunnableConfig], Optional[ChannelWrite]
],
input: Any,
result: Any,
config: RunnableConfig,
) -> Union[Runnable, Any]:
if not isinstance(result, (list, tuple)):
result = [result]
if self.ends:
destinations: Sequence[Union[Send, str]] = [
r if isinstance(r, Send) else self.ends[r] for r in result
]
else:
destinations = cast(Sequence[Union[Send, str]], result)
if any(dest is None or dest == START for dest in destinations):
raise ValueError("Branch did not return a valid destination")
if any(p.node == END for p in destinations if isinstance(p, Send)):
raise InvalidUpdateError("Cannot send a packet to the END node")
return writer(destinations, config) or input
class Graph:
def __init__(self) -> None:
self.nodes: dict[str, NodeSpec] = {}
@@ -267,25 +173,7 @@ class Graph:
"Adding an edge to a graph that has already been compiled. This will "
"not be reflected in the compiled graph."
)
# coerce path_map to a dictionary
try:
if isinstance(path_map, dict):
path_map_ = path_map.copy()
elif isinstance(path_map, list):
path_map_ = {name: name for name in path_map}
elif isinstance(path, Runnable):
path_map_ = None
elif rtn_type := get_type_hints(path.__call__).get( # type: ignore[operator]
"return"
) or get_type_hints(path).get("return"):
if get_origin(rtn_type) is Literal:
path_map_ = {name: name for name in get_args(rtn_type)}
else:
path_map_ = None
else:
path_map_ = None
except Exception:
path_map_ = None
# find a name for the condition
path = coerce_to_runnable(path, name=None, trace=True)
name = path.name or "condition"
@@ -295,7 +183,7 @@ class Graph:
f"Branch with name `{path.name}` already exists for node " f"`{source}`"
)
# save it
self.branches[source][name] = Branch(path, path_map_, then)
self.branches[source][name] = Branch.from_path(path, path_map, then, False)
return self
def set_entry_point(self, key: str) -> Self:
@@ -584,7 +472,7 @@ class CompiledGraph(Pregel):
)
subgraph.trim_first_node()
subgraph.trim_last_node()
if len(subgraph.nodes) > 1:
if len(subgraph.nodes) >= 1:
e, s = graph.extend(subgraph, prefix=key)
if e is None:
raise ValueError(
+94 -13
View File
@@ -7,7 +7,9 @@ from inspect import isclass, isfunction, ismethod, signature
from types import FunctionType
from typing import (
Any,
Awaitable,
Callable,
Hashable,
Literal,
NamedTuple,
Optional,
@@ -40,7 +42,14 @@ from langgraph.errors import (
ParentCommand,
create_error_message,
)
from langgraph.graph.graph import END, START, Branch, CompiledGraph, Graph, Send
from langgraph.graph.branch import Branch
from langgraph.graph.graph import (
END,
START,
CompiledGraph,
Graph,
Send,
)
from langgraph.managed.base import (
ChannelKeyPlaceholder,
ChannelTypePlaceholder,
@@ -461,6 +470,57 @@ class StateGraph(Graph):
self.waiting_edges.add((tuple(start_key), end_key))
return self
def add_conditional_edges(
self,
source: str,
path: Union[
Callable[..., Union[Hashable, list[Hashable]]],
Callable[..., Awaitable[Union[Hashable, list[Hashable]]]],
Runnable[Any, Union[Hashable, list[Hashable]]],
],
path_map: Optional[Union[dict[Hashable, str], list[str]]] = None,
then: Optional[str] = None,
) -> Self:
"""Add a conditional edge from the starting node to any number of destination nodes.
Args:
source (str): The starting node. This conditional edge will run when
exiting this node.
path (Union[Callable, Runnable]): The callable that determines the next
node or nodes. If not specifying `path_map` it should return one or
more nodes. If it returns END, the graph will stop execution.
path_map (Optional[dict[Hashable, str]]): Optional mapping of paths to node
names. If omitted the paths returned by `path` should be node names.
then (Optional[str]): The name of a node to execute after the nodes
selected by `path`.
Returns:
Self: The instance of the graph, allowing for method chaining.
Note: Without typehints on the `path` function's return value (e.g., `-> Literal["foo", "__end__"]:`)
or a path_map, the graph visualization assumes the edge could transition to any node in the graph.
""" # noqa: E501
if self.compiled:
logger.warning(
"Adding an edge to a graph that has already been compiled. This will "
"not be reflected in the compiled graph."
)
# find a name for the condition
path = coerce_to_runnable(path, name=None, trace=True)
name = path.name or "condition"
# validate the condition
if name in self.branches[source]:
raise ValueError(
f"Branch with name `{path.name}` already exists for node " f"`{source}`"
)
# save it
self.branches[source][name] = Branch.from_path(path, path_map, then, True)
if schema := self.branches[source][name].input_schema:
self._add_schema(schema)
return self
def add_sequence(
self,
nodes: Sequence[Union[RunnableLike, tuple[str, RunnableLike]]],
@@ -566,6 +626,11 @@ class StateGraph(Graph):
compiled = CompiledStateGraph(
builder=self,
config_type=self.config_schema,
input_model=self.input
if len(self.channels) > 1
and isclass(self.input)
and issubclass(self.input, (BaseModel, BaseModelV1))
else None,
nodes={},
channels={
**self.channels,
@@ -752,11 +817,7 @@ class CompiledStateGraph(CompiledGraph):
# read state keys and managed values
channels=(list(input_values) if is_single_input else input_values),
# coerce state dict to schema class (eg. pydantic model)
mapper=(
None
if is_single_input or issubclass(input_schema, dict)
else partial(_coerce_state, input_schema)
),
mapper=_pick_mapper(list(input_values), input_schema),
writers=[
# publish to this channel and state keys
ChannelWrite(
@@ -826,12 +887,12 @@ class CompiledStateGraph(CompiledGraph):
config, cast(Sequence[Union[Send, ChannelWriteEntry]], writes)
)
# attach branch publisher
schema = (
schema = branch.input_schema or (
self.builder.nodes[start].input
if start in self.builder.nodes
else self.builder.schema
)
# attach branch publisher
self.nodes[start] |= branch.run(
branch_writer,
_get_state_reader(self.builder, schema) if with_reader else None,
@@ -871,14 +932,34 @@ def _get_state_reader(
select=select[0] if select == ["__root__"] else select,
fresh=True,
# coerce state dict to schema class (eg. pydantic model)
mapper=(
None
if state_keys == ["__root__"] or issubclass(schema, dict)
else partial(_coerce_state, schema)
),
mapper=_pick_mapper(state_keys, schema),
)
def _pick_mapper(
state_keys: Sequence[str], schema: Type[Any]
) -> Optional[Callable[[Any], Any]]:
if state_keys == ["__root__"]:
return None
if issubclass(schema, dict):
return None
if issubclass(schema, BaseModel):
return partial(_coerce_state_pydantic, schema)
if issubclass(schema, BaseModelV1):
return partial(_coerce_state_pydantic_v1, schema)
return partial(_coerce_state, schema)
def _coerce_state_pydantic(schema: Type[Any], input: dict[str, Any]) -> dict[str, Any]:
return schema.model_construct(**input)
def _coerce_state_pydantic_v1(
schema: Type[Any], input: dict[str, Any]
) -> dict[str, Any]:
return schema.construct(**input)
def _coerce_state(schema: Type[Any], input: dict[str, Any]) -> dict[str, Any]:
return schema(**input)
@@ -496,6 +496,8 @@ class Pregel(PregelProtocol):
config_type: Optional[Type[Any]] = None
input_model: Optional[Type[BaseModel]] = None
config: Optional[RunnableConfig] = None
name: str = "LangGraph"
@@ -519,6 +521,7 @@ class Pregel(PregelProtocol):
store: Optional[BaseStore] = None,
retry_policy: Optional[RetryPolicy] = None,
config_type: Optional[Type[Any]] = None,
input_model: Optional[Type[BaseModel]] = None,
config: Optional[RunnableConfig] = None,
name: str = "LangGraph",
) -> None:
@@ -537,6 +540,7 @@ class Pregel(PregelProtocol):
self.store = store
self.retry_policy = retry_policy
self.config_type = config_type
self.input_model = input_model
self.config = config
self.name = name
if auto_validate:
@@ -650,6 +654,8 @@ class Pregel(PregelProtocol):
def get_input_schema(
self, config: Optional[RunnableConfig] = None
) -> Type[BaseModel]:
if self.input_model is not None:
return self.input_model
config = merge_configs(self.config, config)
if isinstance(self.input_channels, str):
return super().get_input_schema(config)
@@ -1967,6 +1973,7 @@ class Pregel(PregelProtocol):
)
with SyncPregelLoop(
input,
input_model=self.input_model,
stream=StreamProtocol(stream.put, stream_modes),
config=config,
store=store,
@@ -2257,6 +2264,7 @@ class Pregel(PregelProtocol):
)
async with AsyncPregelLoop(
input,
input_model=self.input_model,
stream=StreamProtocol(stream.put_nowait, stream_modes),
config=config,
store=store,
+30 -3
View File
@@ -23,6 +23,7 @@ from typing import (
from langchain_core.callbacks import AsyncParentRunManager, ParentRunManager
from langchain_core.runnables import RunnableConfig
from pydantic import BaseModel
from typing_extensions import ParamSpec, Self
from langgraph.channels.base import BaseChannel
@@ -125,6 +126,7 @@ P = ParamSpec("P")
INPUT_DONE = object()
INPUT_RESUMING = object()
INPUT_SHOULD_VALIDATE = object()
SPECIAL_CHANNELS = (ERROR, INTERRUPT, SCHEDULED)
@@ -139,6 +141,7 @@ def DuplexStream(*streams: StreamProtocol) -> StreamProtocol:
class PregelLoop(LoopProtocol):
input: Optional[Any]
input_model: Optional[Type[BaseModel]]
checkpointer: Optional[BaseCheckpointSaver]
nodes: Mapping[str, PregelNode]
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]]
@@ -202,6 +205,7 @@ class PregelLoop(LoopProtocol):
interrupt_after: Union[All, Sequence[str]] = EMPTY_SEQ,
interrupt_before: Union[All, Sequence[str]] = EMPTY_SEQ,
manager: Union[None, AsyncParentRunManager, ParentRunManager] = None,
input_model: Optional[Type[BaseModel]] = None,
debug: bool = False,
) -> None:
super().__init__(
@@ -212,6 +216,7 @@ class PregelLoop(LoopProtocol):
store=store,
)
self.input = input
self.input_model = input_model
self.checkpointer = checkpointer
self.nodes = nodes
self.specs = specs
@@ -395,7 +400,7 @@ class PregelLoop(LoopProtocol):
if self.status != "pending":
raise RuntimeError("Cannot tick when status is no longer 'pending'")
if self.input not in (INPUT_DONE, INPUT_RESUMING):
if self.input not in (INPUT_DONE, INPUT_RESUMING, INPUT_SHOULD_VALIDATE):
self._first(input_keys=input_keys)
elif self.to_interrupt:
# if we need to interrupt, do so
@@ -425,6 +430,13 @@ class PregelLoop(LoopProtocol):
# apply writes to managed values
for key, values in mv_writes.items():
self._update_mv(key, values)
# validate input if requested
if self.input is INPUT_SHOULD_VALIDATE:
self.input = INPUT_DONE
# validate
cast(Type[BaseModel], self.input_model)(
**read_channels(self.channels, self.stream_keys)
)
# produce values output
self._emit(
"values", map_output_values, self.output_keys, writes, self.channels
@@ -622,6 +634,8 @@ class PregelLoop(LoopProtocol):
self._emit(
"values", map_output_values, self.output_keys, True, self.channels
)
# set flag
self.input = INPUT_RESUMING
# map inputs to channel updates
elif input_writes := deque(map_input(input_keys, self.input)):
# TODO shouldn't these writes be passed to put_writes too?
@@ -662,10 +676,19 @@ class PregelLoop(LoopProtocol):
assert not mv_writes, "Can't write to SharedValues in graph input"
# save input checkpoint
self._put_checkpoint({"source": "input", "writes": dict(input_writes)})
# set flag
if (
self.input_model is not None
and not isinstance(self.input, self.input_model)
and not isinstance(self.stream_keys, str)
):
self.input = INPUT_SHOULD_VALIDATE
else:
self.input = INPUT_DONE
elif CONFIG_KEY_RESUMING not in configurable:
raise EmptyInputError(f"Received no input for {input_keys}")
# done with input
self.input = INPUT_RESUMING if is_resuming else INPUT_DONE
else:
self.input = INPUT_DONE
# update config
if not self.is_nested:
self.config = patch_configurable(
@@ -840,10 +863,12 @@ class SyncPregelLoop(PregelLoop, ContextManager):
interrupt_before: Union[All, Sequence[str]] = EMPTY_SEQ,
output_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
input_model: Optional[Type[BaseModel]] = None,
debug: bool = False,
) -> None:
super().__init__(
input,
input_model=input_model,
stream=stream,
config=config,
checkpointer=checkpointer,
@@ -979,10 +1004,12 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager):
manager: Union[None, AsyncParentRunManager, ParentRunManager] = None,
output_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ,
input_model: Optional[Type[BaseModel]] = None,
debug: bool = False,
) -> None:
super().__init__(
input,
input_model=input_model,
stream=stream,
config=config,
checkpointer=checkpointer,
+9 -9
View File
@@ -1324,19 +1324,19 @@ files = [
[[package]]
name = "langchain-core"
version = "0.3.30"
version = "0.3.44"
description = "Building applications with LLMs through composability"
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main", "dev"]
files = [
{file = "langchain_core-0.3.30-py3-none-any.whl", hash = "sha256:0a4c4e02fac5968b67fbb0142c00c2b976c97e45fce62c7ac9eb1636a6926493"},
{file = "langchain_core-0.3.30.tar.gz", hash = "sha256:0f1281b4416977df43baf366633ad18e96c5dcaaeae6fcb8a799f9889c853243"},
{file = "langchain_core-0.3.44-py3-none-any.whl", hash = "sha256:d989ce8bd62f1d07765acd575e6ec1254aec0cf7775aaea39fe4af8102377459"},
{file = "langchain_core-0.3.44.tar.gz", hash = "sha256:7c0a01e78360f007cbca448178fe7e032404068e6431dbe8ce905f84febbdfa5"},
]
[package.dependencies]
jsonpatch = ">=1.33,<2.0"
langsmith = ">=0.1.125,<0.3"
langsmith = ">=0.1.125,<0.4"
packaging = ">=23.2,<25"
pydantic = [
{version = ">=2.5.2,<3.0.0", markers = "python_full_version < \"3.12.4\""},
@@ -1348,7 +1348,7 @@ typing-extensions = ">=4.7"
[[package]]
name = "langgraph-checkpoint"
version = "2.0.16"
version = "2.0.18"
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.15"
version = "2.0.16"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -1386,7 +1386,7 @@ url = "../checkpoint-postgres"
[[package]]
name = "langgraph-checkpoint-sqlite"
version = "2.0.5"
version = "2.0.6"
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
optional = false
python-versions = "^3.9.0"
@@ -1404,7 +1404,7 @@ url = "../checkpoint-sqlite"
[[package]]
name = "langgraph-prebuilt"
version = "0.1.1"
version = "0.1.2"
description = "Library with high-level APIs for creating and executing LangGraph agents and tools."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -1422,7 +1422,7 @@ url = "../prebuilt"
[[package]]
name = "langgraph-sdk"
version = "0.1.53"
version = "0.1.55"
description = "SDK for interacting with LangGraph API"
optional = false
python-versions = "^3.9.0,<4.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph"
version = "0.3.5"
version = "0.3.7"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
license = "MIT"
@@ -3116,8 +3116,6 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -3126,6 +3124,8 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -3141,8 +3141,6 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -3151,6 +3149,8 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -3166,8 +3166,6 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -3176,6 +3174,8 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -3191,8 +3191,6 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -3201,6 +3199,8 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -3216,8 +3216,6 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -3226,6 +3224,8 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -3241,8 +3241,6 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -3251,6 +3249,8 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -6,8 +6,6 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -16,6 +14,8 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -31,8 +31,6 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -41,6 +39,8 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -56,8 +56,6 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -66,6 +64,8 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -81,8 +81,6 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -91,6 +89,8 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -106,8 +106,6 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -116,6 +114,8 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -131,8 +131,6 @@
__start__([<p>__start__</p>]):::first
router_node(router_node)
normal_llm_node(normal_llm_node)
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
__end__([<p>__end__</p>]):::last
__start__ --> router_node;
normal_llm_node --> __end__;
@@ -141,6 +139,8 @@
router_node -.-> weather_graph_model_node;
router_node -.-> __end__;
subgraph weather_graph
weather_graph_model_node(model_node)
weather_graph_weather_node(weather_node<hr/><small><em>__interrupt = before</em></small>)
weather_graph_model_node --> weather_graph_weather_node;
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -1722,13 +1722,13 @@
__start__([<p>__start__</p>]):::first
uno(uno)
dos(dos)
subgraph_one(one)
subgraph_two(two)
subgraph_three(three)
__start__ --> uno;
uno -.-> dos;
uno -.-> subgraph_one;
subgraph subgraph
subgraph_one(one)
subgraph_two(two)
subgraph_three(three)
subgraph_one -.-> subgraph_two;
subgraph_one -.-> subgraph_three;
end
@@ -1752,12 +1752,14 @@
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([<p>__start__</p>]):::first
inner(inner)
side(side)
__end__([<p>__end__</p>]):::last
__start__ --> inner;
inner --> side;
__start__ --> inner_up;
inner_up --> side;
side --> __end__;
subgraph inner
inner_up(up)
end
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
@@ -1895,10 +1897,6 @@
graph TD;
__start__([<p>__start__</p>]):::first
tool_one(tool_one)
tool_two___start__(<p>__start__</p>)
tool_two_tool_two_slow(tool_two_slow)
tool_two_tool_two_fast(tool_two_fast)
tool_two___end__(<p>__end__</p>)
tool_three(tool_three)
__end__([<p>__end__</p>]):::last
__start__ -.-> tool_one;
@@ -1908,6 +1906,10 @@
__start__ -.-> tool_three;
tool_three --> __end__;
subgraph tool_two
tool_two___start__(<p>__start__</p>)
tool_two_tool_two_slow(tool_two_slow)
tool_two_tool_two_fast(tool_two_fast)
tool_two___end__(<p>__end__</p>)
tool_two___start__ -.-> tool_two_tool_two_slow;
tool_two_tool_two_slow --> tool_two___end__;
tool_two___start__ -.-> tool_two_tool_two_fast;
@@ -1962,24 +1964,24 @@
graph TD;
__start__([<p>__start__</p>]):::first
gp_one(gp_one)
gp_two___start__(<p>__start__</p>)
gp_two_p_one(p_one)
gp_two_p_two___start__(<p>__start__</p>)
gp_two_p_two_c_one(c_one)
gp_two_p_two_c_two(c_two)
gp_two_p_two___end__(<p>__end__</p>)
gp_two___end__(<p>__end__</p>)
__end__([<p>__end__</p>]):::last
__start__ --> gp_one;
gp_two___end__ --> gp_one;
gp_one -. &nbsp;0&nbsp; .-> gp_two___start__;
gp_one -. &nbsp;1&nbsp; .-> __end__;
subgraph gp_two
gp_two___start__(<p>__start__</p>)
gp_two_p_one(p_one)
gp_two___end__(<p>__end__</p>)
gp_two___start__ --> gp_two_p_one;
gp_two_p_two___end__ --> gp_two_p_one;
gp_two_p_one -. &nbsp;0&nbsp; .-> gp_two_p_two___start__;
gp_two_p_one -. &nbsp;1&nbsp; .-> gp_two___end__;
subgraph p_two
gp_two_p_two___start__(<p>__start__</p>)
gp_two_p_two_c_one(c_one)
gp_two_p_two_c_two(c_two)
gp_two_p_two___end__(<p>__end__</p>)
gp_two_p_two___start__ --> gp_two_p_two_c_one;
gp_two_p_two_c_two --> gp_two_p_two_c_one;
gp_two_p_two_c_one -. &nbsp;0&nbsp; .-> gp_two_p_two_c_two;
@@ -1998,16 +2000,16 @@
graph TD;
__start__([<p>__start__</p>]):::first
p_one(p_one)
p_two___start__(<p>__start__</p>)
p_two_c_one(c_one)
p_two_c_two(c_two)
p_two___end__(<p>__end__</p>)
__end__([<p>__end__</p>]):::last
__start__ --> p_one;
p_two___end__ --> p_one;
p_one -. &nbsp;0&nbsp; .-> p_two___start__;
p_one -. &nbsp;1&nbsp; .-> __end__;
subgraph p_two
p_two___start__(<p>__start__</p>)
p_two_c_one(c_one)
p_two_c_two(c_two)
p_two___end__(<p>__end__</p>)
p_two___start__ --> p_two_c_one;
p_two_c_two --> p_two_c_one;
p_two_c_one -. &nbsp;0&nbsp; .-> p_two_c_two;
+1 -1
View File
@@ -2827,7 +2827,7 @@ def test_state_graph_packets(
}
# Define decision-making logic
def should_continue(data: AgentState) -> str:
def should_continue(data: dict) -> str:
assert isinstance(data["session"], httpx.Client)
assert (
data["something_extra"] == "hi there"
+53
View File
@@ -7697,3 +7697,56 @@ async def test_interrupt_subgraph_reenter_checkpointer_true(
}
# confirm that we preserve the state values from the previous invocation
assert bar_values == [None, "barbaz", "quxbaz"]
@NEEDS_CONTEXTVARS
async def test_handles_multiple_interrupts_from_tasks() -> None:
@task
async def add_participant(name: str) -> str:
feedback = interrupt(f"Hey do you want to add {name}?")
if feedback is False:
return f"The user changed their mind and doesn't want to add {name}!"
if feedback is True:
return f"Added {name}!"
raise ValueError("Invalid feedback")
@entrypoint(checkpointer=MemorySaver())
async def program(_state: Any) -> list[str]:
first = await add_participant("James")
second = await add_participant("Will")
return [first, second]
config = {"configurable": {"thread_id": "1"}}
result = await program.ainvoke("this is ignored", config=config)
assert result is None
state = await program.aget_state(config=config)
assert len(state.tasks[0].interrupts) == 1
task_interrupt = state.tasks[0].interrupts[0]
assert task_interrupt.resumable is True
assert len(task_interrupt.ns) == 2
assert task_interrupt.ns[0].startswith("program:")
assert task_interrupt.ns[1].startswith("add_participant:")
assert task_interrupt.value == "Hey do you want to add James?"
result = await program.ainvoke(Command(resume=True), config=config)
assert result is None
state = await program.aget_state(config=config)
assert len(state.tasks[0].interrupts) == 1
task_interrupt = state.tasks[0].interrupts[0]
assert task_interrupt.resumable is True
assert len(task_interrupt.ns) == 2
assert task_interrupt.ns[0].startswith("program:")
assert task_interrupt.ns[1].startswith("add_participant:")
assert task_interrupt.value == "Hey do you want to add Will?"
result = await program.ainvoke(Command(resume=True), config=config)
assert result is not None
assert len(result) == 2
assert result[0] == "Added James!"
assert result[1] == "Added Will!"
+72
View File
@@ -1,4 +1,5 @@
import inspect
import operator
import warnings
from dataclasses import dataclass, field
from typing import Annotated as Annotated2
@@ -328,3 +329,74 @@ def test__get_node_name() -> None:
# class method
assert _get_node_name(MyClass().class_method) == "class_method"
def test_input_schema_conditional_edge():
class OverallState(TypedDict):
foo: Annotated[int, operator.add]
bar: str
class PrivateState(TypedDict):
baz: str
builder = StateGraph(OverallState)
def node_1(state: OverallState):
return {"foo": 1, "baz": "bar"}
def node_2(state: PrivateState):
return {"foo": 1, "bar": state["baz"], "something_else": "meow"}
def node_3(state: OverallState):
return {"foo": 1}
def router(state: OverallState):
assert state == {"foo": 2, "bar": "bar"}
if state["foo"] == 2:
return "node_3"
else:
return "__end__"
builder.add_node(node_1)
builder.add_node(node_2)
builder.add_node(node_3)
builder.add_conditional_edges("node_2", router)
builder.add_edge("__start__", "node_1")
builder.add_edge("node_1", "node_2")
graph = builder.compile()
assert graph.invoke({"foo": 0}) == {"foo": 3, "bar": "bar"}
def test_private_input_schema_conditional_edge():
class OverallState(TypedDict):
foo: Annotated[int, operator.add]
bar: str
class RouterState(TypedDict):
baz: str
class Node2State(TypedDict):
foo: Annotated[int, operator.add]
baz: str
builder = StateGraph(OverallState)
def node_1(state: OverallState):
return {"foo": 1, "baz": "meow"}
def node_2(state: Node2State):
return {"foo": 1, "bar": state["baz"]}
def router(state: RouterState):
assert state == {"baz": "meow"}
if state["baz"] == "meow":
return "node_2"
else:
return "__end__"
builder.add_node(node_1)
builder.add_node(node_2)
builder.add_conditional_edges("node_1", router)
builder.add_edge("__start__", "node_1")
graph = builder.compile()
assert graph.invoke({"foo": 0}) == {"foo": 2, "bar": "meow"}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@langchain/langgraph-sdk",
"version": "0.0.50",
"version": "0.0.53",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
+41
View File
@@ -1320,6 +1320,40 @@ export class StoreClient extends BaseClient {
}
}
class UiClient extends BaseClient {
private static promiseCache: Record<string, Promise<unknown> | undefined> =
{};
private static getOrCached<T>(key: string, fn: () => Promise<T>): Promise<T> {
if (UiClient.promiseCache[key] != null) {
return UiClient.promiseCache[key] as Promise<T>;
}
const promise = fn();
UiClient.promiseCache[key] = promise;
return promise;
}
async getComponent(assistantId: string, agentName: string): Promise<string> {
return UiClient["getOrCached"](
`${this.apiUrl}-${assistantId}-${agentName}`,
async () => {
const response = await this.asyncCaller.fetch(
...this.prepareFetchOptions(`/ui/${assistantId}`, {
headers: {
Accept: "text/html",
"Content-Type": "application/json",
},
method: "POST",
json: { name: agentName },
}),
);
return response.text();
},
);
}
}
export class Client<
TStateType = DefaultValues,
TUpdateType = TStateType,
@@ -1350,11 +1384,18 @@ export class Client<
*/
public store: StoreClient;
/**
* The client for interacting with the UI.
* @internal Used by LoadExternalComponent and the API might change in the future.
*/
public "~ui": UiClient;
constructor(config?: ClientConfig) {
this.assistants = new AssistantsClient(config);
this.threads = new ThreadsClient(config);
this.runs = new RunsClient(config);
this.crons = new CronsClient(config);
this.store = new StoreClient(config);
this["~ui"] = new UiClient(config);
}
}
+13 -34
View File
@@ -1,3 +1,5 @@
"use client";
import { useStream } from "../react/index.js";
import type { UIMessage } from "./types.js";
@@ -105,19 +107,11 @@ class ComponentStore {
}
const COMPONENT_STORE = new ComponentStore();
const COMPONENT_PROMISE_CACHE: Record<string, Promise<string> | undefined> = {};
const EXT_STORE_SYMBOL = Symbol.for("LGUI_EXT_STORE");
const REQUIRE_SYMBOL = Symbol.for("LGUI_REQUIRE");
interface LoadExternalComponentProps
extends Pick<React.HTMLAttributes<HTMLDivElement>, "style" | "className"> {
/** API URL of the LangGraph Platform */
apiUrl?: string;
/** ID of the assistant */
assistantId: string;
/** Stream of the assistant */
stream: ReturnType<typeof useStream>;
@@ -137,29 +131,7 @@ interface LoadExternalComponentProps
components?: Record<string, React.FunctionComponent | React.ComponentClass>;
}
function fetchComponent(
apiUrl: string,
assistantId: string,
agentName: string,
): Promise<string> {
const cacheKey = `${apiUrl}-${assistantId}-${agentName}`;
if (COMPONENT_PROMISE_CACHE[cacheKey] != null) {
return COMPONENT_PROMISE_CACHE[cacheKey] as Promise<string>;
}
const request: Promise<string> = fetch(`${apiUrl}/ui/${assistantId}`, {
headers: { Accept: "text/html", "Content-Type": "application/json" },
method: "POST",
body: JSON.stringify({ name: agentName }),
}).then((a) => a.text());
COMPONENT_PROMISE_CACHE[cacheKey] = request;
return request;
}
export function LoadExternalComponent({
apiUrl = "http://localhost:2024",
assistantId,
stream,
message,
meta,
@@ -180,9 +152,10 @@ export function LoadExternalComponent({
const clientComponent = components?.[message.name];
const hasClientComponent = clientComponent != null;
const uiClient = stream.client["~ui"];
React.useEffect(() => {
if (hasClientComponent) return;
fetchComponent(apiUrl, assistantId, message.name).then((html) => {
uiClient.getComponent(stream.assistantId, message.name).then((html) => {
const dom = ref.current;
if (!dom) return;
const root = dom.shadowRoot ?? dom.attachShadow({ mode: "open" });
@@ -193,10 +166,16 @@ export function LoadExternalComponent({
);
root.appendChild(fragment);
});
}, [apiUrl, assistantId, message.name, shadowRootId, hasClientComponent]);
}, [
uiClient,
stream.assistantId,
message.name,
shadowRootId,
hasClientComponent,
]);
if (hasClientComponent) {
return React.createElement(clientComponent, message.content);
return React.createElement(clientComponent, message.props);
}
return (
@@ -206,7 +185,7 @@ export function LoadExternalComponent({
<UseStreamContext.Provider value={{ stream, meta }}>
{state?.target != null
? ReactDOM.createPortal(
React.createElement(state.comp, message.content),
React.createElement(state.comp, message.props),
state.target,
)
: fallback}
+5 -1
View File
@@ -2,4 +2,8 @@ import { bootstrapUiContext } from "./client.js";
bootstrapUiContext();
export { useStreamContext, LoadExternalComponent } from "./client.js";
export type { UIMessage, RemoveUIMessage } from "./types.js";
export {
uiMessageReducer,
type UIMessage,
type RemoveUIMessage,
} from "./types.js";
+36 -23
View File
@@ -2,6 +2,10 @@ import { v4 as uuidv4 } from "uuid";
import type { ComponentPropsWithoutRef, ElementType } from "react";
import type { RemoveUIMessage, UIMessage } from "../types.js";
interface MessageLike {
id?: string;
}
export const typedUi = <Decl extends Record<string, ElementType>>(config: {
writer?: (chunk: unknown) => void;
runId?: string;
@@ -10,7 +14,7 @@ export const typedUi = <Decl extends Record<string, ElementType>>(config: {
runName?: string;
}) => {
type PropMap = { [K in keyof Decl]: ComponentPropsWithoutRef<Decl[K]> };
let collect: (UIMessage | RemoveUIMessage)[] = [];
let items: (UIMessage | RemoveUIMessage)[] = [];
const runId = (config.metadata?.run_id as string | undefined) ?? config.runId;
if (!runId) throw new Error("run_id is required");
@@ -22,28 +26,37 @@ export const typedUi = <Decl extends Record<string, ElementType>>(config: {
run_id: runId,
};
const create = <K extends keyof PropMap & string>(
name: K,
props: PropMap[K],
): UIMessage => ({
type: "ui" as const,
id: uuidv4(),
name,
content: props,
additional_kwargs: metadata,
});
const remove = (id: string): RemoveUIMessage => ({ type: "remove-ui", id });
return {
create,
remove,
collect,
write: <K extends keyof PropMap & string>(name: K, props: PropMap[K]) => {
const evt: UIMessage = create(name, props);
collect.push(evt);
config.writer?.(evt);
const handlePush = <K extends keyof PropMap & string>(
message: {
id?: string;
name: K;
props: PropMap[K];
metadata?: Record<string, unknown>;
},
options?: { message?: MessageLike },
): UIMessage => {
const evt: UIMessage = {
type: "ui" as const,
id: message?.id ?? uuidv4(),
name: message?.name,
props: message?.props,
metadata: {
...metadata,
...message?.metadata,
...(options?.message ? { message_id: options.message.id } : null),
},
};
items.push(evt);
config.writer?.(evt);
return evt;
};
const handleDelete = (id: string): RemoveUIMessage => {
const evt: RemoveUIMessage = { type: "remove-ui", id };
items.push(evt);
config.writer?.(evt);
return evt;
};
return { push: handlePush, delete: handleDelete, items };
};
+3 -2
View File
@@ -3,9 +3,10 @@ export interface UIMessage {
id: string;
name: string;
content: Record<string, unknown>;
additional_kwargs: {
props: Record<string, unknown>;
metadata: {
run_id: string;
message_id?: string;
[key: string]: unknown;
};
}
+31 -1
View File
@@ -464,6 +464,11 @@ interface UseStreamOptions<
*/
onCustomEvent?: (
data: CustomStreamEvent<GetCustomEventType<Bag>>["data"],
options: {
mutate: (
update: Partial<StateType> | ((prev: StateType) => Partial<StateType>),
) => void;
},
) => void;
/**
@@ -558,6 +563,16 @@ export interface UseStream<
message: Message,
index?: number,
) => MessageMetadata<StateType> | undefined;
/**
* LangGraph SDK client used to send request and receive responses.
*/
client: Client;
/**
* The ID of the assistant to use.
*/
assistantId: string;
}
type ConfigWithConfigurable<ConfigurableType extends Record<string, unknown>> =
@@ -627,6 +642,7 @@ export function useStream<
options.defaultHeaders,
],
);
const [threadId, onThreadId] = useControllableThreadId(options);
const [branch, setBranch] = useState<string>("");
@@ -834,7 +850,18 @@ export function useStream<
}
if (event === "updates") options.onUpdateEvent?.(data);
if (event === "custom") options.onCustomEvent?.(data);
if (event === "custom")
options.onCustomEvent?.(data, {
mutate: (update) =>
setStreamValues((prev) => {
// should not happen
if (prev == null) return prev;
return {
...prev,
...(typeof update === "function" ? update(prev) : update),
};
}),
});
if (event === "metadata") options.onMetadataEvent?.(data);
if (event === "values") setStreamValues(data);
@@ -903,6 +930,9 @@ export function useStream<
return values;
},
client,
assistantId,
error,
isLoading,
+3 -3
View File
@@ -1177,9 +1177,9 @@ available-typed-arrays@^1.0.7:
possible-typed-array-names "^1.0.0"
axios@^1.6.7:
version "1.7.7"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==
version "1.8.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.2.tgz#fabe06e241dfe83071d4edfbcaa7b1c3a40f7979"
integrity sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
+2 -2
View File
@@ -2163,7 +2163,7 @@ class StoreClient:
"index": index,
"ttl": ttl,
}
await self.http.put("/store/items", json=payload)
await self.http.put("/store/items", json=_provided_vals(payload))
async def get_item(
self,
@@ -4307,7 +4307,7 @@ class SyncStoreClient:
"index": index,
"ttl": ttl,
}
self.http.put("/store/items", json=payload)
self.http.put("/store/items", json=_provided_vals(payload))
def get_item(
self,
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-sdk"
version = "0.1.55"
version = "0.1.56"
description = "SDK for interacting with LangGraph API"
authors = []
license = "MIT"