Compare commits

..
Author SHA1 Message Date
William FHandGitHub 4d01e69b82 release(checkpoint-postgres): 3.0.1 (#6568) 2025-12-09 23:05:49 +00:00
William FHandGitHub e86b5f4da2 chore: pgqs (#6567)
Add more argument sanitization
2025-12-09 14:51:29 -08:00
Eugene YurtsevandGitHub b70d5aac0e release(checkpoint-sqlite): 3.0.1 (#6566)
Release 3.0.1
2025-12-09 17:00:00 -05:00
Eugene YurtsevandGitHub 297242913f fix(checkpoint-sqlite): harden (#6565)
harden
2025-12-09 16:47:55 -05:00
Mason DaughertyandGitHub 02965fb5f5 chore: revise CONTRIBUTING.md (#6561) 2025-12-09 20:33:11 +00:00
Mason DaughertyandGitHub 64f0458296 chore: nits (#6560)
- Delete `security.md` as it will be inherited from
`langchain-ai/.github`
- Move `CONTRIBUTING.md` to `.github` (will still appear on homepage),
cleaning up top level
- Copy contents of `AGENTS.md` to `CLAUDE.md` since CC still doesn't
take `AGENTS.md`
2025-12-09 15:23:59 -05:00
Andrew NguonlyandGitHub 76711536f9 feat(sdk-py): Add name parameter to Assistants count API (#6558)
**Description**: The Agent Server API now supports counting assistants
by name.

This is similar to adding the `name` parameter to the Assistants search
API: https://github.com/langchain-ai/langgraph/pull/6483
2025-12-09 11:09:12 -08:00
William FHandGitHub 6c6978918e chore(cli): Pass through webhook configuration in dev server (#6557) 2025-12-09 07:05:15 -08:00
William FHandGitHub 2f1a16006a release(cli): 0.4.8 (#6556)
For webhook configuration support
2025-12-09 13:25:02 +00:00
William FHandGitHub 269d08f5d3 feat(cli): webhook configuration (#6555) 2025-12-09 13:17:22 +00:00
94698c8a34 feat: custom encryption at rest (#6482)
**Description:** 
This PR adds the Python SDK types necessary for langgraph platform users
to inject their own custom encryption-at-rest functions. See [docs
PR](https://github.com/langchain-ai/docs/pull/1715) for more details.

note: this PR adds a starlette dev dependency so that custom encryption
can access BaseUser information.

**Issue:**
required for LSD-172 

**Dependencies:**
- [depended upon by associated langgraph-api
changes](https://github.com/langchain-ai/langgraph-api/pull/1773)(this
PR must merge before that one)
- [docs PR](https://github.com/langchain-ai/docs/pull/1715)

**TODO:**
- [x] move docs to docs repo
- [x] bump package versions before merge

---------

Signed-off-by: Connor Braa <cwlbraa@langchain.dev>
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-06 00:10:17 +00:00
Mason DaughertyandGitHub 7d557cb865 chore: fix links for docs (#6538) 2025-12-05 23:09:41 +00:00
William FHandGitHub f211fadc2b release(sdk-py): Configure loopback client (#6536)
Will defer defaulting to deferred registration until a later date.
2025-12-05 11:56:40 -08:00
William FHandGitHub 024468c5dd chore: Bump lockfile (#6537) 2025-12-05 11:16:19 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7f08a6dafd chore(deps): bump js-yaml from 4.1.0 to 4.1.1 in /docs/_scripts/js_translation/codeblocks (#6452)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md">js-yaml's
changelog</a>.</em></p>
<blockquote>
<h2>[4.1.1] - 2025-11-12</h2>
<h3>Security</h3>
<ul>
<li>Fix prototype pollution issue in yaml merge (&lt;&lt;)
operator.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/nodeca/js-yaml/commit/cc482e775913e6625137572a3712d2826170e53a"><code>cc482e7</code></a>
4.1.1 released</li>
<li><a
href="https://github.com/nodeca/js-yaml/commit/50968b862e75866ef90e626572fe0b2f97b55f9f"><code>50968b8</code></a>
dist rebuild</li>
<li><a
href="https://github.com/nodeca/js-yaml/commit/d092d866031751cb27c12d93f3e2470ad74d678b"><code>d092d86</code></a>
lint fix</li>
<li><a
href="https://github.com/nodeca/js-yaml/commit/383665ff4248ec2192d1274e934462bb30426879"><code>383665f</code></a>
fix prototype pollution in merge (&lt;&lt;)</li>
<li><a
href="https://github.com/nodeca/js-yaml/commit/0d3ca7a27b03a6c974790a30a89e456007d62976"><code>0d3ca7a</code></a>
README.md: HTTP =&gt; HTTPS (<a
href="https://redirect.github.com/nodeca/js-yaml/issues/678">#678</a>)</li>
<li><a
href="https://github.com/nodeca/js-yaml/commit/49baadd52af887d2991e2c39a6639baa56d6c71b"><code>49baadd</code></a>
doc: 'empty' style option for !!null</li>
<li><a
href="https://github.com/nodeca/js-yaml/commit/ba3460eb9d3e4478edcbc29edabe17c2157fc9ce"><code>ba3460e</code></a>
Fix demo link (<a
href="https://redirect.github.com/nodeca/js-yaml/issues/618">#618</a>)</li>
<li>See full diff in <a
href="https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=js-yaml&package-manager=npm_and_yarn&previous-version=4.1.0&new-version=4.1.1)](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>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 19:54:26 +00:00
William FHandGitHub 76203e2c20 chore: Sync langgraph.json schema (#6530) 2025-12-02 19:52:06 +00:00
b6cc022861 fix(checkpoint): InMemorySaver context managers should return self in… (#6529)
h/t to @lexi-k for openening. merged here to check/fix CI

Co-authored-by: lexi-k <69981673+lexi-k@users.noreply.github.com>
2025-12-02 11:47:41 -08:00
Mason DaughertyandGitHub 02bb7a436a chore: remove erroneous release title prefix (#6528) 2025-12-02 13:45:43 -05:00
Hunter LovellandGitHub 43d21e9c7d chore(ci): disable redirects build hook (#6527) 2025-12-02 08:43:57 -05:00
William FHandGitHub 55ed7d4b49 feat: Include pagination in assistants search response (#6526) 2025-12-01 20:18:26 -08:00
William FHandGitHub 2b78bf3bad fix: docstring for serializer protocol (#6525) 2025-12-01 18:14:12 +00:00
Mason DaughertyandGitHub 630bd9ab95 fix: release name should be same as package name (#6503) 2025-11-25 19:02:44 -05:00
William FHandGitHub b945b1f21e release(langgraph): 1.0.4 (#6502)
This patch release includes a couple of small fixes and improvements,
such as:
- Include interrupt information in the values stream mode (no longer
rely solely on the updates stream mode):
https://github.com/langchain-ai/langgraph/pull/6475
- Omit thread_id from configurable to allow using context for stateful
runs via RemoteGraph:
https://github.com/langchain-ai/langgraph/pull/6497
2025-11-25 20:27:49 +00:00
William FHandGitHub 4cb108ccd9 release(sdk-py): 0.2.11 (#6501) 2025-11-25 19:17:40 +00:00
Josh RogersandGitHub 23d78c2817 feat(sdk-py): add sentinel to skip auto loading api key on sdk client create (#6500)
**Description:** There are times a user might want to create the client,
but conditionally set the API key. For example, consider a complex auth
situation where the system has user callers using jwts and system
callers using API keys. This allows explicitly disabling the
auto-loading behavior of API keys in the client today, so no key is set.
**Issue:** N/A
**Dependencies:** None
**Twitter handle:** N/A
2025-11-25 11:10:56 -08:00
William FHandGitHub f8c1a323cc chore(sdk-py): Improve type-hinting of inputs (#6480) 2025-11-24 18:29:03 -08:00
William FHandGitHub b7e329cf0a chore: pop thread ID from configurable fields in remote graph (#6497)
Otherwise, you cannot use `context` with stateful runs, because the
server throws if you provide both configurable and context in a single
call (due to ambiguous parameters)
2025-11-24 16:09:08 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3f49249e72 chore(deps): bump actions/checkout from 5 to 6 (#6490)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to
6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>v6-beta by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2298">actions/checkout#2298</a></li>
<li>update readme/changelog for v6 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2311">actions/checkout#2311</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v5.0.0...v6.0.0">https://github.com/actions/checkout/compare/v5.0.0...v6.0.0</a></p>
<h2>v6-beta</h2>
<h2>What's Changed</h2>
<p>Updated persist-credentials to store the credentials under
<code>$RUNNER_TEMP</code> instead of directly in the local git
config.</p>
<p>This requires a minimum Actions Runner version of <a
href="https://github.com/actions/runner/releases/tag/v2.329.0">v2.329.0</a>
to access the persisted credentials for <a
href="https://docs.github.com/en/actions/tutorials/use-containerized-services/create-a-docker-container-action">Docker
container action</a> scenarios.</p>
<h2>v5.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v5...v5.0.1">https://github.com/actions/checkout/compare/v5...v5.0.1</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>V6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>V5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>V5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>V4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>V4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>v4.2.0</h2>
<ul>
<li>Add Ref and Commit outputs by <a
href="https://github.com/lucacome"><code>@​lucacome</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1180">actions/checkout#1180</a></li>
<li>Dependency updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>- <a
href="https://redirect.github.com/actions/checkout/pull/1777">actions/checkout#1777</a>,
<a
href="https://redirect.github.com/actions/checkout/pull/1872">actions/checkout#1872</a></li>
</ul>
<h2>v4.1.7</h2>
<ul>
<li>Bump the minor-npm-dependencies group across 1 directory with 4
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1739">actions/checkout#1739</a></li>
<li>Bump actions/checkout from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1697">actions/checkout#1697</a></li>
<li>Check out other refs/* by commit by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1774">actions/checkout#1774</a></li>
<li>Pin actions/checkout's own workflows to a known, good, stable
version. by <a href="https://github.com/jww3"><code>@​jww3</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1776">actions/checkout#1776</a></li>
</ul>
<h2>v4.1.6</h2>
<ul>
<li>Check platform to set archive extension appropriately by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1732">actions/checkout#1732</a></li>
</ul>
<h2>v4.1.5</h2>
<ul>
<li>Update NPM dependencies by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1703">actions/checkout#1703</a></li>
<li>Bump github/codeql-action from 2 to 3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1694">actions/checkout#1694</a></li>
<li>Bump actions/setup-node from 1 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1696">actions/checkout#1696</a></li>
<li>Bump actions/upload-artifact from 2 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1695">actions/checkout#1695</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/checkout/commit/1af3b93b6815bc44a9784bd300feb67ff0d1eeb3"><code>1af3b93</code></a>
update readme/changelog for v6 (<a
href="https://redirect.github.com/actions/checkout/issues/2311">#2311</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/71cf2267d89c5cb81562390fa70a37fa40b1305e"><code>71cf226</code></a>
v6-beta (<a
href="https://redirect.github.com/actions/checkout/issues/2298">#2298</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/069c6959146423d11cd0184e6accf28f9d45f06e"><code>069c695</code></a>
Persist creds to a separate file (<a
href="https://redirect.github.com/actions/checkout/issues/2286">#2286</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493"><code>ff7abcd</code></a>
Update README to include Node.js 24 support details and requirements (<a
href="https://redirect.github.com/actions/checkout/issues/2248">#2248</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/checkout/compare/v5...v6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=5&new-version=6)](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)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 13:41:21 -08:00
Andrew NguonlyandGitHub 78910dade0 chore: Bump version of sdk-py to 0.2.10 (#6496)
**Description:** Updating `sdk-py` version for release.
2025-11-24 12:27:16 -08:00
Andrew NguonlyandGitHub 33fbd1bb9c feat(sdk-py): Add name parameter to Assistants search API (#6483)
**Description:** The Agent Server API now supports searching for
assistants by name.
2025-11-24 10:58:54 -08:00
Mason DaughertyandGitHub d45f52ab0c chore: clean up some refs (#6487) 2025-11-23 02:13:30 -05:00
William FHandGitHub ed07d4a5d4 chore(sdk-py): Add more type checking. (#6479) 2025-11-20 17:40:23 -08:00
Caspar BroekhuizenandGitHub 2284a54b64 fix: interrupt stream mode values (#6475)
This PR improves the consistency of interrupt streaming. 

- when streaming with stream_mode values, the stream chunk now contains
the entire state alongside the interrupt:

```python
class State(TypedDict):
    robot_input: str
# at this point in time robot_input is already set to  "beep boop i am a robot"
app.stream(..., stream_mode="values")
# before
{"__interrupt__": (Interrupt(value="interrupt",))}}
# after
{"robot_input": "beep boop i am a robot", "__interrupt__": (Interrupt(value="interrupt"))}
```

- when streaming with stream_mode=["values", "updates"], interrupts are
surfaced in both an update stream chunk and the value stream chunk, when
previously we keep interrupt in values only if we request values mode
only

```python
class State(TypedDict):
    robot_input: str
# at this point in time robot_input is already set to  "beep boop i am a robot"
app.stream(..., stream_mode=["values", "updates"])
# before (interrupt would only emit on update chunk, there would be no values chunk)
("updates", {"__interrupt__": (Interrupt(value="interrupt",))}})
# after
("updates", {"__interrupt__": (Interrupt(value="interrupt",))}})
("values", {"robot_input": "beep boop i am a robot", "__interrupt__": (Interrupt(value="interrupt"))})
```

For housekeeping: this PR improves on this revert:
https://github.com/langchain-ai/langgraph/pull/6141
2025-11-20 14:23:09 -08:00
Sydney RunkleandGitHub f8006b2fee release: langgraph-prebuilt 1.0.5 (#6473) 2025-11-20 11:45:19 -05:00
Sydney RunkleandGitHub 2164b7daa3 fix: refactor injection logic to respect function signatures (#6468)
## overview

The main purpose of this is to respect tool signatures that request
injected args (like `ToolRuntime`) even when the explicitly specified
`args_schema` does not.

Ex in the following example, we should still inject `runtime` despite
its absence in `ArgsSchema`

```py
class ArgsSchema(BaseModel):
    some_arg: int = Field(...)

@tool(args_schema=ArgsSchema)
def my_tool(some_arg: int, runtime: ToolRuntime): ...
```

This is accompanied by
https://github.com/langchain-ai/langchain/pull/34051 which has tests
that pass w/ this change. This tests injection w/ `create_agent` (more
end to end than tests added in
https://github.com/langchain-ai/langchain/pull/33999.

This unblocks the injection of `ToolRuntime` into MCP tools which is
exciting bc that exposes tool call id and state, which we previously
were unable to do.

## other benefits

* Cleaner code structure w/ more helpful docs about injected args.
* Nice perf boost, we're no longer inspecting the annotations of a
tool's schema 3 different times to detect store, state, and runtime
injections.

## additional notes

1. I could see a world where we want more of this logic to reside on the
tools themselves, but tools don't now about LG specific injection types
(like `ToolRuntime`, hence having this logic here for now).
2. We could separately add validation for the case where something is
specified in `args_schema` and not in the function signature (probably
at the tool level though).
2025-11-20 11:37:54 -05:00
Sydney RunkleandGitHub 6d20a0b9c7 fix: deprecate setattr on ToolCallRequest (#6462)
* one alternative considered was setting `frozen=True` on the dataclass,
but this is breaking, so a deprecation is a nicer approach
2025-11-19 13:12:11 -05:00
William FHandGitHub df8becd5cf refactor: separate prepare_push_* functions (#6450)
Extract two common cases from the big switch statement of
`prepare_single_task` since it's a tad more composable.

All this does is shift/extract code to separate functions
2025-11-14 16:13:03 -08:00
Lauren Hirata SinghandGitHub 056ba91a71 chore(docs): add more redirects + catchall (#6442) 2025-11-13 14:49:40 -05:00
Sydney RunkleandGitHub 02300de24c fix: dep warnings in prebuilt (#6443) 2025-11-13 13:59:34 -05:00
Sydney RunkleandGitHub ac16bdb795 release: prebuilt 1.0.3 (#6441) 2025-11-13 13:38:24 -05:00
Caspar BroekhuizenandGitHub 0d4ac836e3 chore: langgraph patch release (#6429) 2025-11-10 09:37:35 -08:00
Lauren Hirata SinghandGitHub 201c8015ea chore(docs): Update links in notebook_hooks.py for deployment (#6428) 2025-11-10 09:51:05 -05:00
Mason DaughertyandGitHub cf3e8252f5 feat(docs): warn that StateGraph is a builder class (#6417) 2025-11-07 21:09:15 -05:00
Mason DaughertyandGitHub 7a5e3c1e79 fix(docs): PartialState rendering in MkDocs (#6416)
The carat chars were not rendering without code style formatting
2025-11-07 21:08:01 -05:00
Mason DaughertyandGitHub 218c60717e fix(docs): synchronize invoke and ainvoke docstrings (#6415)
Similar to #6414
2025-11-07 20:44:33 -05:00
Mason DaughertyandGitHub 28b9f578b0 fix(docs): synchronize stream and astream docstrings (#6414)
`stream` and `astream` docstrings listed different available
`stream_mode` options.

Both methods support the same seven stream modes as defined in
`StreamMode`

Fixed for consistency
2025-11-07 20:44:25 -05:00
le-codeur-rapideandGitHub bef76b791c docs(langgraph): Fix docstring code examples of task function (#6410)
Hi all,
I found out that the sync and async code examples of the `task` function
in `libs/langgraph/langgraph/func/__init__.py` have a typo:
```
    Example: Sync Task
        ```python
        from langgraph.func import entrypoint, task


        @task
        def add_one(a: int) -> int:
            return a + 1


        @entrypoint()
        def add_one(numbers: list[int]) -> list[int]:
            futures = [add_one(n) for n in numbers]
            results = [f.result() for f in futures]
            return results


        # Call the entrypoint
        add_one.invoke([1, 2, 3])  # Returns [2, 3, 4]
        ```
```

Both task and entrypoint functions have the same name which gives an
error.

This is a small PR to fix this
2025-11-07 16:01:19 -05:00
a1b34efdb0 fix(checkpoint-postgres): ensure vector extension is created only if not exists (#6154)
Thank you for contributing to LangGraph! Follow these steps to mark your
pull request as ready for review. **If any of these steps are not
completed, your PR will not be considered for review.**

- [x] **PR title**: Follows the format: {TYPE}({SCOPE}): {DESCRIPTION}
  - Examples:
    - feat(core): add multi-tenant support
    - fix(cli): resolve flag parsing error
    - docs(openai): update API usage examples
  - Allowed `{TYPE}` values:
- feat, fix, docs, style, refactor, perf, test, build, ci, chore,
revert, release
  - Allowed `{SCOPE}` values (optional):
- langgraph, docs, cli, checkpoint, checkpoint-postgres,
checkpoint-sqlite, prebuilt, scheduler-kafka, sdk-py
- Once you've written the title, please delete this checklist item; do
not include it in the PR.

- **Description:** Azure Postgres SQL server has a limitation when doing
create extension vector is not exists, even though it manually created
before on a schema.
- **Issue:** Even though `CREATE EXTENSION vector` is executed manually
before, the permission issue arises. Putting it in an if else block
solves the issue and its not a breaking change.
```
Because vector isn't a trusted extension, only members of "azure_pg_admin" are allowed to use CREATE EXTENSION vector
HINT: to learn how to allow an extension or see the list of allowed extensions, please refer to https://go.microsoft.com/fwlink/?linkid=2301063
```

Co-authored-by: Josh Rogers <josh@langchain.dev>
2025-11-07 11:42:15 -05:00
André MenezesandGitHub 7ab5788f25 fix(langgraph): Unexpected behavior for stream_mode sequences that are not lists (#6354)
## Issue
The `stream_mode` argument type includes `Sequence`, but it doesn't
correctly support non-list sequences. On the other hand, the
`print_mode` argument works as expected.

### Example
```python
from langgraph.pregel.main import Pregel

pregel = Pregel(nodes={}, channels=None, input_channels=[], output_channels=[], auto_validate=False)
stream_modes, *_ = pregel._defaults(
    config={"recursion_limit": 1},
    stream_mode=("values", "messages"),
    print_mode=("values"),
    output_keys=None,
    interrupt_before=None,
    interrupt_after=None,
    durability=None,
)
print(stream_modes) # Expected `{'values', 'messages'}`, got `{('values', 'messages'), 'values'}`
```
2025-11-07 08:01:25 -05:00
Cole MurrayandGitHub b0a1029d55 fix(checkpoint-postgres): Replace f-string SQL formatting with parameterized queries in migration statements (#6328)
## Summary

Replace f-string SQL formatting with parameterized queries to prevent
potential SQL injection in checkpoint migration code.

## Changes

Updated the migration version tracking INSERT statements in all
checkpoint saver classes to use parameterized queries instead of
f-string formatting:

- `PostgresSaver`
(libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py:100)
- `AsyncPostgresSaver`
(libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py:104-106)
- `ShallowPostgresSaver`
(libs/checkpoint-postgres/langgraph/checkpoint/postgres/shallow.py:255)
- `AsyncShallowPostgresSaver`
(libs/checkpoint-postgres/langgraph/checkpoint/postgres/shallow.py:617-619)

**Before (vulnerable to SQL injection):**
```python
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
```

**After (using parameterized query):**
```python
cur.execute("INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,))
```

## Risk Assessment

The practical risk is low since `v` is an integer loop variable
controlled by the codebase. However, using string formatting in SQL
queries is a well-known anti-pattern that can lead to SQL injection
vulnerabilities, especially if the code is later refactored or copied to
other contexts.

## Testing

-  All 216 tests passing on PostgreSQL 15 and 16
-  Linting and type checking passing
-  No functional changes to behavior
2025-11-07 08:00:10 -05:00
Mason DaughertyandGitHub c2ef3f3fd3 fix: remove SDK inline links (#6307)
these were broken; remove for now.
2025-11-07 07:56:00 -05:00
Michael LiandGitHub d455bd841d fix: fix previoius edge cases such as 0 (#6379)
Thank you for contributing to LangGraph! Follow these steps to mark your
pull request as ready for review. **If any of these steps are not
completed, your PR will not be considered for review.**

- [x] **PR title**: Follows the format: {TYPE}({SCOPE}): {DESCRIPTION}
  - Examples:
    - feat(core): add multi-tenant support
    - fix(cli): resolve flag parsing error
    - docs(openai): update API usage examples
  - Allowed `{TYPE}` values:
- feat, fix, docs, style, refactor, perf, test, build, ci, chore,
revert, release
  - Allowed `{SCOPE}` values (optional):
- langgraph, docs, cli, checkpoint, checkpoint-postgres,
checkpoint-sqlite, prebuilt, scheduler-kafka, sdk-py
- Once you've written the title, please delete this checklist item; do
not include it in the PR.

- [x] **PR message**: ***Delete this entire checklist*** and replace
with
- **Description:** a description of the change. Include a [closing
keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)
if applicable.
  - **Issue:** the issue # it fixes, if applicable
  - **Dependencies:** any dependencies required for this change
- **Twitter handle:** if your PR gets announced, and you'd like a
mention, we'll gladly shout you out!

- [x] **Add tests and docs**: If you're adding a new integration, you
must include:
1. A test for the integration, preferably unit tests that do not rely on
network access,
2. An example notebook showing its use. It lives in
`docs/docs/integrations` directory.

- [x] **Lint and test**: Run `make format`, `make lint` and `make test`
from the root of the package(s) you've modified. We will not consider a
PR unless these three are passing in CI. See [contribution
guidelines](https://github.com/langchain-ai/langgraph/blob/main/CONTRIBUTING.md)
for more.

Additional guidelines:

- Make sure optional dependencies are imported within a function.
- Please do not add dependencies to `pyproject.toml` files (even
optional ones) unless they are **required** for unit tests.
- Most PRs should not touch more than one package.
- Changes should be backwards compatible.
2025-11-07 07:54:22 -05:00
Pedro Enrique Agurto CastilloandGitHub 69a09adef6 fix(langgraph): export REMOVE_ALL_MESSAGES in __all__ to fix linting (#6375)
`REMOVE_ALL_MESSAGES` is a public constant used with `RemoveMessage` to
clear all messages from the state:

```python
from langchain_core.messages import RemoveMessage
from langgraph.graph.message import REMOVE_ALL_MESSAGES

# Clear all messages
[RemoveMessage(id=REMOVE_ALL_MESSAGES)]
```

However, it is not exported in __all__, causing:

Linting errors in IDEs (PyCharm)
no-member warnings from Pylint
Confusion for users

This PR:

Adds REMOVE_ALL_MESSAGES to __all__
Adds inline docstring with usage example

No runtime behavior changes — only improves IDE support and API clarity.

Thank you for contributing to LangGraph! Follow these steps to mark your
pull request as ready for review. **If any of these steps are not
completed, your PR will not be considered for review.**

---
Local verification:
```bash
# Before
from langgraph.graph.message import REMOVE_ALL_MESSAGES  # Pylint: no-member

# After: no error
```
CI Note: This is a pure export/docs fix. `make lint` and `make test`
pass unchanged.
2025-11-07 07:52:27 -05:00
Kavya GoyalandGitHub 35aa98b110 fix(sdk-py): use correct f-string representation when loading error (#6388)
Thank you for contributing to LangGraph! Follow these steps to mark your
pull request as ready for review. **If any of these steps are not
completed, your PR will not be considered for review.**


## Description
- Fixed a bug in `libs/sdk-py/langgraph_sdk/auth/__init__.py` where the
error message for an already-set authentication handler did not properly
render the handler value.
- Updated the error string from a static `{self._authenticate_handler}`
to a correctly interpolated f-string.

```python
"Authentication handler already set as {self._authenticate_handler}."
```

```python
f"Authentication handler already set as {self._authenticate_handler}."
```

- Error messages now correctly display the actual handler instance,
improving debugging clarity.



- **Issue:** Fixes https://github.com/langchain-ai/langgraph/issues/6387
  - **Dependencies:** -
  - **Twitter handle:** -

- [x] **Add tests and docs**: If you're adding a new integration, you
must include:
1. A test for the integration, preferably unit tests that do not rely on
network access,
2. An example notebook showing its use. It lives in
`docs/docs/integrations` directory.

- [ ] **Lint and test**: Run `make format`, `make lint` and `make test`
from the root of the package(s) you've modified. We will not consider a
PR unless these three are passing in CI. See [contribution
guidelines](https://github.com/langchain-ai/langgraph/blob/main/CONTRIBUTING.md)
for more.

Additional guidelines:

- Make sure optional dependencies are imported within a function.
- Please do not add dependencies to `pyproject.toml` files (even
optional ones) unless they are **required** for unit tests.
- Most PRs should not touch more than one package.
- Changes should be backwards compatible.
2025-11-07 07:51:35 -05:00
Mason DaughertyandGitHub 0f83d9fafe style: update docstrings to reference StateGraph (#6308)
nit
2025-11-07 07:47:29 -05:00
Logan RosenandGitHub 52d66df92c docs(langgraph): update streaming guide links (#6314)
Updating links to the LangGraph streaming guide to point to the new
documentation website for 1.0.
2025-11-07 07:46:48 -05:00
Mason DaughertyandGitHub 4ec92f9fb1 chore: add pyproject.toml links (#6364) 2025-11-07 07:43:51 -05:00
inhunandGitHub 2b72953064 docs: add license files for checkpoint-sqlite and checkpoint-postgres (#6392)
In this PR:

- Add missing LICENSE files for checkpoint-sqlite and
checkpoint-postgres libraries.

Both libraries specify the MIT License in their pyproject.toml files,
but the actual LICENSE files were missing.
This update adds the corresponding LICENSE files to ensure proper
license documentation and compliance.
2025-11-07 07:39:06 -05:00
le-codeur-rapideandGitHub 232014e8ef docs(langgraph): Fix typo in docstring of PregelLoop.tick (#6407)
This is a very small PR to correct a typo in the docstring of the
`PregelLoop.tick()` method.
```python
  def tick(self) -> bool:
      """Execute a single iteration of the Pregel loop.

      Args:
          input_keys: The key(s) to read input from.

      Returns:
          True if more iterations are needed.
      """
```

Corrected to :
```python
  def tick(self) -> bool:
      """Execute a single iteration of the Pregel loop.

      Returns:
          True if more iterations are needed.
      """
```

The docstring was written in #2946 when the signature of tick was
```python
    def tick(
        self,
        *,
        input_keys: Union[str, Sequence[str]],
    ) -> bool:
```
but  it was simplified to 
```python
def tick(self) -> bool:
```
in #5080
2025-11-07 07:38:16 -05:00
Josh RogersandGitHub 9fd3dfc542 chore(checkpoint-postgres): bump to 3.0.1 (#6402)
**Description:** Bumping the checkpoint-postgres package to version
3.0.1 to release an update to migrations
(https://github.com/langchain-ai/langgraph/pull/6400).
**Issue:** N/A
**Dependencies:** N/A
**Twitter handle:** N/A
2025-11-06 11:14:02 -05:00
Josh RogersandGitHub ff38f75594 fix(checkpoint-postgres): make async PG checkpoint migration idempotent (#6400)
- **Description:** The final migration for the postgres checkpointer is
not currently idempotent. That presents problems when migrating from one
checkpointer to another or if migrations otherwise get applied twice.
This makes the final migration idempotent to avoid this problem.
- **Issue:** N/A
- **Dependencies:** N/A
- **Twitter handle:** N/A
2025-11-06 09:49:51 -05:00
Kathryn MayandGitHub bbdd007341 chore(docs): Update redirects for langgraph server to agent server rename (#6399)
Renaming LangGraph Server to Agent Server, this updates the redirects
from the old site to the new site's renamed files.
PR also includes some hosting --> platform setup redirects
2025-11-05 11:21:09 -05:00
Caspar BroekhuizenandGitHub 3c75e414e5 fix(langgraph): do not apply pending writes when updating state (#6389)
PR #6195 fixed `bulk_update_state` to populate `task.result` by calling
`prepare_next_tasks` to discover task IDs. Before #6195,
prepare_next_tasks was gated by the condition `CONFIG_KEY_CHECKPOINT_ID
not in config[CONF]` - so it only ran if we were resuming from an empty
checkpoint. This check was removed in order to properly populate task
results. However, the removal of this check inadvertently applied
pending writes during manual state updates which caused issues when
forking:

- When you fork from a checkpoint by calling `update_state(config,
new_values, as_node="mynode")`, pending writes from the original
execution were being applied
- This caused stale data to leak into forked threads (eg. old tool call
results appearing in forked execution)

Changes

Removed pending writes application from `bulk_update_state` and
`abulk_update_state`:
- Still call `prepare_next_tasks` to discover task IDs, but skip the
code that applies null writes and regular pending writes

Tests

- Added `test_fork_does_not_apply_pending_writes` for sync and async
which verifies forking doesn't include stale pending writes from
original execution
2025-11-04 14:38:40 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
575401c78c chore(deps): bump hono from 4.9.7 to 4.10.3 in /docs/_scripts/js_translation/codeblocks (#6339)
Bumps [hono](https://github.com/honojs/hono) from 4.9.7 to 4.10.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/honojs/hono/releases">hono's
releases</a>.</em></p>
<blockquote>
<h2>v4.10.3</h2>
<h2>Securiy Fix</h2>
<p>A security issue in the CORS middleware has been fixed. In some
cases, a request header could affect the Vary response header. Please
update to the latest version if you are using the CORS middleware.</p>
<h2>What's Changed</h2>
<ul>
<li>fix(aws-lambda): serve microsoft office files as binary in lambda
handler by <a
href="https://github.com/matthiasfeist"><code>@​matthiasfeist</code></a>
in <a
href="https://redirect.github.com/honojs/hono/pull/4469">honojs/hono#4469</a></li>
<li>fix(request-id): validation accepts <code>=</code> by <a
href="https://github.com/ryuapp"><code>@​ryuapp</code></a> in <a
href="https://redirect.github.com/honojs/hono/pull/4478">honojs/hono#4478</a></li>
<li>refactor(jwt): reduce the size of the code generated by minification
by <a href="https://github.com/usualoma"><code>@​usualoma</code></a> in
<a
href="https://redirect.github.com/honojs/hono/pull/4480">honojs/hono#4480</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/matthiasfeist"><code>@​matthiasfeist</code></a>
made their first contribution in <a
href="https://redirect.github.com/honojs/hono/pull/4469">honojs/hono#4469</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/honojs/hono/compare/v4.10.2...v4.10.3">https://github.com/honojs/hono/compare/v4.10.2...v4.10.3</a></p>
<h2>v4.10.2</h2>
<h2>Security hardening improvement</h2>
<p>If you are using JWT middleware, please read the following and
consider applying the configuration.</p>
<h3>Improper Authorization in Hono (JWT Audience Validation)</h3>
<p>Hono’s JWT authentication middleware did not validate the aud
(Audience) claim by default. As a result, applications using the
middleware without an explicit audience check could accept tokens
intended for other audiences, leading to potential cross-service access
(token mix-up).</p>
<p>The issue is addressed by adding a new <code>verification.aud</code>
configuration option to allow RFC 7519–compliant audience validation.
This change is classified as a security hardening improvement, but the
lack of validation can still be considered a vulnerability in
deployments that rely on default JWT verification.</p>
<h3>Recommended secure configuration</h3>
<p>You can enable RFC 7519–compliant audience validation using the new
<code>verification.aud</code> option:</p>
<pre lang="ts"><code>import { Hono } from 'hono'
import { jwt } from 'hono/jwt'
<p>const app = new Hono()</p>
<p>app.use(<br />
'/api/*',<br />
jwt({<br />
secret: 'my-secret',<br />
verification: {<br />
// Require this API to only accept tokens with aud = 'service-a'<br />
aud: 'service-a',<br />
},<br />
})<br />
)<br />
</code></pre></p>
<h2>What's Changed</h2>
<ul>
<li>tests: Fix test case of handlers without a path by <a
href="https://github.com/IAmSSH"><code>@​IAmSSH</code></a> in <a
href="https://redirect.github.com/honojs/hono/pull/4472">honojs/hono#4472</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/honojs/hono/commit/fcefd50c65144eda31e2bc6752c81904171d9629"><code>fcefd50</code></a>
4.10.3</li>
<li><a
href="https://github.com/honojs/hono/commit/95ae4d372119cddba32e4935d2bbc6f4e2768dab"><code>95ae4d3</code></a>
refactor(jwt): reduce the size of the code generated by minification (<a
href="https://redirect.github.com/honojs/hono/issues/4480">#4480</a>)</li>
<li><a
href="https://github.com/honojs/hono/commit/d9b8b4b73b4f997994f2764013207365fe711282"><code>d9b8b4b</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/honojs/hono/commit/52161170e83298fc3d13312bfceba3992916bfa2"><code>5216117</code></a>
fix(request-id): validation accepts <code>=</code> (<a
href="https://redirect.github.com/honojs/hono/issues/4478">#4478</a>)</li>
<li><a
href="https://github.com/honojs/hono/commit/253ec2857a083595e52a446694923645084e9ecd"><code>253ec28</code></a>
fix(aws-lambda): serve microsoft office files as binary in lambda
handler (<a
href="https://redirect.github.com/honojs/hono/issues/4">#4</a>...</li>
<li><a
href="https://github.com/honojs/hono/commit/0c6455dc10db6428257bdd601eca559247e27de6"><code>0c6455d</code></a>
4.10.2</li>
<li><a
href="https://github.com/honojs/hono/commit/45ba3bf9e3dff8e4bd85d6b47d4b71c8d6c66bef"><code>45ba3bf</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/honojs/hono/commit/4cbad8b3e2a67d77849710ec400d9de020c435fd"><code>4cbad8b</code></a>
tests: Fix test case of handlers without a path (<a
href="https://redirect.github.com/honojs/hono/issues/4472">#4472</a>)</li>
<li><a
href="https://github.com/honojs/hono/commit/db764c2f1d8a2905d66c78c41aa47e47d3a4165d"><code>db764c2</code></a>
4.10.1</li>
<li><a
href="https://github.com/honojs/hono/commit/8774bf9a59278a9593d5e91cc85543d5a4bb518c"><code>8774bf9</code></a>
fix(types): cannot <code>.use</code> non-return mw from
<code>createMiddleware</code> (<a
href="https://redirect.github.com/honojs/hono/issues/4465">#4465</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/honojs/hono/compare/v4.9.7...v4.10.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=hono&package-manager=npm_and_yarn&previous-version=4.9.7&new-version=4.10.3)](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>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-04 13:52:15 -08:00
William FHandGitHub 092c9ecde6 chore: update ormsgpack minbound and add OPT_REPLACE_SURROGATES (#6395)
This lets the default `msgpack` serialization mode handle more cases
where user data contains invalid unicode.
2025-11-04 13:51:34 -08:00
William FHandGitHub 8336686e89 release(cli): 0.4.7 expand api bounds (#6390)
Fixes #6380
2025-11-03 23:47:05 +00:00
Parker J. RuleandGitHub 8b080a091e fix(checkpoint): update checkpoint interface specification in README (#6386)
This syncs the checkpoint interface specification with the base class
(`BaseCheckpointSaver`) in
`langgraph/libs/checkpoint/langgraph/checkpoint/base /__init__.py`.
2025-11-03 21:44:41 +00:00
Asamu DavidandGitHub 1157ec77b4 fix(cli): add buildkit syntax directiive, update tests (#6385)
**Description:** Adds the syntax directive to generated dockerfile for
langgraph builds if we have additional contexts
**Issue:** fixes issue with python monorepo builds failing
**Dependencies:** N/A
2025-11-03 18:13:19 +00:00
William FHandGitHub a10a66cbd1 chore: Update cli config schema (#6372) 2025-11-01 09:50:46 -07:00
Kathryn MayandGitHub ae525fb74f docs: Update hosting page name redirect to platform setup (#6371) 2025-10-31 14:12:59 -04:00
Mason DaughertyandGitHub a6dde39be7 chore: style fixes for refs (#6365) 2025-10-30 17:59:40 -04:00
Sydney RunkleandGitHub c1661dd07f chore: bump prebuilt dep for lg (#6361) 2025-10-29 18:34:28 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2d848ffddd chore(deps): bump actions/upload-artifact from 4 to 5 (#6347)
Bumps
[actions/upload-artifact](https://github.com/actions/upload-artifact)
from 4 to 5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/upload-artifact/releases">actions/upload-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<p><strong>BREAKING CHANGE:</strong> this update supports Node
<code>v24.x</code>. This is not a breaking change per-se but we're
treating it as such.</p>
<ul>
<li>Update README.md by <a
href="https://github.com/GhadimiR"><code>@​GhadimiR</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/681">actions/upload-artifact#681</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/712">actions/upload-artifact#712</a></li>
<li>Readme: spell out the first use of GHES by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/727">actions/upload-artifact#727</a></li>
<li>Update GHES guidance to include reference to Node 20 version by <a
href="https://github.com/patrikpolyak"><code>@​patrikpolyak</code></a>
in <a
href="https://redirect.github.com/actions/upload-artifact/pull/725">actions/upload-artifact#725</a></li>
<li>Bump <code>@actions/artifact</code> to <code>v4.0.0</code></li>
<li>Prepare <code>v5.0.0</code> by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/upload-artifact/pull/734">actions/upload-artifact#734</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/GhadimiR"><code>@​GhadimiR</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/681">actions/upload-artifact#681</a></li>
<li><a href="https://github.com/nebuk89"><code>@​nebuk89</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/712">actions/upload-artifact#712</a></li>
<li><a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/727">actions/upload-artifact#727</a></li>
<li><a
href="https://github.com/patrikpolyak"><code>@​patrikpolyak</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/725">actions/upload-artifact#725</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v4...v5.0.0">https://github.com/actions/upload-artifact/compare/v4...v5.0.0</a></p>
<h2>v4.6.2</h2>
<h2>What's Changed</h2>
<ul>
<li>Update to use artifact 2.3.2 package &amp; prepare for new
upload-artifact release by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/685">actions/upload-artifact#685</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/685">actions/upload-artifact#685</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v4...v4.6.2">https://github.com/actions/upload-artifact/compare/v4...v4.6.2</a></p>
<h2>v4.6.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Update to use artifact 2.2.2 package by <a
href="https://github.com/yacaovsnc"><code>@​yacaovsnc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/673">actions/upload-artifact#673</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v4...v4.6.1">https://github.com/actions/upload-artifact/compare/v4...v4.6.1</a></p>
<h2>v4.6.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Expose env vars to control concurrency and timeout by <a
href="https://github.com/yacaovsnc"><code>@​yacaovsnc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/662">actions/upload-artifact#662</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v4...v4.6.0">https://github.com/actions/upload-artifact/compare/v4...v4.6.0</a></p>
<h2>v4.5.0</h2>
<h2>What's Changed</h2>
<ul>
<li>fix: deprecated <code>Node.js</code> version in action by <a
href="https://github.com/hamirmahal"><code>@​hamirmahal</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/578">actions/upload-artifact#578</a></li>
<li>Add new <code>artifact-digest</code> output by <a
href="https://github.com/bdehamer"><code>@​bdehamer</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/656">actions/upload-artifact#656</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/hamirmahal"><code>@​hamirmahal</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/upload-artifact/pull/578">actions/upload-artifact#578</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/upload-artifact/commit/330a01c490aca151604b8cf639adc76d48f6c5d4"><code>330a01c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/734">#734</a>
from actions/danwkennedy/prepare-5.0.0</li>
<li><a
href="https://github.com/actions/upload-artifact/commit/03f282445299bbefc96171af272a984663b63a26"><code>03f2824</code></a>
Update <code>github.dep.yml</code></li>
<li><a
href="https://github.com/actions/upload-artifact/commit/905a1ecb5915b264cbc519e4eb415b5d82916018"><code>905a1ec</code></a>
Prepare <code>v5.0.0</code></li>
<li><a
href="https://github.com/actions/upload-artifact/commit/2d9f9cdfa99fedaddba68e9b5b5c281eca26cc63"><code>2d9f9cd</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/725">#725</a>
from patrikpolyak/patch-1</li>
<li><a
href="https://github.com/actions/upload-artifact/commit/9687587dec67f2a8bc69104e183d311c42af6d6f"><code>9687587</code></a>
Merge branch 'main' into patch-1</li>
<li><a
href="https://github.com/actions/upload-artifact/commit/2848b2cda0e5190984587ec6bb1f36730ca78d50"><code>2848b2c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/727">#727</a>
from danwkennedy/patch-1</li>
<li><a
href="https://github.com/actions/upload-artifact/commit/9b511775fd9ce8c5710b38eea671f856de0e70a7"><code>9b51177</code></a>
Spell out the first use of GHES</li>
<li><a
href="https://github.com/actions/upload-artifact/commit/cd231ca1eda77976a84805c4194a1954f56b0727"><code>cd231ca</code></a>
Update GHES guidance to include reference to Node 20 version</li>
<li><a
href="https://github.com/actions/upload-artifact/commit/de65e23aa2b7e23d713bb51fbfcb6d502f8667d8"><code>de65e23</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/712">#712</a>
from actions/nebuk89-patch-1</li>
<li><a
href="https://github.com/actions/upload-artifact/commit/8747d8cd7632611ad6060b528f3e0f654c98869c"><code>8747d8c</code></a>
Update README.md</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/upload-artifact/compare/v4...v5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=4&new-version=5)](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)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 10:03:28 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4c8d965710 chore(deps): bump actions/download-artifact from 5 to 6 (#6348)
Bumps
[actions/download-artifact](https://github.com/actions/download-artifact)
from 5 to 6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/download-artifact/releases">actions/download-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.0</h2>
<h2>What's Changed</h2>
<p><strong>BREAKING CHANGE:</strong> this update supports Node
<code>v24.x</code>. This is not a breaking change per-se but we're
treating it as such.</p>
<ul>
<li>Update README for download-artifact v5 changes by <a
href="https://github.com/yacaovsnc"><code>@​yacaovsnc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/417">actions/download-artifact#417</a></li>
<li>Update README with artifact extraction details by <a
href="https://github.com/yacaovsnc"><code>@​yacaovsnc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/424">actions/download-artifact#424</a></li>
<li>Readme: spell out the first use of GHES by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/download-artifact/pull/431">actions/download-artifact#431</a></li>
<li>Bump <code>@actions/artifact</code> to <code>v4.0.0</code></li>
<li>Prepare <code>v6.0.0</code> by <a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a> in
<a
href="https://redirect.github.com/actions/download-artifact/pull/438">actions/download-artifact#438</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/danwkennedy"><code>@​danwkennedy</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/download-artifact/pull/431">actions/download-artifact#431</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/download-artifact/compare/v5...v6.0.0">https://github.com/actions/download-artifact/compare/v5...v6.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/download-artifact/commit/018cc2cf5baa6db3ef3c5f8a56943fffe632ef53"><code>018cc2c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/438">#438</a>
from actions/danwkennedy/prepare-6.0.0</li>
<li><a
href="https://github.com/actions/download-artifact/commit/815651c680ffe1c95719d0ed08aba1a2f9d5c177"><code>815651c</code></a>
Revert &quot;Remove <code>github.dep.yml</code>&quot;</li>
<li><a
href="https://github.com/actions/download-artifact/commit/bb3a066a8babc8ed7b3e4218896c548fe34e7115"><code>bb3a066</code></a>
Remove <code>github.dep.yml</code></li>
<li><a
href="https://github.com/actions/download-artifact/commit/fa1ce46bbd11b8387539af12741055a76dfdf804"><code>fa1ce46</code></a>
Prepare <code>v6.0.0</code></li>
<li><a
href="https://github.com/actions/download-artifact/commit/4a24838f3d5601fd639834081e118c2995d51e1c"><code>4a24838</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/431">#431</a>
from danwkennedy/patch-1</li>
<li><a
href="https://github.com/actions/download-artifact/commit/5e3251c4ff5a32e4cf8dd4adaee0e692365237ae"><code>5e3251c</code></a>
Readme: spell out the first use of GHES</li>
<li><a
href="https://github.com/actions/download-artifact/commit/abefc31eafcfbdf6c5336127c1346fdae79ff41c"><code>abefc31</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/424">#424</a>
from actions/yacaovsnc/update_readme</li>
<li><a
href="https://github.com/actions/download-artifact/commit/ac43a6070aa7db8a41e756e7a2846221edca7027"><code>ac43a60</code></a>
Update README with artifact extraction details</li>
<li><a
href="https://github.com/actions/download-artifact/commit/de96f4613b77ec03b5cf633e7c350c32bd3c5660"><code>de96f46</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/417">#417</a>
from actions/yacaovsnc/update_readme</li>
<li><a
href="https://github.com/actions/download-artifact/commit/7993cb44e9052f2f08f9b828ae5ef3ecca7d2ac7"><code>7993cb4</code></a>
Remove migration guide for artifact download changes</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/download-artifact/compare/v5...v6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/download-artifact&package-manager=github_actions&previous-version=5&new-version=6)](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)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 10:03:18 -07:00
Sydney RunkleandGitHub 4ac1c628ee chore: port tool node improvements back to langgraph (#6321)
namespace decisions

```
langgraph.prebuilt
  ├── ToolRuntime  # new
# all of the other stuff that was already there

langgraph.prebuilt.tool_node
  ├── ToolNode
  ├── ToolCallRequest  # new
  ├── ToolRuntime  # new
  ├── InjectedState
  ├── InjectedStore
  ├── ToolCallWrapper
  ├── AsyncToolCallWrapper
  ├── tools_condition
```
```
langchain.tools
  ├── ToolRuntime  # now from langgraph.prebuilt
  ├── InjectedState  # now from langgraph.prebuilt
  ├── InjectedStore  # now from langgraph.prebuilt
  ├── ToolException
  ├── tool
  ├── BaseTool
  ├── InjectedToolArg
  ├── InjectedToolCallId
```
2025-10-29 09:58:06 -07:00
Sydney RunkleandGitHub 41f8e61589 chore: bump core dep for prebuilt (#6323)
bumping core dependency for `langgraph-prebuilt` to `>1.0.0` so that we
can take advantage of internal utils that allow `ToolRuntime` injection.

We were previously bumping the version in lock step with prebuilt (prev
version was 0.3.67), so this pattern is in line with that.

Also updating snapshots accordingly:
* New mermaid syntax for a few graphs
* Removal of `examples` from `AIMessage`
2025-10-29 09:51:51 -07:00
Lauren Hirata SinghandGitHub 57a877279d chore(docs): Revert the revert "fix: rollback doc redirects" (add redirects) (#6353)
Reverts langchain-ai/langgraph#6301

This adds redirects back
2025-10-29 12:12:06 -04:00
Caspar BroekhuizenandGitHub d6dea53323 fix(langgraph): dont persist UntrackedValue (#6316)
UntrackedValue is a special channel type where the values in it are not
persisted to memory. Our v1 create_agent middleware used UntrackedValue
in middleware (e.g. ShellToolMiddleware) for some cool features like
temp files.

If a user has elected to use a checkpointer, we normally enforce that
the values they write to channels are serializable. However, this
doesn't make sense to enforce for UntrackedValues because the contract
is they're never written to checkpoint - so the user should not be
forced to make the contents of the channel serializable

However when using a checkpointer and durability sync/async, we found
that writes would still be persisted that contained UntrackedValue
contents in two forms:
a) UntrackedValue channel objects 
b) Send objects - in the state passed to another node

Patched this in put_writes by a) skipping persisting writes to
UntrackedValue channels altogether and b) popping all UntrackedValue kv
pairs nested within Send packets. We also need to sanitize in
_put_checkpoint which is called when durability=="exit".

Added a basic test for UntrackedValue in test_channel.py and added more
comprehensive tests using Send under some different scenarios in
test_pregel.py
2025-10-28 16:40:22 -07:00
Caspar BroekhuizenandGitHub 504e91ad5a feat(langgraph): add Overwrite to bypass reducer (#6286)
See https://github.com/langchain-ai/langgraph/pull/6277

Adds langgraph.types.Overwrite, a deterministic way to bypass a reducer.
When encountering a value wrapped with Overwrite,
BinaryOperatorAggregate overwrites the channel value.

<img width="227" height="329" alt="image"
src="https://github.com/user-attachments/assets/f2136117-9aa3-4246-863d-d5df0e7d1df1"
/>

If either node_b or node_c overwrite (but not both), then at END the
channel is equal to the value node_b or node_c wrote. Order of execution
doesn't matter because once an Overwrite value is encountered, regular
values are ignored (self.operator is not called for the rest of the
update)

If multiple nodes overwrite in the same superstep then
InvalidUpdateError is thrown

Usage
```python
from langgraph.types import Overwrite

def node_b(state:State):
    return {"messages": Overwrite(["b"])}
```
or
``` python
def node_b(state:State):
    return {"messages": {"__overwrite__": ["b"]}}
```
2025-10-27 11:31:35 -07:00
Mason DaughertyandGitHub 10abf2deb1 fix: replace python.langchain links with new docs.langchain (#6352) 2025-10-27 13:36:12 -04:00
Parker J. RuleandGitHub 5796ca9a0a fix(sdk-py): refine body param type (Auth.authenticate) (#6322)
Requests are not guaranteed to contain a body, and a request's body is
not guaranteed to be valid JSON. 

This updates the type signature for authentication handlers 
to account for these scenarios.
2025-10-22 17:35:15 +00:00
William FHandGitHub fca3e4513c release: Checkpointers 3.0 (#6313)
In this PR:

- Bump `langgraph-checkpoint` to 3.0
- Bump `langgraph-checkpoint-sqlite` to 3.0; Update
`langgraph-checkpoint` deps to >=3,<4
- Bump `langgraph-checkpoint-postgres` to 3.0; Update
`langgraph-checkpoint` max to <4 (keep prior min since the deprecated
functionality wasn't explicitly used)
- Bump `langgraph` to 1.0.1; update `langgraph-checkpoint` max bound to
4
- Bump `prebuilt` to 1.0.1; update `langgraph-checkpoint` max bound to 4
2025-10-20 11:31:55 -07:00
c5744f583b chore: Restrict "json" type deserialization (#6269)
- Rm untyped loads/dumps
- Restrict to an allow list

---------

Co-authored-by: Sydney Runkle <sydneymarierunkle@gmail.com>
2025-10-20 10:18:36 -07:00
Hunter LovellandGitHub d298b489b4 fix: rollback doc redirects (#6301) 2025-10-17 16:54:42 -04:00
Sydney RunkleandGitHub c4144bb48f release: langgraph + langgraph-prebuilt v1.0.0 (#6300) 2025-10-17 19:15:29 +00:00
Sydney RunkleandGitHub 2c3e380a35 feat: adding cursory Python 3.14 support (#6298)
* catching error thrown by asyncio
* using 2nd check for annotations given Pydantic 2.12 changes
* skipping tests for remote graph bc langgraph-api is dependent on
`jsonschema-rs`
* skipping tests w/ pydantic v1 models

```bash
hint: This usually indicates a problem with the package or the build environment.
  help: `jsonschema-rs` (v0.29.1) was included because `langgraph:dev` (v1.0.0rc1) depends on `langgraph-cli[inmem]` which
        depends on `langgraph-api` (v0.4.29) which depends on `jsonschema-rs`
```

not yet testing for free threaded python, that'll be much more involved!

ended up separating lint / testing deps during this process bc I was
getting a ton of not required deps while testing that were complicating
things :/
2025-10-17 08:26:52 -04:00
Lauren Hirata SinghandGitHub cf39fa5a91 fix(docs): fix catchall redirect (#6299) 2025-10-17 07:27:41 -04:00
Mason DaughertyandGitHub 7e666b58cd style: fixes for ref docs (#6297) 2025-10-16 20:58:16 -04:00
140 changed files with 16378 additions and 6820 deletions
+6
View File
@@ -0,0 +1,6 @@
# Contributing to LangGraph
Hi there! Thank you for even being interested in contributing to LangGraph.
As an open-source project in a rapidly developing field, we are extremely open to contributions, whether they involve new features, improved infrastructure, better documentation, or bug fixes.
To learn how to contribute to LangGraph, please follow the [contribution guide here](https://docs.langchain.com/oss/python/contributing).
+5 -5
View File
@@ -1,21 +1,21 @@
name: "\U0001F41B Bug Report"
description: Report a bug in LangGraph. To report a security issue, please instead use the security option below. For questions, please use the LangChain Forum at forum.langchain.com.
labels: [pending,bug]
labels: [pending, bug]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to file a bug report.
Use this to report BUGS in LangGraph. For usage questions, feature requests and general design questions, please use the [LangChain Forum](https://forum.langchain.com/).
Relevant links to check before filing a bug report to see if your issue has already been reported, fixed or
if there's another way to solve your problem:
* [LangChain Forum](https://forum.langchain.com/),
* [LangGraph Github Issues](https://github.com/langchain-ai/langgraph/issues),
* [LangGraph how-to guides](https://langchain-ai.github.io/langgraph/how-tos/).
* [LangChain documentation with the integrated search](https://python.langchain.com/docs/get_started/introduction),
* [LangChain documentation with the integrated search](https://docs.langchain.com/),
* [GitHub search](https://github.com/langchain-ai/langgraph),
- type: checkboxes
id: checks
+1 -1
View File
@@ -38,7 +38,7 @@ def main():
tree = ast.parse(file.read())
classes = find_classes(tree)
def is_sync(class_spec: Tuple[str, List[str]]) -> bool:
return class_spec[0].startswith("Sync")
+37 -13
View File
@@ -1,7 +1,8 @@
import logging
import pathlib
import sys
import time
from urllib import request, error
from urllib import error, request
import langgraph_cli
import langgraph_cli.config
@@ -11,9 +12,13 @@ from langgraph_cli.constants import DEFAULT_PORT
from langgraph_cli.exec import Runner, subp_exec
from langgraph_cli.progress import Progress
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
def test(config: pathlib.Path, port: int, tag: str, verbose: bool):
"""Spin up API with Postgres/Redis via docker compose and wait until ready."""
logger.info("Starting test...")
with Runner() as runner, Progress(message="Pulling...") as set:
# Detect docker/compose capabilities
capabilities = langgraph_cli.docker.check_capabilities(runner)
@@ -57,7 +62,9 @@ def test(config: pathlib.Path, port: int, tag: str, verbose: bool):
sys.stderr.write(f"docker compose up failed: {e}\n")
try:
sys.stderr.write("\n== docker compose ps ==\n")
runner.run(subp_exec(*compose_cmd, *args, "ps", input=stdin, verbose=False))
runner.run(
subp_exec(*compose_cmd, *args, "ps", input=stdin, verbose=False)
)
except Exception:
pass
try:
@@ -93,7 +100,7 @@ def test(config: pathlib.Path, port: int, tag: str, verbose: bool):
set("")
base_url = f"http://localhost:{port}"
ok_url = f"{base_url}/ok"
print(f"Waiting for {ok_url} to respond with 200...")
logger.info(f"Waiting for {ok_url} to respond with 200...")
deadline = time.time() + 30
last_err: Exception | None = None
while time.time() < deadline:
@@ -107,13 +114,16 @@ def test(config: pathlib.Path, port: int, tag: str, verbose: bool):
break
else:
last_err = RuntimeError(f"Unexpected status: {resp.status}")
print(f"Unexpected status: {resp.status}")
logger.error(f"Unexpected status: {resp.status}")
except error.URLError as e:
logger.error(f"URLError: {e}")
last_err = e
except Exception as e: # noqa: BLE001
logger.error(f"Exception: {e}")
last_err = e
time.sleep(0.5)
else:
logger.error("Timeout waiting for /ok to return 200")
# Bring stack down before raising
args_down = [*args, "down", "-v", "--remove-orphans"]
try:
@@ -131,15 +141,23 @@ def test(config: pathlib.Path, port: int, tag: str, verbose: bool):
)
# Clean up: bring compose stack down to free ports for next test
args_down = [*args, "down", "-v", "--remove-orphans"]
runner.run(
subp_exec(
*compose_cmd,
*args_down,
input=stdin,
verbose=verbose,
logger.info("Test succeeded. Bringing down compose stack...")
try:
args_down = [*args, "down", "-v", "--remove-orphans"]
runner.run(
subp_exec(
*compose_cmd,
*args_down,
input=stdin,
verbose=verbose,
)
)
)
logger.info("Compose stack down. Finishing...")
except Exception:
logger.exception("Failed to bring down compose stack")
pass
logger.info("Test finished")
if __name__ == "__main__":
@@ -150,4 +168,10 @@ if __name__ == "__main__":
parser.add_argument("-c", "--config", type=str, default="./langgraph.json")
parser.add_argument("-p", "--port", type=int, default=DEFAULT_PORT)
args = parser.parse_args()
test(pathlib.Path(args.config), args.port, args.tag, verbose=True)
try:
test(pathlib.Path(args.config), args.port, args.tag, verbose=True)
except BaseException:
logger.exception("Test failed")
raise
logger.info("Test execution finished")
+17 -9
View File
@@ -13,7 +13,7 @@ jobs:
matrix:
python-version:
- "3.10"
- "3.11"
- "3.14"
example:
- name: A
workdir: libs/cli/examples
@@ -32,7 +32,7 @@ jobs:
run:
working-directory: libs/cli
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Get changed files
id: changed-files
uses: Ana06/get-changed-files@v2.3.0
@@ -66,19 +66,19 @@ jobs:
timeout 60 python "$REPO_ROOT/.github/scripts/run_langgraph_cli_test.py" -t ${{ matrix.example.tag }}
- name: Build JS service
if: steps.changed-files.outputs.all
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
working-directory: libs/cli/js-examples
run: |
langgraph build -t langgraph-test-e
- name: Build JS monorepo service
if: steps.changed-files.outputs.all
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
working-directory: libs/cli/js-monorepo-example
run: |
langgraph build -t langgraph-test-f -c apps/agent/langgraph.json --build-command "yarn run turbo build" --install-command "yarn install"
- name: Build Python monorepo service
if: steps.changed-files.outputs.all
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
working-directory: libs/cli/python-monorepo-example
run: |
langgraph build -t langgraph-test-g -c apps/agent/langgraph.json
@@ -87,24 +87,32 @@ jobs:
timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-g -c apps/agent/langgraph.json
- name: Build and test prerelease reqs service
if: steps.changed-files.outputs.all
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
working-directory: libs/cli/examples/graph_prerelease_reqs
run: |
langgraph build -t langgraph-test-h
cp ../.env.example .env
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-h
echo "Finished starting up langgraph-test-h"
LANGGRAPH_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h python -c "import sys; from importlib.metadata import version; v = version('langgraph'); print(v);")
if [ "$LANGGRAPH_VERSION" != "1.0.0a2" ]; then
if [ "$LANGGRAPH_VERSION" != "1.0.2" ]; then
echo "LANGGRAPH_VERSION != 1.0.2; $LANGGRAPH_VERSION"
exit 1
fi
LANGCHAIN_OPENAI_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h python -c "import sys; from importlib.metadata import version; v = version('langchain-openai'); print(v);")
if [ "$LANGCHAIN_OPENAI_VERSION" != "0.3.0" ]; then
if [ "$LANGCHAIN_OPENAI_VERSION" != "1.0.1" ]; then
echo "LANGCHAIN_OPENAI_VERSION != 1.0.1; $LANGCHAIN_OPENAI_VERSION"
exit 1
fi
LANGCHAIN_ANTHROPIC_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h python -c "import sys; from importlib.metadata import version; v = version('langchain-anthropic'); print(v);")
if [ "$LANGCHAIN_ANTHROPIC_VERSION" != "1.0.0a5" ]; then
echo "LANGCHAIN_ANTHROPIC_VERSION != 1.0.0a5; $LANGCHAIN_ANTHROPIC_VERSION"
exit 1
fi
- name: Build and test prerelease reqs fail service
if: steps.changed-files.outputs.all
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
working-directory: libs/cli/examples/graph_prerelease_reqs_fail
run: |
langgraph build -t langgraph-test-i || [ $? -eq 1 ]
+3 -3
View File
@@ -31,7 +31,7 @@ jobs:
- "3.12"
name: "lint #${{ matrix.python-version }}"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Get changed files
id: changed-files
uses: Ana06/get-changed-files@v2.3.0
@@ -48,7 +48,7 @@ jobs:
- name: Install dependencies
if: steps.changed-files.outputs.all
working-directory: ${{ inputs.working-directory }}
run: uv sync --frozen --group dev
run: uv sync --frozen --group lint
- name: Get .mypy_cache to speed up mypy
if: steps.changed-files.outputs.all
@@ -74,7 +74,7 @@ jobs:
- name: Install test dependencies
if: steps.changed-files.outputs.all
working-directory: ${{ inputs.working-directory }}
run: uv sync --group dev
run: uv sync --group lint
- name: Get .mypy_cache_test to speed up mypy
if: steps.changed-files.outputs.all
+3 -2
View File
@@ -21,10 +21,11 @@ jobs:
- "3.11"
- "3.12"
- "3.13"
- "3.14"
name: "test #${{ matrix.python-version }}"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v7
with:
@@ -41,7 +42,7 @@ jobs:
- name: Install dependencies
shell: bash
working-directory: ${{ inputs.working-directory }}
run: uv sync --frozen --group dev
run: uv sync --frozen --group test --no-dev
- name: Run tests
shell: bash
+3 -2
View File
@@ -16,13 +16,14 @@ jobs:
- "3.11"
- "3.12"
- "3.13"
- "3.14"
defaults:
run:
working-directory: libs/langgraph
name: "test #${{ matrix.python-version }}"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v7
with:
@@ -38,7 +39,7 @@ jobs:
- name: Install dependencies
shell: bash
run: uv sync --frozen --group dev
run: uv sync --frozen --group test --no-dev
- name: Run tests
shell: bash
+4 -4
View File
@@ -23,7 +23,7 @@ jobs:
version: ${{ steps.check-version.outputs.version }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python $${ env.PYTHON_VERSION }}
uses: astral-sh/setup-uv@v7
@@ -48,7 +48,7 @@ jobs:
working-directory: ${{ inputs.working-directory }}
- name: Upload build
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: test-dist
path: ${{ inputs.working-directory }}/dist/
@@ -74,9 +74,9 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v6
with:
name: test-dist
path: ${{ inputs.working-directory }}/dist/
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
run:
working-directory: libs/langgraph
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- run: SHA=$(git rev-parse HEAD) && echo "SHA=$SHA" >> $GITHUB_ENV
- name: Set up Python 3.11
uses: astral-sh/setup-uv@v7
@@ -26,7 +26,7 @@ jobs:
enable-cache: true
cache-suffix: "bench"
- name: Install dependencies
run: uv sync --group dev
run: uv sync --group test
- name: Run benchmarks
run: OUTPUT=out/benchmark-baseline.json make -s benchmark
- name: Save outputs
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
run:
working-directory: libs/langgraph
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- id: files
name: Get changed files
uses: Ana06/get-changed-files@v2.3.0
@@ -28,7 +28,7 @@ jobs:
enable-cache: true
cache-suffix: "bench"
- name: Install dependencies
run: uv sync --group dev
run: uv sync --group test
- name: Download baseline
uses: actions/cache/restore@v4
with:
+5 -5
View File
@@ -27,7 +27,7 @@ jobs:
python: ${{ steps.filter.outputs.python }}
deps: ${{ steps.filter.outputs.deps }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
id: filter
with:
@@ -100,7 +100,7 @@ jobs:
name: "Check SDK methods matching"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -116,13 +116,13 @@ jobs:
strategy:
matrix:
python-version:
- "3.11"
- "3.13"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v7
with:
python-version: "3.11"
python-version: "3.13"
enable-cache: true
cache-suffix: "schema-check-cli"
- name: Install CLI dependencies
+1 -1
View File
@@ -21,7 +21,7 @@
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install Dependencies
run: |
-72
View File
@@ -1,72 +0,0 @@
name: Deploy Docs Redirects
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
defaults:
run:
working-directory: docs
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 10 # Job will be cancelled if it runs for more than 10 minutes
env:
GITHUB_TOKEN: ${{ secrets.MKDOCS_GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Python
uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
enable-cache: true
cache-suffix: "docs"
- name: Install dependencies
run: |
yarn
uv sync --all-groups
# we run this installation only for internal PRs
# as GITHUB_TOKEN is not available for PRs from outside contributors
if [ -n "${GITHUB_TOKEN}" ]; then
uv run pip install "git+https://${GITHUB_TOKEN}@github.com/langchain-ai/mkdocs-material-insiders.git"
fi
- name: Build llms-text
run: make llms-text
- name: Build site (redirects only)
run: make build-docs
env:
OPENAI_API_KEY: sf-proj-1234567890 # fake placeholder, shouldn't actually be used
ANTHROPIC_API_KEY: sk-ant-api03-1234567890 # fake placeholder, shouldn't actually be used
- name: Configure GitHub Pages
if: github.ref == 'refs/heads/main'
uses: actions/configure-pages@v5
- name: Upload Pages Artifact
if: github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v4
with:
path: ./docs/site/
- name: Deploy to GitHub Pages
if: github.ref == 'refs/heads/main'
id: deployment
uses: actions/deploy-pages@v4
+2 -2
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -36,7 +36,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 1
+10 -9
View File
@@ -25,7 +25,7 @@ jobs:
tag: ${{ steps.check-version.outputs.tag }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python
uses: astral-sh/setup-uv@v7
@@ -50,7 +50,7 @@ jobs:
working-directory: ${{ inputs.working-directory }}
- name: Upload build
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: dist
path: ${{ inputs.working-directory }}/dist/
@@ -86,7 +86,7 @@ jobs:
outputs:
release-body: ${{ steps.generate-release-body.outputs.release-body }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
repository: langchain-ai/langgraph
path: langgraph
@@ -157,7 +157,7 @@ jobs:
- test-pypi-publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
# We explicitly *don't* set up caching here. This ensures our tests are
# maximally sensitive to catching breakage.
@@ -221,7 +221,7 @@ jobs:
uv run python -c "import $IMPORT_NAME; print(dir($IMPORT_NAME))"
- name: Import test dependencies
run: uv sync --group dev
run: uv sync --group test
working-directory: ${{ inputs.working-directory }}
# Overwrite the local version of the package with the test PyPI version.
@@ -260,7 +260,7 @@ jobs:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python
uses: astral-sh/setup-uv@v7
@@ -269,7 +269,7 @@ jobs:
enable-cache: true
cache-suffix: "release"
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v6
with:
name: dist
path: ${{ inputs.working-directory }}/dist/
@@ -301,7 +301,7 @@ jobs:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python
uses: astral-sh/setup-uv@v7
@@ -310,7 +310,7 @@ jobs:
enable-cache: true
cache-suffix: "release"
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v6
with:
name: dist
path: ${{ inputs.working-directory }}/dist/
@@ -322,5 +322,6 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
generateReleaseNotes: false
tag: ${{needs.build.outputs.tag}}
name: ${{ needs.build.outputs.pkg-name }}==${{ needs.build.outputs.version }}
body: ${{ needs.release-notes.outputs.release-body }}
commit: ${{ github.sha }}
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
- "latest"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python + Poetry
uses: astral-sh/setup-uv@v7
with:
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up uv
uses: astral-sh/setup-uv@v7
+55
View File
@@ -0,0 +1,55 @@
# AGENTS Instructions
This repository is a monorepo. Each library lives in a subdirectory under `libs/`.
When you modify code in any library, run the following commands in that library's directory before creating a pull request:
- `make format` run code formatters
- `make lint` run the linter
- `make test` execute the test suite
To run a particular test file or to pass additional pytest options you can specify the `TEST` variable:
```
TEST=path/to/test.py make test
```
Other pytest arguments can also be supplied inside the `TEST` variable.
## Libraries
The repository contains several Python and JavaScript/TypeScript libraries.
Below is a high-level overview:
- **checkpoint** base interfaces for LangGraph checkpointers.
- **checkpoint-postgres** Postgres implementation of the checkpoint saver.
- **checkpoint-sqlite** SQLite implementation of the checkpoint saver.
- **cli** official command-line interface for LangGraph.
- **langgraph** core framework for building stateful, multi-actor agents.
- **prebuilt** high-level APIs for creating and running agents and tools.
- **sdk-js** JS/TS SDK for interacting with the LangGraph REST API.
- **sdk-py** Python SDK for the LangGraph Server API.
### Dependency map
The diagram below lists downstream libraries for each production dependency as
declared in that library's `pyproject.toml` (or `package.json`).
```text
checkpoint
├── checkpoint-postgres
├── checkpoint-sqlite
├── prebuilt
└── langgraph
prebuilt
└── langgraph
sdk-py
├── langgraph
└── cli
sdk-js (standalone)
```
Changes to a library may impact all of its dependents shown above.
-293
View File
@@ -1,293 +0,0 @@
# Contributing to LangGraph
Thank you for being interested in contributing to LangGraph!
## General guidelines
Here are some things to keep in mind for all types of contributions:
- Follow the ["fork and pull request"](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project) workflow.
- Fill out the checked-in pull request template when opening pull requests. Note related issues and tag relevant maintainers.
- Ensure your PR passes formatting, linting, and testing checks before requesting a review.
- If you would like comments or feedback, please tag a maintainer.
- Backwards compatibility is key. Your changes must not be breaking, except in case of critical bug and security fixes.
- Look for duplicate PRs or issues that have already been opened before opening a new one.
- Keep scope as isolated as possible. As a general rule, your changes should not affect more than one package at a time.
### Bugfixes
For bug fixes, please open up an issue before proposing a fix to ensure the proposal properly addresses the underlying problem. In general, bug fixes should all have an accompanying unit test that fails before the fix.
### New features
For new features, please start a new [discussion](https://forum.langchain.com/), where the maintainers will help with scoping out the necessary changes.
## Contribute Documentation
Documentation is a vital part of LangGraph. We welcome both new documentation for new features and
community improvements to our current documentation. Please read the resources below before getting started:
- [Documentation style guide](#documentation-style-guide)
- [Documentation setup](#setup)
## Documentation Style Guide
As LangGraph continues to grow, the surface area of documentation required to cover it continues to grow too.
This page provides guidelines for anyone writing documentation for LangGraph, as well as some of our philosophies around organization and structure.
## Philosophy
LangGraph's documentation follows the [Diataxis framework](https://diataxis.fr).
Under this framework, all documentation falls under one of four categories: [Tutorials](#tutorials),
[How-to guides](#how-to-guides),
[References](#references), and [Explanations (aka conceptual guides)](#conceptual-guide).
### Tutorials
Tutorials are lessons that take the reader through a practical activity. Their purpose is to help the user
gain understanding of concepts and how they interact by showing one way to achieve some goal in a hands-on way.
They should **avoid** giving
multiple permutations of ways to achieve that goal in-depth. Choice is burdensome. Instead, they should guide a new user through a recommended path to accomplishing a concrete goal. While the end result of a tutorial does not necessarily need to
be completely production-ready, it should be useful and practically satisfy the goal that you clearly stated in the tutorial's introduction.
To quote the Diataxis website:
> A tutorial serves the users *acquisition* of skills and knowledge - their study. Its purpose is not to help the user get something done, but to help them learn.
In LangGraph, these are often higher level guides that show off end-to-end use cases.
Some examples include:
- [Build a Customer Support Bot](https://langchain-ai.github.io/langgraph/tutorials/customer-support/customer-support/)
- [Build a SQL Agent](https://langchain-ai.github.io/langgraph/tutorials/sql/sql-agent/)
Here are some high-level tips on writing a good tutorial:
- Focus on guiding the user to get something done, but keep in mind the end-goal is more to impart principles than to create a perfect production system.
- Be specific, not abstract and follow one path.
- No need to go deeply into alternative approaches, but its ok to reference them, ideally with a link to an appropriate how-to guide.
- Get "a point on the board" as soon as possible - something the user can run that outputs something.
- You can iterate and expand afterwards.
- Try to frequently checkpoint at given steps where the user can run code and see progress.
- Focus on results, not technical explanation.
- Crosslink heavily to appropriate conceptual/reference pages
- The first time you mention a LangGraph concept, use its full name (e.g. "human-in-the-loop"), and link to its conceptual/other documentation page.
- It's also helpful to add a prerequisite callout that links to any pages with necessary background information.
- End with a recap/next steps section summarizing what the tutorial covered and future reading, such as related how-to guides.
- Use phrases like "Next we can run X & Y. We will expect Z.". Then afterwards, use language like "Notice Z" that recalls our expectations and directs the reader's attention to the topic we are trying to teach.
- Do not shy away from repetition.
### How-to guides
A how-to guide, as the name implies, demonstrates how to do something discrete and specific.
It should assume that the user is already familiar with underlying concepts, and is trying to solve an immediate problem, but
should still give some background or list the scenarios where the information contained within can be relevant.
They can and should discuss alternatives if one approach may be better than another in certain cases.
To quote the Diataxis website:
> A how-to guide serves the work of the already-competent user, whom you can assume to know what they want to do, and to be able to follow your instructions correctly.
Some examples include:
- [How to add persistence to your graph](https://langchain-ai.github.io/langgraph/how-tos/persistence/)
- [How to view and update past graph state](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/time-travel/)
Here are some high-level tips on writing a good how-to guide:
- Clearly explain what you are guiding the user through at the start
- Assume higher intent than a tutorial and show what the user needs to do to get that task done
- Assume familiarity of concepts, but explain why suggested actions are helpful
- Crosslink heavily to conceptual/reference pages
- Discuss alternatives and responses to real-world tradeoffs that may arise when solving a problem
- Use lots of example code, ideally within complete code blocks that the reader can copy and run.
- End with a recap/next steps section summarizing what the tutorial covered and future reading, such as other related how-to guides
### Conceptual guides
LangGraph's conceptual guides fall under the **Explanation** quadrant of Diataxis. They should cover LangChain terms and concepts
in a more abstract way than how-to guides or tutorials, and should be geared towards curious users interested in
gaining a deeper understanding of the framework. Try to avoid excessively large code examples. The goal here is to
impart perspective to the user rather than to finish a practical project. These guides should cover **why** things work the way they do.
To quote the Diataxis website:
> The perspective of explanation is higher and wider than that of the other types. It does not take the users eye-level view, as in a how-to guide, or a close-up view of the machinery, like reference material. Its scope in each case is a topic - “an area of knowledge”, that somehow has to be bounded in a reasonable, meaningful way.
Some examples include:
- [What does it mean to be agentic?](https://langchain-ai.github.io/langgraph/concepts/high_level/)
- [Tool calling](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#tool-calling)
Here are some high-level tips on writing a good conceptual guide:
- Explain design decisions. Why does concept X exist and why was it designed this way?
- Use analogies and reference other concepts and alternatives
- Avoid blending in too much reference content
- You can and should reference content covered in other guides, but make sure to link to them
### References
References contain detailed, low-level information that describes exactly what functionality exists and how to use it.
In LangGraph, this is mainly our API reference pages, which are populated from docstrings within code.
References pages are generally not read end-to-end, but are consulted as necessary when a user needs to know
how to use something specific.
To quote the Diataxis website:
> The only purpose of a reference guide is to describe, as succinctly as possible, and in an orderly way. Whereas the content of tutorials and how-to guides are led by needs of the user, reference material is led by the product it describes.
Many of the reference pages in LangChain are automatically generated from code,
but here are some high-level tips on writing a good docstring:
- Be concise
- Discuss special cases and deviations from a user's expectations
- Go into detail on required inputs and outputs
- Light details on when one might use the feature are fine, but in-depth details belong in other sections.
Each category serves a distinct purpose and requires a specific approach to writing and structuring the content.
## General guidelines
Here are some other guidelines you should think about when writing and organizing documentation.
We generally do not merge new tutorials from outside contributors without an actual need.
We welcome updates as well as new integration docs, how-tos, and references.
### Avoid duplication
Multiple pages that cover the same material in depth are difficult to maintain and cause confusion. There should
be only one (very rarely two), canonical pages for a given concept or feature. Instead, you should link to other guides.
### Link to other sections
Because sections of the docs do not exist in a vacuum, it is important to link to other sections as often as possible
to allow a developer to learn more about an unfamiliar topic inline.
This includes linking to the API references as well as conceptual sections!
### Be concise
In general, take a less-is-more approach. If a section with a good explanation of a concept already exists, you should link to it rather than
re-explain it, unless the concept you are documenting presents some new wrinkle.
Be concise, including in code samples.
### General style
- Use active voice and present tense whenever possible
- Use examples and code snippets to illustrate concepts and usage
- Use appropriate header levels (`#`, `##`, `###`, etc.) to organize the content hierarchically
- Use fewer cells with more code to make copy/paste easier
- Use bullet points and numbered lists to break down information into easily digestible chunks
- Use tables (especially for **Reference** sections) and diagrams often to present information visually
- Include the table of contents for longer documentation pages to help readers navigate the content, but hide it for shorter pages
## Setup
LangGraph documentation consists of two components:
1. Main Documentation: Hosted at [https://langchain-ai.github.io/langgraph/](https://langchain-ai.github.io/langgraph/),
this comprehensive resource serves as the primary user-facing documentation.
It covers a wide array of topics, including tutorials, use cases, integrations,
and more, offering extensive guidance on building with LangGraph.
The content for this documentation lives in the `/docs` directory of the monorepo.
2. In-code Documentation: This is documentation of the codebase itself, which is also
used to generate the externally facing [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/).
The content for the API reference is autogenerated by scanning the docstrings in the codebase. For this reason we ask that developers document their code well.
We appreciate all contributions to the documentation, whether it be fixing a typo,
adding a new tutorial or example and whether it be in the main documentation or the API Reference.
### 📜 Main Documentation
The content for the main documentation is located in the `/docs` directory of the monorepo.
The documentation is written using a combination of ipython notebooks (`.ipynb` files)
and markdown (`.md` files). The notebooks are converted to markdown
and then built using [MkDocs](https://www.mkdocs.org/).
Feel free to make contributions to the main documentation! 🥰
After modifying the documentation:
1. Run the linting and formatting commands (see below) to ensure that the documentation is well-formatted and free of errors.
2. Optionally build the documentation locally to verify that the changes look good.
3. Make a pull request with the changes.
### ⚒️ Linting and Building Documentation Locally
After writing up the documentation, you may want to lint and build the documentation
locally to ensure that it looks good and is free of errors.
If you're unable to build it locally that's okay as well, as you will be able to
see a preview of the documentation on the pull request page.
From the **monorepo root**, run the following command to install the dependencies:
<!-- TODO -->
```bash
poetry install --with docs --no-root
```
#### Building
The code that builds the documentation is located in the `/docs` directory of the monorepo.
Before building the documentation, it is always a good idea to clean the build directory:
```bash
make clean-docs
```
You can build and preview the documentation as outlined below:
```bash
make serve-docs
```
#### Linting
To spell check the docs, run the following from the `docs` directory:
```bash
codespell --skip="*.ambr,*.lock,*.ipynb,*.yaml,*.zlib,*.css.map,*.js.map" --ignore-words-list="infor,thead,stdio,nd,jupyter,lets,lite,uis,deque" .
```
### In-code Documentation
The in-code documentation is autogenerated from docstrings.
For the API reference to be useful, the codebase must be well-documented. This means that all functions, classes, and methods should have a docstring that explains what they do, what the arguments are, and what the return value is. This is a good practice in general, but it is especially important for LangGraph because the API reference is the primary resource for developers to understand how to use the codebase.
We generally follow the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) for docstrings.
Here is an example of a well-documented function:
```python
def my_function(arg1: int, arg2: str) -> float:
"""This is a short description of the function. (It should be a single sentence.)
This is a longer description of the function. It should explain what
the function does, what the arguments are, and what the return value is.
It should wrap at 88 characters.
Examples:
This is a section for examples of how to use the function.
```python
my_function(1, "hello")
\```
Args:
arg1: This is a description of arg1. We do not need to specify the type since
it is already specified in the function signature.
arg2: This is a description of arg2.
Returns:
This is a description of the return value.
"""
return 3.14
```
+3 -3
View File
@@ -64,10 +64,10 @@ While LangGraph can be used standalone, it also integrates seamlessly with any L
- [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
- [LangSmith Deployment](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
- [LangChain](https://python.langchain.com/docs/introduction/) Provides integrations and composable components to streamline LLM application development.
- [LangChain](https://docs.langchain.com/oss/python/langchain/overview) Provides integrations and composable components to streamline LLM application development.
> [!NOTE]
> Looking for the JS version of LangGraph? See the [JS repo](https://github.com/langchain-ai/langgraphjs) and the [JS docs](https://langchain-ai.github.io/langgraphjs/).
> Looking for the JS version of LangGraph? See the [JS repo](https://github.com/langchain-ai/langgraphjs) and the [JS docs](https://docs.langchain.com/oss/javascript/langgraph/overview).
## Additional resources
@@ -81,4 +81,4 @@ While LangGraph can be used standalone, it also integrates seamlessly with any L
## Acknowledgements
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
@@ -2108,9 +2108,9 @@ __metadata:
linkType: hard
"hono@npm:^4.5.4":
version: 4.9.7
resolution: "hono@npm:4.9.7"
checksum: 10c0/089184660a9211ea216ab95bafa45260e371651cb019db49828064b7982b0ae61cc3c4715324bfeb9037aa2460c39ffa2c91d84ad0c8d500fa77cbcc7fc07a8f
version: 4.10.3
resolution: "hono@npm:4.10.3"
checksum: 10c0/bdcc4c7066c74ba7cfa63ed6550768a0f43a420286c8f8f74b7012ea4901b8b06778fa8e98264b46f1a86920f056b7ede1f07814da4934912f9945def4977c29
languageName: node
linkType: hard
@@ -2340,13 +2340,13 @@ __metadata:
linkType: hard
"js-yaml@npm:^4.1.0":
version: 4.1.0
resolution: "js-yaml@npm:4.1.0"
version: 4.1.1
resolution: "js-yaml@npm:4.1.1"
dependencies:
argparse: "npm:^2.0.1"
bin:
js-yaml: bin/js-yaml.js
checksum: 10c0/184a24b4eaacfce40ad9074c64fd42ac83cf74d8c8cd137718d456ced75051229e5061b8633c3366b8aada17945a7a356b337828c19da92b51ae62126575018f
checksum: 10c0/561c7d7088c40a9bb53cc75becbfb1df6ae49b34b5e6e5a81744b14ae8667ec564ad2527709d1a6e7d5e5fa6d483aa0f373a50ad98d42fde368ec4a190d4fae7
languageName: node
linkType: hard
+64 -14
View File
@@ -81,7 +81,7 @@ REDIRECT_MAP = {
# cloud redirects
"cloud/index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
"cloud/how-tos/index.md": "https://docs.langchain.com/langsmith/home",
"cloud/concepts/api.md": "https://docs.langchain.com/langsmith/langgraph-server",
"cloud/concepts/api.md": "https://docs.langchain.com/langsmith/agent-server",
"cloud/concepts/cloud.md": "https://docs.langchain.com/langsmith/cloud",
"cloud/faq/studio.md": "https://docs.langchain.com/langsmith/studio",
"cloud/how-tos/human_in_the_loop_edit_state.md": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
@@ -111,8 +111,8 @@ REDIRECT_MAP = {
"tutorials/introduction.ipynb": "https://docs.langchain.com/oss/python/langgraph/overview",
"agents/deployment.md": "https://docs.langchain.com/oss/python/langgraph/local-server",
# deployment redirects
"how-tos/deploy-self-hosted.md": "https://docs.langchain.com/langsmith/hosting",
"concepts/self_hosted.md": "https://docs.langchain.com/langsmith/hosting",
"how-tos/deploy-self-hosted.md": "https://docs.langchain.com/langsmith/platform-setup",
"concepts/self_hosted.md": "https://docs.langchain.com/langsmith/platform-setup",
"tutorials/deployment.md": "https://docs.langchain.com/langsmith/deployments",
# assistant redirects
"cloud/how-tos/assistant_versioning.md": "https://docs.langchain.com/langsmith/configuration-cloud",
@@ -129,16 +129,30 @@ REDIRECT_MAP = {
"how-tos/human_in_the_loop/edit-graph-state.ipynb": "https://docs.langchain.com/oss/python/langgraph/use-time-travel",
# LGP mintlify migration redirects
"examples/index.md": "https://docs.langchain.com/oss/python/learn",
"guides/index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
"concepts/index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
"tutorials/index.md": "https://docs.langchain.com/oss/python/learn",
"llms-txt-overview.md": "https://docs.langchain.com/llms.txt",
"tutorials/rag/langgraph_adaptive_rag.md": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
"tutorials/multi_agent/multi-agent-collaboration.ipynb": "https://docs.langchain.com/oss/python/langchain/multi-agent",
"how-tos/create-react-agent-manage-message-history.ipynb": "https://docs.langchain.com/oss/python/langgraph/add-memory",
"how-tos/many-tools.ipynb": "https://docs.langchain.com/oss/python/langchain/tools",
"tutorials/customer-support/customer-support.ipynb": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
"how-tos/react-agent-structured-output.ipynb": "https://docs.langchain.com/oss/python/langchain/agents#structured-output",
"tutorials/code_assistant/langgraph_code_assistant.ipynb": "https://docs.langchain.com/oss/python/langgraph/agentic-rag",
"tutorials/multi_agent/hierarchical_agent_teams.ipynb": "https://docs.langchain.com/oss/python/langchain/supervisor",
"tutorials/auth/getting_started.md": "https://docs.langchain.com/langsmith/auth",
"tutorials/auth/resource_auth.md": "https://docs.langchain.com/langsmith/resource-auth",
"tutorials/auth/add_auth_server.md": "https://docs.langchain.com/langsmith/add-auth-server",
"how-tos/use-remote-graph.md": "https://docs.langchain.com/langsmith/use-remote-graph",
"how-tos/autogen-integration.md": "https://docs.langchain.com/langsmith/autogen-integration",
"how-tos/human_in_the_loop/wait-user-input.ipynb": "https://docs.langchain.com/oss/python/langgraph/interrupts",
"cloud/how-tos/use_stream_react.md": "https://docs.langchain.com/langsmith/use-stream-react",
"cloud/how-tos/generative_ui_react.md": "https://docs.langchain.com/langsmith/generative-ui-react",
"concepts/langgraph_platform.md": "https://docs.langchain.com/langsmith/deployments",
"concepts/langgraph_components.md": "https://docs.langchain.com/langsmith/components",
"concepts/langgraph_server.md": "https://docs.langchain.com/langsmith/langgraph-server",
"concepts/langgraph_server.md": "https://docs.langchain.com/langsmith/agent-server",
"concepts/langgraph_data_plane.md": "https://docs.langchain.com/langsmith/data-plane",
"concepts/langgraph_control_plane.md": "https://docs.langchain.com/langsmith/control-plane",
"concepts/langgraph_cli.md": "https://docs.langchain.com/langsmith/cli",
@@ -180,7 +194,7 @@ REDIRECT_MAP = {
"cloud/concepts/data_storage_and_privacy.md": "https://docs.langchain.com/langsmith/data-storage-and-privacy",
"cloud/deployment/semantic_search.md": "https://docs.langchain.com/langsmith/semantic-search",
"how-tos/ttl/configure_ttl.md": "https://docs.langchain.com/langsmith/configure-ttl",
"concepts/deployment_options.md": "https://docs.langchain.com/langsmith/hosting",
"concepts/deployment_options.md": "https://docs.langchain.com/langsmith/platform-setup",
"cloud/quick_start.md": "https://docs.langchain.com/langsmith/deployment-quickstart",
"cloud/deployment/setup.md": "https://docs.langchain.com/langsmith/setup-app-requirements-txt",
"cloud/deployment/setup_pyproject.md": "https://docs.langchain.com/langsmith/setup-pyproject",
@@ -201,7 +215,7 @@ REDIRECT_MAP = {
"cloud/deployment/egress.md": "https://docs.langchain.com/langsmith/env-var",
"cloud/how-tos/streaming.md": "https://docs.langchain.com/langsmith/streaming",
"cloud/reference/api/api_ref.md": "https://docs.langchain.com/langsmith/server-api-ref",
"cloud/reference/langgraph_server_changelog.md": "https://docs.langchain.com/langsmith/langgraph-server-changelog",
"cloud/reference/langgraph_server_changelog.md": "https://docs.langchain.com/langsmith/agent-server-changelog",
"cloud/reference/api/api_ref_control_plane.md": "https://docs.langchain.com/langsmith/api-ref-control-plane",
"cloud/reference/cli.md": "https://docs.langchain.com/langsmith/cli",
"cloud/reference/env_var.md": "https://docs.langchain.com/langsmith/env-var",
@@ -219,12 +233,15 @@ REDIRECT_MAP = {
"tutorials/get-started/6-time-travel.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
"tutorials/langsmith/local-server.md": "https://docs.langchain.com/oss/python/langgraph/local-server",
"tutorials/workflows.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
"tutorials/plan-and-execute/plan-and-execute.ipynb": "https://docs.langchain.com/oss/python/langchain/middleware/built-in#to-do-list",
"tutorials/langgraph-platform/local-server/local-server.md": "https://docs.langchain.com/langsmith/local-server",
"concepts/agentic_concepts.md": "https://docs.langchain.com/oss/python/langgraph/workflows-agents",
"guides/index.md": "https://docs.langchain.com/oss/python/langchain/overview",
"agents/overview.md": "https://docs.langchain.com/oss/python/langchain/agents",
"agents/run_agents.md": "https://docs.langchain.com/oss/python/langgraph/quickstart",
"concepts/low_level.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
"how-tos/graph-api.md": "https://docs.langchain.com/oss/python/langgraph/graph-api",
"how-tos/react-agent-from-scratch.ipynb": "https://docs.langchain.com/oss/python/langchain/quickstart",
"concepts/functional_api.md": "https://docs.langchain.com/oss/python/langgraph/functional-api",
"how-tos/use-functional-api.md": "https://docs.langchain.com/oss/python/langgraph/functional-api",
"concepts/pregel.md": "https://docs.langchain.com/oss/python/langgraph/pregel",
@@ -282,8 +299,8 @@ REDIRECT_MAP = {
"reference/supervisor.md": "https://reference.langchain.com/python/langgraph/supervisor/",
"reference/swarm.md": "https://reference.langchain.com/python/langgraph/swarm/",
"reference/mcp.md": "https://reference.langchain.com/python/langgraph/mcp/",
"cloud/reference/sdk/python_sdk_ref.md": "https://reference.langchain.com/python/platform/python_sdk/",
"reference/remote_graph.md": "https://reference.langchain.com/python/platform/remote_graph/",
"cloud/reference/sdk/python_sdk_ref.md": "https://reference.langchain.com/python/langsmith/deployment/sdk/",
"reference/remote_graph.md": "https://reference.langchain.com/python/langsmith/deployment/remote_graph/",
# additional exclude-search entries from mkdocs.yml
"additional-resources/index.md": "https://docs.langchain.com/oss/python/langchain/overview",
@@ -294,8 +311,8 @@ REDIRECT_MAP = {
"cloud/deployment/custom_docker.md": "https://docs.langchain.com/langsmith/custom-docker",
"cloud/deployment/egress.md": "https://docs.langchain.com/langsmith/env-var",
"cloud/deployment/graph_rebuild.md": "https://docs.langchain.com/langsmith/graph-rebuild",
"cloud/deployment/self_hosted_control_plane.md": "https://docs.langchain.com/langsmith/hosting",
"cloud/deployment/self_hosted_data_plane.md": "https://docs.langchain.com/langsmith/hosting",
"cloud/deployment/self_hosted_control_plane.md": "https://docs.langchain.com/langsmith/platform-setup",
"cloud/deployment/self_hosted_data_plane.md": "https://docs.langchain.com/langsmith/platform-setup",
"cloud/deployment/semantic_search.md": "https://docs.langchain.com/langsmith/semantic-search",
"cloud/deployment/setup_javascript.md": "https://docs.langchain.com/langsmith/setup-javascript",
"cloud/deployment/setup_pyproject.md": "https://docs.langchain.com/langsmith/setup-pyproject",
@@ -331,7 +348,7 @@ REDIRECT_MAP = {
"cloud/reference/api/api_ref.md": "https://docs.langchain.com/langsmith/server-api-ref",
"cloud/reference/cli.md": "https://docs.langchain.com/langsmith/cli",
"cloud/reference/env_var.md": "https://docs.langchain.com/langsmith/env-var",
"cloud/reference/langgraph_server_changelog.md": "https://docs.langchain.com/langsmith/langgraph-server-changelog",
"cloud/reference/langgraph_server_changelog.md": "https://docs.langchain.com/langsmith/agent-server-changelog",
"cloud/reference/sdk/js_ts_sdk_ref.md": "https://reference.langchain.com/javascript/modules/langsmith.html",
"concepts/application_structure.md": "https://docs.langchain.com/langsmith/application-structure",
"concepts/assistants.md": "https://docs.langchain.com/langsmith/assistants",
@@ -345,9 +362,9 @@ REDIRECT_MAP = {
"concepts/langgraph_control_plane.md": "https://docs.langchain.com/langsmith/control-plane",
"concepts/langgraph_data_plane.md": "https://docs.langchain.com/langsmith/data-plane",
"concepts/langgraph_platform.md": "https://docs.langchain.com/langsmith/home",
"concepts/langgraph_self_hosted_control_plane.md": "https://docs.langchain.com/langsmith/hosting",
"concepts/langgraph_self_hosted_data_plane.md": "https://docs.langchain.com/langsmith/hosting",
"concepts/langgraph_server.md": "https://docs.langchain.com/langsmith/langgraph-server",
"concepts/langgraph_self_hosted_control_plane.md": "https://docs.langchain.com/langsmith/platform-setup",
"concepts/langgraph_self_hosted_data_plane.md": "https://docs.langchain.com/langsmith/platform-setup",
"concepts/langgraph_server.md": "https://docs.langchain.com/langsmith/agent-server",
"concepts/langgraph_standalone_container.md": "https://docs.langchain.com/langsmith/docker",
"concepts/langgraph_studio.md": "https://docs.langchain.com/langsmith/studio",
"concepts/plans.md": "https://docs.langchain.com/langsmith/home",
@@ -793,6 +810,17 @@ def on_post_build(config):
# Track which paths have explicit redirects
redirected_paths = set()
# Collect all existing HTML files in the site
all_html_files = set()
for root, dirs, files in os.walk(site_dir):
for file in files:
if file.endswith(".html"):
# Get relative path from site_dir
html_path = os.path.relpath(os.path.join(root, file), site_dir)
# Normalize path separators to forward slashes
html_path = html_path.replace(os.sep, "/")
all_html_files.add(html_path)
# Process explicit redirects from REDIRECT_MAP
for page_old, page_new in REDIRECT_MAP.items():
# Convert .ipynb to .md for path calculation
@@ -847,6 +875,24 @@ def on_post_build(config):
_write_html(site_dir, old_html_path, new_html_path)
# Create catch-all redirects for any HTML files not explicitly redirected
catchall_url = "https://docs.langchain.com/oss/python/langgraph/overview"
for html_file in all_html_files:
# Skip if this file is already explicitly redirected
if html_file in redirected_paths:
continue
# Skip the root index.html (we handle that separately)
if html_file == "index.html":
continue
# Skip reference documentation (keep those accessible)
if html_file.startswith("reference/"):
continue
# Create redirect for this unmapped file
_write_html(site_dir, html_file, catchall_url)
# Create root index.html redirect
root_redirect_html = """<!doctype html>
<html lang="en">
@@ -872,10 +918,14 @@ def on_post_build(config):
# Create server-side catch-all redirect file for Netlify/Cloudflare Pages
# This handles any pages not explicitly mapped in REDIRECT_MAP
# Note: This won't work on GitHub Pages, but kept for potential future use
redirects_content = """# Netlify/Cloudflare Pages redirect rules
# Specific redirects are handled by individual HTML redirect pages
# This is the catch-all for any unmapped pages
# Exclude reference docs from catch-all
/reference/* 200
# Catch-all: redirect any page not explicitly mapped
/* https://docs.langchain.com/oss/python/langgraph/overview 301
"""
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 LangChain, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -97,7 +97,7 @@ class PostgresSaver(BasePostgresSaver):
strict=False,
):
cur.execute(migration)
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
cur.execute("INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,))
if self.pipe:
self.pipe.sync()
@@ -143,11 +143,13 @@ class PostgresSaver(BasePostgresSaver):
"""
where, args = self._search_where(config, filter, before)
query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
if limit:
query += f" LIMIT {limit}"
params = list(args)
if limit is not None:
query += " LIMIT %s"
params.append(int(limit))
# if we change this to use .stream() we need to make sure to close the cursor
with self._cursor() as cur:
cur.execute(query, args)
cur.execute(query, params)
values = cur.fetchall()
if not values:
return
@@ -102,7 +102,9 @@ class AsyncPostgresSaver(BasePostgresSaver):
strict=False,
):
await cur.execute(migration)
await cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
await cur.execute(
"INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,)
)
if self.pipe:
await self.pipe.sync()
@@ -130,11 +132,13 @@ class AsyncPostgresSaver(BasePostgresSaver):
"""
where, args = self._search_where(config, filter, before)
query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
if limit:
query += f" LIMIT {limit}"
params = list(args)
if limit is not None:
query += " LIMIT %s"
params.append(int(limit))
# if we change this to use .stream() we need to make sure to close the cursor
async with self._cursor() as cur:
await cur.execute(query, args, binary=True)
await cur.execute(query, params, binary=True)
values = await cur.fetchall()
if not values:
return
@@ -81,7 +81,7 @@ MIGRATIONS = [
"""
CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id);
""",
"""ALTER TABLE checkpoint_writes ADD COLUMN task_path TEXT NOT NULL DEFAULT '';""",
"""ALTER TABLE checkpoint_writes ADD COLUMN IF NOT EXISTS task_path TEXT NOT NULL DEFAULT '';""",
]
SELECT_SQL = """
@@ -77,7 +77,7 @@ MIGRATIONS = [
CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id);
""",
"""
ALTER TABLE checkpoint_writes ADD COLUMN task_path TEXT NOT NULL DEFAULT '';
ALTER TABLE checkpoint_writes ADD COLUMN IF NOT EXISTS task_path TEXT NOT NULL DEFAULT '';
""",
]
@@ -252,7 +252,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
strict=False,
):
cur.execute(migration)
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
cur.execute("INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,))
if self.pipe:
self.pipe.sync()
@@ -272,10 +272,12 @@ class ShallowPostgresSaver(BasePostgresSaver):
"""
where, args = self._search_where(config, filter, before)
query = self.SELECT_SQL + where
if limit:
query += f" LIMIT {limit}"
params = list(args)
if limit is not None:
query += " LIMIT %s"
params.append(int(limit))
with self._cursor() as cur:
cur.execute(self.SELECT_SQL + where, args, binary=True)
cur.execute(query, params, binary=True)
for value in cur:
checkpoint: Checkpoint = {
**value["checkpoint"],
@@ -614,7 +616,9 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
strict=False,
):
await cur.execute(migration)
await cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
await cur.execute(
"INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,)
)
if self.pipe:
await self.pipe.sync()
@@ -634,10 +638,12 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
"""
where, args = self._search_where(config, filter, before)
query = self.SELECT_SQL + where
if limit:
query += f" LIMIT {limit}"
params = list(args)
if limit is not None:
query += " LIMIT %s"
params.append(int(limit))
async with self._cursor() as cur:
await cur.execute(self.SELECT_SQL + where, args, binary=True)
await cur.execute(query, params, binary=True)
async for value in cur:
checkpoint: Checkpoint = {
**value["checkpoint"],
@@ -266,6 +266,27 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
k: v(self) if v is not None and callable(v) else v
for k, v in migration.params.items()
}
if "dims" in params:
try:
params["dims"] = int(params["dims"])
except Exception as e:
raise ValueError(
f"Invalid dims for vector index: {params['dims']}"
) from e
if "vector_type" in params:
vt = str(params["vector_type"])
if vt not in ("vector", "halfvec"):
raise ValueError(
f"Invalid vector_type for pgvector: {vt}"
)
params["vector_type"] = vt
if "index_type" in params:
it = str(params["index_type"])
if it not in ("hnsw", "ivfflat"):
raise ValueError(
f"Invalid index_type for pgvector: {it}"
)
params["index_type"] = it
sql = sql % params
await cur.execute(sql)
await cur.execute(
@@ -91,7 +91,12 @@ WHERE expires_at IS NOT NULL;
VECTOR_MIGRATIONS: Sequence[Migration] = [
Migration(
"""
CREATE EXTENSION IF NOT EXISTS vector;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'vector') THEN
CREATE EXTENSION vector;
END IF;
END $$;
""",
),
Migration(
@@ -322,31 +327,36 @@ class BasePostgresStore(Generic[C]):
embedding_request: tuple[str, Sequence[tuple[str, str, str, str]]] | None = None
if inserts:
values = []
insertion_params = []
insertion_params: list[Any] = []
vector_values = []
embedding_request_params = []
# Handle TTL expiration
# First handle main store insertions
for op in inserts:
if op.ttl is not None:
expires_at_str = f"NOW() + INTERVAL '{op.ttl * 60} seconds'"
ttl_minutes = op.ttl
else:
expires_at_str = "NULL"
ttl_minutes = None
values.append(
f"(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, {expires_at_str}, %s)"
)
insertion_params.extend(
[
(
_namespace_to_text(op.namespace),
op.key,
Jsonb(cast(dict, op.value)),
ttl_minutes,
]
)
)
if op.ttl is not None:
values.append(
"(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NOW() + %s::interval, %s)"
)
ttl_minutes = float(op.ttl)
insertion_params.extend(
(
f"{ttl_minutes * 60} seconds",
ttl_minutes,
)
)
else:
values.append(
"(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, %s)"
)
insertion_params.append(None)
# Then handle embeddings if configured
if self.index_config:
@@ -460,6 +470,10 @@ class BasePostgresStore(Generic[C]):
cast(dict, self.index_config)["dims"],
)
else:
if vector_type not in ("vector", "halfvec"):
raise ValueError(
f"Invalid vector_type for pgvector: {vector_type}"
)
score_operator = score_operator % ("%s", vector_type)
vectors_per_doc_estimate = cast(dict, self.index_config)[
@@ -1117,6 +1131,27 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
k: v(self) if v is not None and callable(v) else v
for k, v in migration.params.items()
}
if "dims" in params:
try:
params["dims"] = int(params["dims"])
except Exception as e:
raise ValueError(
f"Invalid dims for vector index: {params['dims']}"
) from e
if "vector_type" in params:
vt = str(params["vector_type"])
if vt not in ("vector", "halfvec"):
raise ValueError(
f"Invalid vector_type for pgvector: {vt}"
)
params["vector_type"] = vt
if "index_type" in params:
it = str(params["index_type"])
if it not in ("hnsw", "ivfflat"):
raise ValueError(
f"Invalid index_type for pgvector: {it}"
)
params["index_type"] = it
sql = sql % params
cur.execute(sql)
cur.execute("INSERT INTO vector_migrations (v) VALUES (%s)", (v,))
@@ -1170,15 +1205,44 @@ def _get_vector_type_ops(store: BasePostgresStore) -> str:
def _get_index_params(store: Any) -> tuple[str, dict[str, Any]]:
"""Get the index type and configuration based on config."""
"""Get a sanitized index type and configuration based on config.
Only allow known-safe kinds and integer parameters to avoid SQL injection
when constructing DDL strings for index creation.
"""
if not store.index_config:
return "hnsw", {}
config = cast(PostgresIndexConfig, store.index_config)
index_config = config.get("ann_index_config", _DEFAULT_ANN_CONFIG).copy()
kind = index_config.pop("kind", "hnsw")
index_config.pop("vector_type", None)
return kind, index_config
raw = config.get("ann_index_config", _DEFAULT_ANN_CONFIG).copy()
kind = str(raw.pop("kind", "hnsw"))
if kind not in ("hnsw", "ivfflat", "flat"):
raise ValueError(
f"Invalid index kind for pgvector: {kind}. Expected 'hnsw', 'ivfflat', or 'flat'."
)
raw.pop("vector_type", None)
if kind == "hnsw":
allowed_keys = {"m", "ef_construction"}
else: # ivfflat/flat
allowed_keys = {"lists", "nlist"}
sanitized: dict[str, int] = {}
for k, v in list(raw.items()):
if k not in allowed_keys:
continue
key = "lists" if k == "nlist" else k
try:
ivalue = int(v) # type: ignore[call-overload]
except Exception as e:
raise ValueError(f"Invalid index parameter value for {k}: {v}") from e
if ivalue <= 0:
continue
sanitized[key] = ivalue
return kind, sanitized
def _namespace_to_text(
+16 -7
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-checkpoint-postgres"
version = "2.0.25"
version = "3.0.2"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
authors = []
requires-python = ">=3.10"
@@ -12,28 +12,37 @@ readme = "README.md"
license = "MIT"
license-files = ['LICENSE']
dependencies = [
"langgraph-checkpoint>=2.1.2,<3.0.0",
"langgraph-checkpoint>=2.1.2,<4.0.0",
"orjson>=3.10.1",
"psycopg>=3.2.0",
"psycopg-pool>=3.2.0",
]
[project.urls]
Repository = "https://www.github.com/langchain-ai/langgraph"
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-postgres"
Twitter = "https://x.com/LangChainAI"
Slack = "https://www.langchain.com/join-community"
Reddit = "https://www.reddit.com/r/LangChain/"
[dependency-groups]
dev = [
"ruff",
"codespell",
test = [
"pytest",
"anyio",
"pytest-asyncio",
"pytest-mock",
"mypy",
"psycopg[binary]",
"langgraph-checkpoint",
"pytest-watcher",
]
lint = [
"ruff",
"codespell",
"mypy",
]
dev = [
{include-group = "test"},
{include-group = "lint"},
]
[tool.uv]
default-groups = ['dev']
+663 -429
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 LangChain, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -1,5 +1,6 @@
from __future__ import annotations
import json
import random
import sqlite3
import threading
@@ -265,9 +266,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
self.serde.loads_typed((type, checkpoint)),
cast(
CheckpointMetadata,
self.jsonplus_serde.loads(metadata)
if metadata is not None
else {},
json.loads(metadata) if metadata is not None else {},
),
(
{
@@ -330,8 +329,9 @@ class SqliteSaver(BaseCheckpointSaver[str]):
FROM checkpoints
{where}
ORDER BY checkpoint_id DESC"""
if limit:
query += f" LIMIT {limit}"
if limit is not None:
query += " LIMIT ?"
param_values = (*param_values, limit)
with self.cursor(transaction=False) as cur, closing(self.conn.cursor()) as wcur:
cur.execute(query, param_values)
for (
@@ -358,9 +358,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
self.serde.loads_typed((type, checkpoint)),
cast(
CheckpointMetadata,
self.jsonplus_serde.loads(metadata)
if metadata is not None
else {},
json.loads(metadata) if metadata is not None else {},
),
(
{
@@ -413,9 +411,9 @@ class SqliteSaver(BaseCheckpointSaver[str]):
thread_id = config["configurable"]["thread_id"]
checkpoint_ns = config["configurable"]["checkpoint_ns"]
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
serialized_metadata = self.jsonplus_serde.dumps(
get_checkpoint_metadata(config, metadata)
)
serialized_metadata = json.dumps(
get_checkpoint_metadata(config, metadata), ensure_ascii=False
).encode("utf-8", "ignore")
with self.cursor() as cur:
cur.execute(
"INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
@@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
import json
import random
from collections.abc import AsyncIterator, Callable, Iterator, Sequence
from contextlib import asynccontextmanager
@@ -377,9 +378,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
self.serde.loads_typed((type, checkpoint)),
cast(
CheckpointMetadata,
self.jsonplus_serde.loads(metadata)
if metadata is not None
else {},
(json.loads(metadata) if metadata is not None else {}),
),
(
{
@@ -426,8 +425,9 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
FROM checkpoints
{where}
ORDER BY checkpoint_id DESC"""
if limit:
query += f" LIMIT {limit}"
if limit is not None:
query += " LIMIT ?"
params = (*params, limit)
async with (
self.lock,
self.conn.execute(query, params) as cur,
@@ -457,9 +457,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
self.serde.loads_typed((type, checkpoint)),
cast(
CheckpointMetadata,
self.jsonplus_serde.loads(metadata)
if metadata is not None
else {},
(json.loads(metadata) if metadata is not None else {}),
),
(
{
@@ -503,9 +501,9 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
thread_id = config["configurable"]["thread_id"]
checkpoint_ns = config["configurable"]["checkpoint_ns"]
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
serialized_metadata = self.jsonplus_serde.dumps(
get_checkpoint_metadata(config, metadata)
)
serialized_metadata = json.dumps(
get_checkpoint_metadata(config, metadata), ensure_ascii=False
).encode("utf-8", "ignore")
async with (
self.lock,
self.conn.execute(
@@ -1,12 +1,32 @@
from __future__ import annotations
import json
import re
from collections.abc import Sequence
from typing import Any
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import get_checkpoint_id
_FILTER_PATTERN = re.compile(r"^[a-zA-Z0-9_.-]+$")
def _validate_filter_key(key: str) -> None:
"""Validate that a filter key is safe for use in SQL queries.
Args:
key: The filter key to validate
Raises:
ValueError: If the key contains invalid characters that could enable SQL injection
"""
# Allow alphanumeric characters, underscores, dots, and hyphens
# This covers typical JSON property names while preventing SQL injection
if not _FILTER_PATTERN.match(key):
raise ValueError(
f"Invalid filter key: '{key}'. Filter keys must contain only alphanumeric characters, underscores, dots, and hyphens."
)
def _metadata_predicate(
metadata_filter: dict[str, Any],
@@ -43,6 +63,7 @@ def _metadata_predicate(
# process metadata query
for query_key, query_value in metadata_filter.items():
_validate_filter_key(query_key)
operator, param_value = _where_value(query_value)
predicates.append(
f"json_extract(CAST(metadata AS TEXT), '$.{query_key}') {operator}"
@@ -107,6 +107,9 @@ def _decode_ns_text(namespace: str) -> tuple[str, ...]:
return tuple(namespace.split("."))
_FILTER_PATTERN = re.compile(r"^[a-zA-Z0-9_.-]+$")
def _validate_filter_key(key: str) -> None:
"""Validate that a filter key is safe for use in SQL queries.
@@ -118,7 +121,7 @@ def _validate_filter_key(key: str) -> None:
"""
# Allow alphanumeric characters, underscores, dots, and hyphens
# This covers typical JSON property names while preventing SQL injection
if not re.match(r"^[a-zA-Z0-9_.-]+$", key):
if not _FILTER_PATTERN.match(key):
raise ValueError(
f"Invalid filter key: '{key}'. Filter keys must contain only alphanumeric characters, underscores, dots, and hyphens."
)
@@ -404,12 +407,9 @@ class BaseSqliteStore:
# SQLite json_extract returns unquoted string values
if isinstance(value, str):
filter_conditions.append(
"json_extract(value, '$."
+ key
+ "') = '"
+ value.replace("'", "''")
+ "'"
"json_extract(value, '$." + key + "') = ?"
)
filter_params.append(value)
elif value is None:
filter_conditions.append(
"json_extract(value, '$." + key + "') IS NULL"
@@ -423,9 +423,11 @@ class BaseSqliteStore:
+ ("1" if value else "0")
)
elif isinstance(value, (int, float)):
# Use parameterized query to handle special floats and large integers
filter_conditions.append(
"json_extract(value, '$." + key + "') = " + str(value)
"json_extract(value, '$." + key + "') = ?"
)
filter_params.append(float(value))
else:
# Complex objects (list, dict, …) compare JSON text
filter_conditions.append(
@@ -636,85 +638,66 @@ class BaseSqliteStore:
# We need to properly format values for SQLite JSON extraction comparison
if op == "$eq":
if isinstance(value, str):
# Direct string comparison with proper quoting for unquoted json_extract result
return (
f"json_extract(value, '$.{key}') = '"
+ value.replace("'", "''")
+ "'",
[],
)
return f"json_extract(value, '$.{key}') = ?", [value]
elif value is None:
return f"json_extract(value, '$.{key}') IS NULL", []
elif isinstance(value, bool):
# SQLite JSON stores booleans as integers
return f"json_extract(value, '$.{key}') = {1 if value else 0}", []
elif isinstance(value, (int, float)):
return f"json_extract(value, '$.{key}') = {value}", []
# Convert to float to handle inf, -inf, nan, and very large integers
# SQLite REAL can handle these cases better than INTEGER
return f"json_extract(value, '$.{key}') = ?", [float(value)]
else:
return f"json_extract(value, '$.{key}') = ?", [orjson.dumps(value)]
elif op == "$gt":
# For numeric values, SQLite needs to compare as numbers, not strings
if isinstance(value, (int, float)):
return f"CAST(json_extract(value, '$.{key}') AS REAL) > {value}", []
# Convert to float to handle special values and very large integers
return f"CAST(json_extract(value, '$.{key}') AS REAL) > ?", [
float(value)
]
elif isinstance(value, str):
return (
f"json_extract(value, '$.{key}') > '"
+ value.replace("'", "''")
+ "'",
[],
)
return f"json_extract(value, '$.{key}') > ?", [value]
else:
return f"json_extract(value, '$.{key}') > ?", [orjson.dumps(value)]
elif op == "$gte":
if isinstance(value, (int, float)):
return f"CAST(json_extract(value, '$.{key}') AS REAL) >= {value}", []
return f"CAST(json_extract(value, '$.{key}') AS REAL) >= ?", [
float(value)
]
elif isinstance(value, str):
return (
f"json_extract(value, '$.{key}') >= '"
+ value.replace("'", "''")
+ "'",
[],
)
return f"json_extract(value, '$.{key}') >= ?", [value]
else:
return f"json_extract(value, '$.{key}') >= ?", [orjson.dumps(value)]
elif op == "$lt":
if isinstance(value, (int, float)):
return f"CAST(json_extract(value, '$.{key}') AS REAL) < {value}", []
return f"CAST(json_extract(value, '$.{key}') AS REAL) < ?", [
float(value)
]
elif isinstance(value, str):
return (
f"json_extract(value, '$.{key}') < '"
+ value.replace("'", "''")
+ "'",
[],
)
return f"json_extract(value, '$.{key}') < ?", [value]
else:
return f"json_extract(value, '$.{key}') < ?", [orjson.dumps(value)]
elif op == "$lte":
if isinstance(value, (int, float)):
return f"CAST(json_extract(value, '$.{key}') AS REAL) <= {value}", []
return f"CAST(json_extract(value, '$.{key}') AS REAL) <= ?", [
float(value)
]
elif isinstance(value, str):
return (
f"json_extract(value, '$.{key}') <= '"
+ value.replace("'", "''")
+ "'",
[],
)
return f"json_extract(value, '$.{key}') <= ?", [value]
else:
return f"json_extract(value, '$.{key}') <= ?", [orjson.dumps(value)]
elif op == "$ne":
if isinstance(value, str):
return (
f"json_extract(value, '$.{key}') != '"
+ value.replace("'", "''")
+ "'",
[],
)
return f"json_extract(value, '$.{key}') != ?", [value]
elif value is None:
return f"json_extract(value, '$.{key}') IS NOT NULL", []
elif isinstance(value, bool):
return f"json_extract(value, '$.{key}') != {1 if value else 0}", []
elif isinstance(value, (int, float)):
return f"json_extract(value, '$.{key}') != {value}", []
# Convert to float for consistency
return f"json_extract(value, '$.{key}') != ?", [float(value)]
else:
return f"json_extract(value, '$.{key}') != ?", [orjson.dumps(value)]
else:
@@ -792,8 +775,9 @@ class SqliteStore(BaseSqliteStore, BaseStore):
self,
conn: sqlite3.Connection,
*,
deserializer: Callable[[bytes | str | orjson.Fragment], dict[str, Any]]
| None = None,
deserializer: (
Callable[[bytes | str | orjson.Fragment], dict[str, Any]] | None
) = None,
index: SqliteIndexConfig | None = None,
ttl: TTLConfig | None = None,
):
@@ -874,85 +858,66 @@ class SqliteStore(BaseSqliteStore, BaseStore):
# We need to properly format values for SQLite JSON extraction comparison
if op == "$eq":
if isinstance(value, str):
# Direct string comparison with proper quoting for unquoted json_extract result
return (
f"json_extract(value, '$.{key}') = '"
+ value.replace("'", "''")
+ "'",
[],
)
return f"json_extract(value, '$.{key}') = ?", [value]
elif value is None:
return f"json_extract(value, '$.{key}') IS NULL", []
elif isinstance(value, bool):
# SQLite JSON stores booleans as integers
return f"json_extract(value, '$.{key}') = {1 if value else 0}", []
elif isinstance(value, (int, float)):
return f"json_extract(value, '$.{key}') = {value}", []
# Convert to float to handle inf, -inf, nan, and very large integers
# SQLite REAL can handle these cases better than INTEGER
return f"json_extract(value, '$.{key}') = ?", [float(value)]
else:
return f"json_extract(value, '$.{key}') = ?", [orjson.dumps(value)]
elif op == "$gt":
# For numeric values, SQLite needs to compare as numbers, not strings
if isinstance(value, (int, float)):
return f"CAST(json_extract(value, '$.{key}') AS REAL) > {value}", []
# Convert to float to handle special values and very large integers
return f"CAST(json_extract(value, '$.{key}') AS REAL) > ?", [
float(value)
]
elif isinstance(value, str):
return (
f"json_extract(value, '$.{key}') > '"
+ value.replace("'", "''")
+ "'",
[],
)
return f"json_extract(value, '$.{key}') > ?", [value]
else:
return f"json_extract(value, '$.{key}') > ?", [orjson.dumps(value)]
elif op == "$gte":
if isinstance(value, (int, float)):
return f"CAST(json_extract(value, '$.{key}') AS REAL) >= {value}", []
return f"CAST(json_extract(value, '$.{key}') AS REAL) >= ?", [
float(value)
]
elif isinstance(value, str):
return (
f"json_extract(value, '$.{key}') >= '"
+ value.replace("'", "''")
+ "'",
[],
)
return f"json_extract(value, '$.{key}') >= ?", [value]
else:
return f"json_extract(value, '$.{key}') >= ?", [orjson.dumps(value)]
elif op == "$lt":
if isinstance(value, (int, float)):
return f"CAST(json_extract(value, '$.{key}') AS REAL) < {value}", []
return f"CAST(json_extract(value, '$.{key}') AS REAL) < ?", [
float(value)
]
elif isinstance(value, str):
return (
f"json_extract(value, '$.{key}') < '"
+ value.replace("'", "''")
+ "'",
[],
)
return f"json_extract(value, '$.{key}') < ?", [value]
else:
return f"json_extract(value, '$.{key}') < ?", [orjson.dumps(value)]
elif op == "$lte":
if isinstance(value, (int, float)):
return f"CAST(json_extract(value, '$.{key}') AS REAL) <= {value}", []
return f"CAST(json_extract(value, '$.{key}') AS REAL) <= ?", [
float(value)
]
elif isinstance(value, str):
return (
f"json_extract(value, '$.{key}') <= '"
+ value.replace("'", "''")
+ "'",
[],
)
return f"json_extract(value, '$.{key}') <= ?", [value]
else:
return f"json_extract(value, '$.{key}') <= ?", [orjson.dumps(value)]
elif op == "$ne":
if isinstance(value, str):
return (
f"json_extract(value, '$.{key}') != '"
+ value.replace("'", "''")
+ "'",
[],
)
return f"json_extract(value, '$.{key}') != ?", [value]
elif value is None:
return f"json_extract(value, '$.{key}') IS NOT NULL", []
elif isinstance(value, bool):
return f"json_extract(value, '$.{key}') != {1 if value else 0}", []
elif isinstance(value, (int, float)):
return f"json_extract(value, '$.{key}') != {value}", []
# Convert to float for consistency
return f"json_extract(value, '$.{key}') != ?", [float(value)]
else:
return f"json_extract(value, '$.{key}') != ?", [orjson.dumps(value)]
else:
+16 -7
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-checkpoint-sqlite"
version = "2.0.11"
version = "3.0.1"
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
authors = []
requires-python = ">=3.10"
@@ -12,26 +12,35 @@ readme = "README.md"
license = "MIT"
license-files = ['LICENSE']
dependencies = [
"langgraph-checkpoint>=2.0.21,<3.0.0",
"langgraph-checkpoint>=3,<4.0.0",
"aiosqlite>=0.20",
"sqlite-vec>=0.1.6",
]
[project.urls]
Repository = "https://www.github.com/langchain-ai/langgraph"
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-sqlite"
Twitter = "https://x.com/LangChainAI"
Slack = "https://www.langchain.com/join-community"
Reddit = "https://www.reddit.com/r/LangChain/"
[dependency-groups]
dev = [
"ruff",
"codespell",
test = [
"pytest",
"pytest-asyncio",
"pytest-mock",
"pytest-watcher",
"mypy",
"langgraph-checkpoint",
"pytest-retry>=1.7.0",
]
lint = [
"ruff",
"codespell",
"mypy",
]
dev = [
{include-group = "test"},
{include-group = "lint"},
]
[tool.uv]
default-groups = ['dev']
+75 -1
View File
@@ -113,4 +113,78 @@ class TestAsyncSqliteSaver:
search_results_5[1].config["configurable"]["checkpoint_ns"],
} == {"", "inner"}
# TODO: test before and limit params
# Test limit param
search_results_6 = [
c
async for c in saver.alist(
{"configurable": {"thread_id": "thread-2"}}, limit=1
)
]
assert len(search_results_6) == 1
assert search_results_6[0].config["configurable"]["thread_id"] == "thread-2"
# Test before param
search_results_7 = [
c async for c in saver.alist(None, before=search_results_5[1].config)
]
assert len(search_results_7) == 1
assert search_results_7[0].config["configurable"]["thread_id"] == "thread-1"
async def test_limit_parameter_sql_injection_prevention(self) -> None:
"""Test that the limit parameter properly uses parameterized queries to prevent SQL injection."""
async with AsyncSqliteSaver.from_conn_string(":memory:") as saver:
# Setup: Create multiple checkpoints
for i in range(5):
config: RunnableConfig = {
"configurable": {
"thread_id": f"thread-{i}",
"checkpoint_ns": "",
}
}
checkpoint = empty_checkpoint()
metadata: CheckpointMetadata = {"index": i}
await saver.aput(config, checkpoint, metadata, {})
# Test that limit works correctly with valid integer
results = [c async for c in saver.alist(None, limit=2)]
assert len(results) == 2
# Test that limit=0 returns no results
results = [c async for c in saver.alist(None, limit=0)]
assert len(results) == 0
# Test that limit=None returns all results
results = [c async for c in saver.alist(None, limit=None)]
assert len(results) == 5
# Test explicit SQL injection attempt via limit parameter
# Even if type checking is bypassed and a malicious string is passed,
# the parameterized query will treat it as a value, not SQL code
# This would cause an error (can't convert string to int for LIMIT),
# which is the correct secure behavior
malicious_limits = [
"1; DROP TABLE checkpoints; --",
"1 OR 1=1",
"999999 UNION SELECT * FROM checkpoints",
]
for malicious_limit in malicious_limits:
# The parameterized query should safely reject non-integer limits
# or convert them in a way that prevents SQL injection
try:
# Bypass type checking by casting
results = [
c
async for c in saver.alist(None, limit=malicious_limit) # type: ignore
]
# If it doesn't raise an error, it should at least not execute the injection
# SQLite's parameter binding will try to convert the string to an integer
# which will either fail or treat it as 0
except Exception:
# Expected: SQLite should reject invalid limit values
pass
# Verify the checkpoints table still exists and has all data
# (would have been dropped if injection succeeded)
results = [c async for c in saver.alist(None, limit=None)]
assert len(results) == 5
+125
View File
@@ -182,3 +182,128 @@ class TestSqliteSaver:
with pytest.raises(NotImplementedError, match="AsyncSqliteSaver"):
async for _ in saver.alist(self.config_1):
pass
def test_metadata_predicate_sql_injection_prevention(self) -> None:
"""Test that _metadata_predicate rejects malicious filter keys."""
# Test various SQL injection payloads
malicious_keys = [
"x') OR '1'='1", # Boolean-based injection
"x') OR 1=1 --", # Comment-based injection
"x') UNION SELECT 1,2,3,4,5,6,7 --", # UNION-based injection
"access') = 'public' OR '1'='1' OR json_extract(value, '$.", # Complex injection
"'; DROP TABLE checkpoints; --", # Destructive injection
]
for malicious_key in malicious_keys:
with pytest.raises(ValueError, match="Invalid filter key"):
_metadata_predicate({malicious_key: "dummy"})
def test_checkpoint_search_sql_injection_prevention(self) -> None:
"""Test that SQL injection via malicious filter keys is prevented in checkpoint search."""
with SqliteSaver.from_conn_string(":memory:") as saver:
# Setup: Create checkpoints with different metadata
config_public: RunnableConfig = {
"configurable": {
"thread_id": "thread-public",
"checkpoint_ns": "",
}
}
config_private: RunnableConfig = {
"configurable": {
"thread_id": "thread-private",
"checkpoint_ns": "",
}
}
checkpoint_public = empty_checkpoint()
checkpoint_private = empty_checkpoint()
metadata_public: CheckpointMetadata = {
"access": "public",
"data": "public information",
}
metadata_private: CheckpointMetadata = {
"access": "private",
"data": "secret information",
"password": "secret123",
}
saver.put(config_public, checkpoint_public, metadata_public, {})
saver.put(config_private, checkpoint_private, metadata_private, {})
# Normal query - should return only public checkpoint
normal_results = list(saver.list(None, filter={"access": "public"}))
assert len(normal_results) == 1
assert normal_results[0].metadata["access"] == "public"
# SQL injection attempt should raise ValueError
malicious_key = (
"access') = 'public' OR '1'='1' OR json_extract(metadata, '$."
)
with pytest.raises(ValueError, match="Invalid filter key"):
list(saver.list(None, filter={malicious_key: "dummy"}))
def test_limit_parameter_sql_injection_prevention(self) -> None:
"""Test that the limit parameter properly uses parameterized queries to prevent SQL injection."""
with SqliteSaver.from_conn_string(":memory:") as saver:
# Setup: Create multiple checkpoints
for i in range(5):
config: RunnableConfig = {
"configurable": {
"thread_id": f"thread-{i}",
"checkpoint_ns": "",
}
}
checkpoint = empty_checkpoint()
metadata: CheckpointMetadata = {"index": i}
saver.put(config, checkpoint, metadata, {})
# Test that limit works correctly with valid integer
results = list(saver.list(None, limit=2))
assert len(results) == 2
# Test that limit=0 returns no results
results = list(saver.list(None, limit=0))
assert len(results) == 0
# Test that limit=None returns all results
results = list(saver.list(None, limit=None))
assert len(results) == 5
def test_metadata_filter_keys_with_hyphens_and_digits(self) -> None:
"""Metadata keys with hyphens and digit-start should be filterable.
This exposes incorrect JSON path handling (unquoted segments) by asserting
that such filters successfully match saved checkpoints.
"""
with SqliteSaver.from_conn_string(":memory:") as saver:
config: RunnableConfig = {
"configurable": {
"thread_id": "thread-hyphen-digit",
"checkpoint_ns": "",
}
}
checkpoint = empty_checkpoint()
metadata: CheckpointMetadata = {
"access-level": "public",
"user": {"access-level": "nested", "123abc": "ok2"},
"123abc": "ok",
}
saver.put(config, checkpoint, metadata, {})
# Top-level hyphenated key
results = list(saver.list(None, filter={"access-level": "public"}))
assert len(results) == 1
# Nested hyphenated key via dotted path
results = list(saver.list(None, filter={"user.access-level": "nested"}))
assert len(results) == 1
# Top-level digit-starting key
results = list(saver.list(None, filter={"123abc": "ok"}))
assert len(results) == 1
# Nested digit-starting key via dotted path
results = list(saver.list(None, filter={"user.123abc": "ok2"}))
assert len(results) == 1
+135
View File
@@ -1069,6 +1069,141 @@ def test_sql_injection_vulnerability(store: SqliteStore) -> None:
store.search(("docs",), filter={malicious_key: "dummy"})
def test_sql_injection_filter_values(store: SqliteStore) -> None:
"""Test that SQL injection via malicious filter values is properly escaped."""
# Setup: Create documents with different access levels
store.put(("docs",), "doc1", {"access": "public", "title": "Public Document"})
store.put(("docs",), "doc2", {"access": "private", "title": "Private Document"})
store.put(("docs",), "doc3", {"access": "secret", "title": "Secret Document"})
# Test 1: Basic SQL injection attempt with single quote
malicious_value = "public' OR '1'='1"
results = store.search(("docs",), filter={"access": malicious_value})
# Should return 0 results because the malicious value is escaped and won't match anything
assert len(results) == 0, "SQL injection via string value should be blocked"
# Test 2: SQL injection with comment
malicious_value = "public'; --"
results = store.search(("docs",), filter={"access": malicious_value})
assert len(results) == 0, "SQL comment injection should be blocked"
# Test 3: UNION injection attempt
malicious_value = "public' UNION SELECT * FROM store --"
results = store.search(("docs",), filter={"access": malicious_value})
assert len(results) == 0, "UNION injection should be blocked"
# Test 4: Parameterized queries handle strings with null bytes and SQL injection attempts safely
malicious_value = "public\x00' OR '1'='1"
results = store.search(("docs",), filter={"access": malicious_value})
assert len(results) == 0, (
"Parameterized queries treat injection attempts as literal strings"
)
# Test 5: Multiple single quotes
malicious_value = "''''"
results = store.search(("docs",), filter={"access": malicious_value})
assert len(results) == 0, "Multiple quotes should be handled safely"
# Test 6: Legitimate value with single quote should work
store.put(("docs",), "doc4", {"title": "O'Brien's Document", "access": "public"})
results = store.search(("docs",), filter={"title": "O'Brien's Document"})
assert len(results) == 1, "Legitimate single quotes should work"
assert results[0].value["title"] == "O'Brien's Document"
# Test 7: Unicode characters with injection attempt
malicious_value = "public' OR 'א'='א"
results = store.search(("docs",), filter={"access": malicious_value})
assert len(results) == 0, "Unicode-based injection should be blocked"
def test_numeric_filter_safety(store: SqliteStore) -> None:
"""Test that numeric filter values are handled safely."""
# Setup: Create documents with numeric fields
store.put(("items",), "item1", {"price": 10, "quantity": 5})
store.put(("items",), "item2", {"price": 20, "quantity": 3})
store.put(("items",), "item3", {"price": 30, "quantity": 1})
# Test 1: Normal numeric comparison
results = store.search(("items",), filter={"price": {"$gt": 15}})
assert len(results) == 2
assert all(r.value["price"] > 15 for r in results)
# Test 2: Special float values (infinity)
results = store.search(("items",), filter={"price": {"$lt": float("inf")}})
assert len(results) == 3, "All finite values should be less than infinity"
# Test 3: Special float values (negative infinity)
results = store.search(("items",), filter={"price": {"$gt": float("-inf")}})
assert len(results) == 3, (
"All finite values should be greater than negative infinity"
)
# Test 4: NaN handling - NaN comparisons should not cause errors
try:
results = store.search(("items",), filter={"price": {"$eq": float("nan")}})
# NaN never equals anything, including itself, so should return 0 results
assert len(results) == 0
except Exception as e:
pytest.fail(f"NaN handling should not raise exception: {e}")
# Test 5: Very large numbers
results = store.search(("items",), filter={"price": {"$lt": 10**100}})
assert len(results) == 3, "Very large numbers should be handled safely"
# Test 6: Negative numbers
store.put(("items",), "item4", {"price": -10, "quantity": 0})
results = store.search(("items",), filter={"price": {"$lt": 0}})
assert len(results) == 1
assert results[0].key == "item4"
def test_boolean_filter_safety(store: SqliteStore) -> None:
"""Test that boolean filter values are handled safely."""
store.put(("flags",), "flag1", {"active": True, "name": "Feature A"})
store.put(("flags",), "flag2", {"active": False, "name": "Feature B"})
store.put(("flags",), "flag3", {"active": True, "name": "Feature C"})
# Test boolean filters
results = store.search(("flags",), filter={"active": True})
assert len(results) == 2
assert all(r.value["active"] is True for r in results)
results = store.search(("flags",), filter={"active": False})
assert len(results) == 1
assert results[0].value["active"] is False
def test_filter_keys_with_hyphens_and_digits(store: SqliteStore) -> None:
"""Keys with hyphens or leading digits should be queryable via filters.
Current unquoted JSON path construction (e.g., '$.access-level' or '$.123abc')
is not valid JSON1 syntax, so this test will catch regressions in path handling.
"""
# Documents with top-level and nested keys requiring bracket-quoted JSON paths
store.put(
("docs",),
"hyphen",
{"access-level": "public", "user": {"access-level": "nested"}},
)
store.put(("docs",), "digit", {"123abc": "ok", "user": {"123abc": "ok2"}})
# Top-level hyphenated key
results = store.search(("docs",), filter={"access-level": "public"})
assert [r.key for r in results] == ["hyphen"]
# Nested hyphenated key via dotted path
results = store.search(("docs",), filter={"user.access-level": "nested"})
assert [r.key for r in results] == ["hyphen"]
# Top-level digit-starting key
results = store.search(("docs",), filter={"123abc": "ok"})
assert [r.key for r in results] == ["digit"]
# Nested digit-starting key via dotted path
results = store.search(("docs",), filter={"user.123abc": "ok2"})
assert [r.key for r in results] == ["digit"]
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
def test_non_ascii(
fake_embeddings: CharacterEmbeddings,
+205 -121
View File
@@ -246,7 +246,7 @@ wheels = [
[[package]]
name = "langgraph-checkpoint"
version = "2.1.2"
version = "3.0.1"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },
@@ -256,7 +256,7 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "langchain-core", specifier = ">=0.2.38" },
{ name = "ormsgpack", specifier = ">=1.10.0" },
{ name = "ormsgpack", specifier = ">=1.12.0" },
]
[package.metadata.requires-dev]
@@ -274,10 +274,26 @@ dev = [
{ name = "redis" },
{ name = "ruff" },
]
lint = [
{ name = "codespell" },
{ name = "mypy" },
{ name = "ruff" },
]
test = [
{ name = "dataclasses-json" },
{ name = "numpy" },
{ name = "pandas" },
{ name = "pandas-stubs", specifier = ">=2.2.2.240807" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-mock" },
{ name = "pytest-watcher" },
{ name = "redis" },
]
[[package]]
name = "langgraph-checkpoint-sqlite"
version = "2.0.11"
version = "3.0.1"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },
@@ -297,6 +313,19 @@ dev = [
{ name = "pytest-watcher" },
{ name = "ruff" },
]
lint = [
{ name = "codespell" },
{ name = "mypy" },
{ name = "ruff" },
]
test = [
{ name = "langgraph-checkpoint" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-mock" },
{ name = "pytest-retry" },
{ name = "pytest-watcher" },
]
[package.metadata]
requires-dist = [
@@ -317,6 +346,19 @@ dev = [
{ name = "pytest-watcher" },
{ name = "ruff" },
]
lint = [
{ name = "codespell" },
{ name = "mypy" },
{ name = "ruff" },
]
test = [
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-mock" },
{ name = "pytest-retry", specifier = ">=1.7.0" },
{ name = "pytest-watcher" },
]
[[package]]
name = "langsmith"
@@ -469,42 +511,57 @@ wheels = [
[[package]]
name = "ormsgpack"
version = "1.10.0"
version = "1.12.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/92/36/44eed5ef8ce93cded76a576780bab16425ce7876f10d3e2e6265e46c21ea/ormsgpack-1.10.0.tar.gz", hash = "sha256:7f7a27efd67ef22d7182ec3b7fa7e9d147c3ad9be2a24656b23c989077e08b16", size = 58629, upload-time = "2025-05-24T19:07:53.944Z" }
sdist = { url = "https://files.pythonhosted.org/packages/6c/67/d5ef41c3b4a94400be801984ef7c7fc9623e1a82b643e74eeec367e7462b/ormsgpack-1.12.0.tar.gz", hash = "sha256:94be818fdbb0285945839b88763b269987787cb2f7ef280cad5d6ec815b7e608", size = 49959, upload-time = "2025-11-04T18:30:10.083Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/74/c2dd5daf069e3798d09d5746000f9b210de04df83834e5cb47f0ace51892/ormsgpack-1.10.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8a52c7ce7659459f3dc8dec9fd6a6c76f855a0a7e2b61f26090982ac10b95216", size = 376280, upload-time = "2025-05-24T19:06:51.3Z" },
{ url = "https://files.pythonhosted.org/packages/78/7b/30ff4bffb709e8a242005a8c4d65714fd96308ad640d31cff1b85c0d8cc4/ormsgpack-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:060f67fe927582f4f63a1260726d019204b72f460cf20930e6c925a1d129f373", size = 204335, upload-time = "2025-05-24T19:06:53.442Z" },
{ url = "https://files.pythonhosted.org/packages/8f/3f/c95b7d142819f801a0acdbd04280e8132e43b6e5a8920173e8eb92ea0e6a/ormsgpack-1.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7058ef6092f995561bf9f71d6c9a4da867b6cc69d2e94cb80184f579a3ceed5", size = 215373, upload-time = "2025-05-24T19:06:55.153Z" },
{ url = "https://files.pythonhosted.org/packages/ef/1a/e30f4bcf386db2015d1686d1da6110c95110294d8ea04f86091dd5eb3361/ormsgpack-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f6f3509c1b0e51b15552d314b1d409321718122e90653122ce4b997f01453a", size = 216469, upload-time = "2025-05-24T19:06:56.555Z" },
{ url = "https://files.pythonhosted.org/packages/96/fc/7e44aeade22b91883586f45b7278c118fd210834c069774891447f444fc9/ormsgpack-1.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c1edafd5c72b863b1f875ec31c529f09c872a5ff6fe473b9dfaf188ccc3227", size = 384590, upload-time = "2025-05-24T19:06:58.286Z" },
{ url = "https://files.pythonhosted.org/packages/ec/78/f92c24e8446697caa83c122f10b6cf5e155eddf81ce63905c8223a260482/ormsgpack-1.10.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c780b44107a547a9e9327270f802fa4d6b0f6667c9c03c3338c0ce812259a0f7", size = 478891, upload-time = "2025-05-24T19:07:00.126Z" },
{ url = "https://files.pythonhosted.org/packages/5a/75/87449690253c64bea2b663c7c8f2dbc9ad39d73d0b38db74bdb0f3947b16/ormsgpack-1.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:137aab0d5cdb6df702da950a80405eb2b7038509585e32b4e16289604ac7cb84", size = 390121, upload-time = "2025-05-24T19:07:01.777Z" },
{ url = "https://files.pythonhosted.org/packages/69/cc/c83257faf3a5169ec29dd87121317a25711da9412ee8c1e82f2e1a00c0be/ormsgpack-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:3e666cb63030538fa5cd74b1e40cb55b6fdb6e2981f024997a288bf138ebad07", size = 121196, upload-time = "2025-05-24T19:07:03.47Z" },
{ url = "https://files.pythonhosted.org/packages/30/27/7da748bc0d7d567950a378dee5a32477ed5d15462ab186918b5f25cac1ad/ormsgpack-1.10.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4bb7df307e17b36cbf7959cd642c47a7f2046ae19408c564e437f0ec323a7775", size = 376275, upload-time = "2025-05-24T19:07:05.128Z" },
{ url = "https://files.pythonhosted.org/packages/7b/65/c082cc8c74a914dbd05af0341c761c73c3d9960b7432bbf9b8e1e20811af/ormsgpack-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8817ae439c671779e1127ee62f0ac67afdeaeeacb5f0db45703168aa74a2e4af", size = 204335, upload-time = "2025-05-24T19:07:06.423Z" },
{ url = "https://files.pythonhosted.org/packages/46/62/17ef7e5d9766c79355b9c594cc9328c204f1677bc35da0595cc4e46449f0/ormsgpack-1.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f345f81e852035d80232e64374d3a104139d60f8f43c6c5eade35c4bac5590e", size = 215372, upload-time = "2025-05-24T19:07:08.149Z" },
{ url = "https://files.pythonhosted.org/packages/4e/92/7c91e8115fc37e88d1a35e13200fda3054ff5d2e5adf017345e58cea4834/ormsgpack-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21de648a1c7ef692bdd287fb08f047bd5371d7462504c0a7ae1553c39fee35e3", size = 216470, upload-time = "2025-05-24T19:07:09.903Z" },
{ url = "https://files.pythonhosted.org/packages/2c/86/ce053c52e2517b90e390792d83e926a7a523c1bce5cc63d0a7cd05ce6cf6/ormsgpack-1.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3a7d844ae9cbf2112c16086dd931b2acefce14cefd163c57db161170c2bfa22b", size = 384591, upload-time = "2025-05-24T19:07:11.24Z" },
{ url = "https://files.pythonhosted.org/packages/07/e8/2ad59f2ab222c6029e500bc966bfd2fe5cb099f8ab6b7ebeb50ddb1a6fe5/ormsgpack-1.10.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e4d80585403d86d7f800cf3d0aafac1189b403941e84e90dd5102bb2b92bf9d5", size = 478892, upload-time = "2025-05-24T19:07:13.147Z" },
{ url = "https://files.pythonhosted.org/packages/f4/73/f55e4b47b7b18fd8e7789680051bf830f1e39c03f1d9ed993cd0c3e97215/ormsgpack-1.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:da1de515a87e339e78a3ccf60e39f5fb740edac3e9e82d3c3d209e217a13ac08", size = 390122, upload-time = "2025-05-24T19:07:14.557Z" },
{ url = "https://files.pythonhosted.org/packages/f7/87/073251cdb93d4c6241748568b3ad1b2a76281fb2002eed16a3a4043d61cf/ormsgpack-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:57c4601812684024132cbb32c17a7d4bb46ffc7daf2fddf5b697391c2c4f142a", size = 121197, upload-time = "2025-05-24T19:07:15.981Z" },
{ url = "https://files.pythonhosted.org/packages/99/95/f3ab1a7638f6aa9362e87916bb96087fbbc5909db57e19f12ad127560e1e/ormsgpack-1.10.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4e159d50cd4064d7540e2bc6a0ab66eab70b0cc40c618b485324ee17037527c0", size = 376806, upload-time = "2025-05-24T19:07:17.221Z" },
{ url = "https://files.pythonhosted.org/packages/6c/2b/42f559f13c0b0f647b09d749682851d47c1a7e48308c43612ae6833499c8/ormsgpack-1.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eeb47c85f3a866e29279d801115b554af0fefc409e2ed8aa90aabfa77efe5cc6", size = 204433, upload-time = "2025-05-24T19:07:18.569Z" },
{ url = "https://files.pythonhosted.org/packages/45/42/1ca0cb4d8c80340a89a4af9e6d8951fb8ba0d076a899d2084eadf536f677/ormsgpack-1.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c28249574934534c9bd5dce5485c52f21bcea0ee44d13ece3def6e3d2c3798b5", size = 215547, upload-time = "2025-05-24T19:07:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/0a/38/184a570d7c44c0260bc576d1daaac35b2bfd465a50a08189518505748b9a/ormsgpack-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1957dcadbb16e6a981cd3f9caef9faf4c2df1125e2a1b702ee8236a55837ce07", size = 216746, upload-time = "2025-05-24T19:07:21.83Z" },
{ url = "https://files.pythonhosted.org/packages/69/2f/1aaffd08f6b7fdc2a57336a80bdfb8df24e6a65ada5aa769afecfcbc6cc6/ormsgpack-1.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b29412558c740bf6bac156727aa85ac67f9952cd6f071318f29ee72e1a76044", size = 384783, upload-time = "2025-05-24T19:07:23.674Z" },
{ url = "https://files.pythonhosted.org/packages/a9/63/3e53d6f43bb35e00c98f2b8ab2006d5138089ad254bc405614fbf0213502/ormsgpack-1.10.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6933f350c2041ec189fe739f0ba7d6117c8772f5bc81f45b97697a84d03020dd", size = 479076, upload-time = "2025-05-24T19:07:25.047Z" },
{ url = "https://files.pythonhosted.org/packages/b8/19/fa1121b03b61402bb4d04e35d164e2320ef73dfb001b57748110319dd014/ormsgpack-1.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a86de06d368fcc2e58b79dece527dc8ca831e0e8b9cec5d6e633d2777ec93d0", size = 390447, upload-time = "2025-05-24T19:07:26.568Z" },
{ url = "https://files.pythonhosted.org/packages/b0/0d/73143ecb94ac4a5dcba223402139240a75dee0cc6ba8a543788a5646407a/ormsgpack-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:35fa9f81e5b9a0dab42e09a73f7339ecffdb978d6dbf9deb2ecf1e9fc7808722", size = 121401, upload-time = "2025-05-24T19:07:28.308Z" },
{ url = "https://files.pythonhosted.org/packages/61/f8/ec5f4e03268d0097545efaab2893aa63f171cf2959cb0ea678a5690e16a1/ormsgpack-1.10.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d816d45175a878993b7372bd5408e0f3ec5a40f48e2d5b9d8f1cc5d31b61f1f", size = 376806, upload-time = "2025-05-24T19:07:29.555Z" },
{ url = "https://files.pythonhosted.org/packages/c1/19/b3c53284aad1e90d4d7ed8c881a373d218e16675b8b38e3569d5b40cc9b8/ormsgpack-1.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a90345ccb058de0f35262893751c603b6376b05f02be2b6f6b7e05d9dd6d5643", size = 204433, upload-time = "2025-05-24T19:07:30.977Z" },
{ url = "https://files.pythonhosted.org/packages/09/0b/845c258f59df974a20a536c06cace593698491defdd3d026a8a5f9b6e745/ormsgpack-1.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:144b5e88f1999433e54db9d637bae6fe21e935888be4e3ac3daecd8260bd454e", size = 215549, upload-time = "2025-05-24T19:07:32.345Z" },
{ url = "https://files.pythonhosted.org/packages/61/56/57fce8fb34ca6c9543c026ebebf08344c64dbb7b6643d6ddd5355d37e724/ormsgpack-1.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2190b352509d012915921cca76267db136cd026ddee42f1b0d9624613cc7058c", size = 216747, upload-time = "2025-05-24T19:07:34.075Z" },
{ url = "https://files.pythonhosted.org/packages/b8/3f/655b5f6a2475c8d209f5348cfbaaf73ce26237b92d79ef2ad439407dd0fa/ormsgpack-1.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:86fd9c1737eaba43d3bb2730add9c9e8b5fbed85282433705dd1b1e88ea7e6fb", size = 384785, upload-time = "2025-05-24T19:07:35.83Z" },
{ url = "https://files.pythonhosted.org/packages/4b/94/687a0ad8afd17e4bce1892145d6a1111e58987ddb176810d02a1f3f18686/ormsgpack-1.10.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:33afe143a7b61ad21bb60109a86bb4e87fec70ef35db76b89c65b17e32da7935", size = 479076, upload-time = "2025-05-24T19:07:37.533Z" },
{ url = "https://files.pythonhosted.org/packages/c8/34/68925232e81e0e062a2f0ac678f62aa3b6f7009d6a759e19324dbbaebae7/ormsgpack-1.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f23d45080846a7b90feabec0d330a9cc1863dc956728412e4f7986c80ab3a668", size = 390446, upload-time = "2025-05-24T19:07:39.469Z" },
{ url = "https://files.pythonhosted.org/packages/12/ad/f4e1a36a6d1714afb7ffb74b3ababdcb96529cf4e7a216f9f7c8eda837b6/ormsgpack-1.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:534d18acb805c75e5fba09598bf40abe1851c853247e61dda0c01f772234da69", size = 121399, upload-time = "2025-05-24T19:07:40.854Z" },
{ url = "https://files.pythonhosted.org/packages/4d/0c/8f45fcd22c95190b05d4fba71375d8f783a9e3b0b6aaf476812b693e3868/ormsgpack-1.12.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e08904c232358b94a682ccfbb680bc47d3fd5c424bb7dccb65974dd20c95e8e1", size = 369156, upload-time = "2025-11-04T18:29:17.629Z" },
{ url = "https://files.pythonhosted.org/packages/d2/f8/c7adc093d4ceb05e38786906815f868210f808326c27f8124b4d9466a26b/ormsgpack-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9ed7a4b0037d69c8ba7e670e03ee65ae8d5c5114a409e73c5770d7fb5e4b895", size = 195743, upload-time = "2025-11-04T18:29:18.964Z" },
{ url = "https://files.pythonhosted.org/packages/fe/b8/bf002648fa6c150ed6157837b00303edafb35f65c352429463f83214de18/ormsgpack-1.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:db2928525b684f3f2af0367aef7ae8d20cde37fc5349c700017129d493a755aa", size = 206472, upload-time = "2025-11-04T18:29:19.951Z" },
{ url = "https://files.pythonhosted.org/packages/23/d6/1f445947c95a931bb189b7864a3f9dcbeebe7dcbc1b3c7387427b3779228/ormsgpack-1.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45f911d9c5b23d11e49ff03fc8f9566745a2b1a7d9033733a1c0a2fa9301cd60", size = 207959, upload-time = "2025-11-04T18:29:21.282Z" },
{ url = "https://files.pythonhosted.org/packages/5c/9c/dd8ccd7553a5c1d0b4b69a0541611983a2f9bc2e8c5708a4bfffc0100468/ormsgpack-1.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:98c54ae6fd682b2aceb264505af9b2255f3df9d84e6e4369bc44d2110f1f311d", size = 377659, upload-time = "2025-11-04T18:29:22.561Z" },
{ url = "https://files.pythonhosted.org/packages/3d/08/3282d8f6330e742d4cdbcfbe2c100b403ae5526ae82a1c1b6a11b7967e37/ormsgpack-1.12.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:857ab987c3502de08258cc4baf0e87267cb2c80931601084e13df3c355b1ab9d", size = 471391, upload-time = "2025-11-04T18:29:23.663Z" },
{ url = "https://files.pythonhosted.org/packages/18/d4/94a2fbfd4837754bda7a099b6a23b9d40aba9e76e1af8b8fb8133612eb54/ormsgpack-1.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:27579d45dc502ee736238e1024559cb0a01aa72a3b68827448b8edf6a2dcdc9c", size = 381501, upload-time = "2025-11-04T18:29:24.771Z" },
{ url = "https://files.pythonhosted.org/packages/d8/c6/1a9fa122cb5deb10b067bbaa43165b12291a914cc0ce364988ff17bbf405/ormsgpack-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:c78379d054760875540cf2e81f28da1bb78d09fda3eabdbeb6c53b3e297158cb", size = 112715, upload-time = "2025-11-04T18:29:26.016Z" },
{ url = "https://files.pythonhosted.org/packages/1a/ba/3cae83cf36420c1c8dd294f16c852c03313aafe2439a165c4c6ac611b1d0/ormsgpack-1.12.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c40d86d77391b18dd34de5295e3de2b8ad818bcab9c9def4121c8ec5c9714ae4", size = 369159, upload-time = "2025-11-04T18:29:27.057Z" },
{ url = "https://files.pythonhosted.org/packages/97/d4/5e176309e01a8b9098d80201aac1eb7db9336c3b5b4fa6254a2bbb0d0fa0/ormsgpack-1.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:777b7fab364dc0f200bb382a98a385c8222ffa6a2333d627d763797326202c86", size = 195744, upload-time = "2025-11-04T18:29:28.069Z" },
{ url = "https://files.pythonhosted.org/packages/4f/83/6d80c8c5571639c000a39f38f77752dfaf9d9e552d775331e8d280f66a4e/ormsgpack-1.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b5089ad9dd5b3d3013b245a55e4abaea2f8ad70f4a78e1b002127b02340004", size = 206474, upload-time = "2025-11-04T18:29:29.034Z" },
{ url = "https://files.pythonhosted.org/packages/5e/e6/940311e48dc0cfc3e212bd7007a21ed0825158638057687d804f2c5c2cca/ormsgpack-1.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deaf0c87cace7bc08fbf68c5cc66605b593df6427e9f4de235b2da358787e008", size = 207959, upload-time = "2025-11-04T18:29:30.315Z" },
{ url = "https://files.pythonhosted.org/packages/1a/e3/fbe94b0a311815343b86a95a0627e4901b11ff6fd522679ca29a2a88c99b/ormsgpack-1.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f62d476fe28bc5675d9aff30341bfa9f41d7de332c5b63fbbe9aaf6bb7ec74d4", size = 377666, upload-time = "2025-11-04T18:29:31.38Z" },
{ url = "https://files.pythonhosted.org/packages/a3/3b/229cfa28076798ffb619aaa854b842de3f2ed5ea4e6509bf34d14c038c4d/ormsgpack-1.12.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ded7810095b887e28434f32f5a345d354e88cf851bab3c5435aeb86a718618d2", size = 471394, upload-time = "2025-11-04T18:29:32.521Z" },
{ url = "https://files.pythonhosted.org/packages/6b/bd/4eae4ab35586e4175c07acb5f98aec83aa9d8987f71ea0443aa900191bdf/ormsgpack-1.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f72a1dea0c4ae7c4101dcfbe8133f274a9d769d0b87fe5188db4fab07ffabaee", size = 381506, upload-time = "2025-11-04T18:29:33.533Z" },
{ url = "https://files.pythonhosted.org/packages/dd/51/f9d56d6d015cbfa1ce9a4358ca30a41744644f0cf606e060d7203efe5af8/ormsgpack-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:8f479bfef847255d7d0b12c7a198f6a21490155da2da3062e082ba370893d4a1", size = 112707, upload-time = "2025-11-04T18:29:34.898Z" },
{ url = "https://files.pythonhosted.org/packages/f4/07/bb189ef7072979f2f96e8716e952172efdce9c54930aa0814bec73aee19b/ormsgpack-1.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:3583ca410e4502144b2594170542e4bbef7b15643fd1208703ae820f11029036", size = 106533, upload-time = "2025-11-04T18:29:36.112Z" },
{ url = "https://files.pythonhosted.org/packages/a2/f2/c1036b2775fcc0cfa5fd618c53bcd3b862ee07298fb627f03af4c7982f84/ormsgpack-1.12.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e0c1e08b64d99076fee155276097489b82cc56e8d5951c03c721a65a32f44494", size = 369538, upload-time = "2025-11-04T18:29:37.125Z" },
{ url = "https://files.pythonhosted.org/packages/d9/ca/526c4ae02f3cb34621af91bf8282a10d666757c2e0c6ff391ff5d403d607/ormsgpack-1.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fd43bcb299131690b8e0677af172020b2ada8e625169034b42ac0c13adf84aa", size = 195872, upload-time = "2025-11-04T18:29:38.34Z" },
{ url = "https://files.pythonhosted.org/packages/7f/0f/83bb7968e9715f6a85be53d041b1e6324a05428f56b8b980dac866886871/ormsgpack-1.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0149d595341e22ead340bf281b2995c4cc7dc8d522a6b5f575fe17aa407604", size = 206469, upload-time = "2025-11-04T18:29:39.749Z" },
{ url = "https://files.pythonhosted.org/packages/02/e3/9e93ca1065f2d4af035804a842b1ff3025bab580c7918239bb225cd1fee2/ormsgpack-1.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f19a1b27d169deb553c80fd10b589fc2be1fc14cee779fae79fcaf40db04de2b", size = 208273, upload-time = "2025-11-04T18:29:40.769Z" },
{ url = "https://files.pythonhosted.org/packages/b3/d8/6d6ef901b3a8b8f3ab8836b135a56eb7f66c559003e251d9530bedb12627/ormsgpack-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6f28896942d655064940dfe06118b7ce1e3468d051483148bf02c99ec157483a", size = 377839, upload-time = "2025-11-04T18:29:42.092Z" },
{ url = "https://files.pythonhosted.org/packages/4c/72/fcb704bfa4c2c3a37b647d597cc45a13cffc9d50baac635a9ad620731d29/ormsgpack-1.12.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9396efcfa48b4abbc06e44c5dbc3c4574a8381a80cb4cd01eea15d28b38c554e", size = 471446, upload-time = "2025-11-04T18:29:43.133Z" },
{ url = "https://files.pythonhosted.org/packages/84/f8/402e4e3eb997c2ee534c99bec4b5bb359c2a1f9edadf043e254a71e11378/ormsgpack-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:96586ed537a5fb386a162c4f9f7d8e6f76e07b38a990d50c73f11131e00ff040", size = 381783, upload-time = "2025-11-04T18:29:44.466Z" },
{ url = "https://files.pythonhosted.org/packages/f0/8d/5897b700360bc00911b70ae5ef1134ee7abf5baa81a92a4be005917d3dfd/ormsgpack-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e70387112fb3870e4844de090014212cdcf1342f5022047aecca01ec7de05d7a", size = 112943, upload-time = "2025-11-04T18:29:45.468Z" },
{ url = "https://files.pythonhosted.org/packages/5b/44/1e73649f79bb96d6cf9e5bcbac68b6216d238bba80af351c4c0cbcf7ee15/ormsgpack-1.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:d71290a23de5d4829610c42665d816c661ecad8979883f3f06b2e3ab9639962e", size = 106688, upload-time = "2025-11-04T18:29:46.411Z" },
{ url = "https://files.pythonhosted.org/packages/2e/e8/35f11ce9313111488b26b3035e4cbe55caa27909c0b6c8b5b5cd59f9661e/ormsgpack-1.12.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:766f2f3b512d85cd375b26a8b1329b99843560b50b93d3880718e634ad4a5de5", size = 369574, upload-time = "2025-11-04T18:29:47.431Z" },
{ url = "https://files.pythonhosted.org/packages/61/b0/77461587f412d4e598d3687bafe23455ed0f26269f44be20252eddaa624e/ormsgpack-1.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84b285b1f3f185aad7da45641b873b30acfd13084cf829cf668c4c6480a81583", size = 195893, upload-time = "2025-11-04T18:29:48.735Z" },
{ url = "https://files.pythonhosted.org/packages/c6/67/e197ceb04c3b550589e5407fc9fdae10f4e2e2eba5fdac921a269e02e974/ormsgpack-1.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e23604fc79fe110292cb365f4c8232e64e63a34f470538be320feae3921f271b", size = 206503, upload-time = "2025-11-04T18:29:49.99Z" },
{ url = "https://files.pythonhosted.org/packages/0b/b1/7fa8ba82a25cef678983c7976f85edeef5014f5c26495f338258e6a3cf1c/ormsgpack-1.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc32b156c113a0fae2975051417d8d9a7a5247c34b2d7239410c46b75ce9348a", size = 208257, upload-time = "2025-11-04T18:29:51.007Z" },
{ url = "https://files.pythonhosted.org/packages/ce/b1/759e999390000d2589e6d0797f7265e6ec28378547075d28d3736248ab63/ormsgpack-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:94ac500dd10c20fa8b8a23bc55606250bfe711bf9716828d9f3d44dfd1f25668", size = 377852, upload-time = "2025-11-04T18:29:52.103Z" },
{ url = "https://files.pythonhosted.org/packages/51/e7/0af737c94272494d9d84a3c29cc42c973ef7fd2342917020906596db863c/ormsgpack-1.12.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c5201ff7ec24f721f813a182885a17064cffdbe46b2412685a52e6374a872c8f", size = 471456, upload-time = "2025-11-04T18:29:53.336Z" },
{ url = "https://files.pythonhosted.org/packages/f4/ba/c81f0aa4f19fbf457213395945b672e6fde3ce777e3587456e7f0fca2147/ormsgpack-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a9740bb3839c9368aacae1cbcfc474ee6976458f41cc135372b7255d5206c953", size = 381813, upload-time = "2025-11-04T18:29:54.394Z" },
{ url = "https://files.pythonhosted.org/packages/ce/15/429c72d64323503fd42cc4ca8398930ded8aa8b3470df8a86b3bbae7a35c/ormsgpack-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ed37f29772432048b58174e920a1d4c4cde0404a5d448d3d8bbcc95d86a6918", size = 112949, upload-time = "2025-11-04T18:29:55.371Z" },
{ url = "https://files.pythonhosted.org/packages/55/b9/e72c451a40f8c57bfc229e0b8e536ecea7203c8f0a839676df2ffb605c62/ormsgpack-1.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:b03994bbec5d6d42e03d6604e327863f885bde67aa61e06107ce1fa5bdd3e71d", size = 106689, upload-time = "2025-11-04T18:29:56.262Z" },
{ url = "https://files.pythonhosted.org/packages/13/16/13eab1a75da531b359105fdee90dda0b6bd1ca0a09880250cf91d8bdfdea/ormsgpack-1.12.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0f3981ba3cba80656012090337e548e597799e14b41e3d0b595ab5ab05a23d7f", size = 369620, upload-time = "2025-11-04T18:29:57.255Z" },
{ url = "https://files.pythonhosted.org/packages/a0/c1/cbcc38b7af4ce58d8893e56d3595c0c8dcd117093bf048f889cf351bdba0/ormsgpack-1.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:901f6f55184d6776dbd5183cbce14caf05bf7f467eef52faf9b094686980bf71", size = 195925, upload-time = "2025-11-04T18:29:58.34Z" },
{ url = "https://files.pythonhosted.org/packages/5c/59/4fa4dc0681490e12b75333440a1c0fd9741b0ebff272b1db4a29d35c2021/ormsgpack-1.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e13b15412571422b711b40f45e3fe6d993ea3314b5e97d1a853fe99226c5effc", size = 206594, upload-time = "2025-11-04T18:29:59.329Z" },
{ url = "https://files.pythonhosted.org/packages/39/67/249770896bc32bb91b22c30256961f935d0915cbcf6e289a7fc961d9b14c/ormsgpack-1.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91fa8a452553a62e5fb3fbab471e7faf7b3bec3c87a2f355ebf3d7aab290fe4f", size = 208307, upload-time = "2025-11-04T18:30:00.377Z" },
{ url = "https://files.pythonhosted.org/packages/07/0a/e041a248cd72f2f4c07e155913e0a3ede4c86cf21a40ae6cd79f135f2847/ormsgpack-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74ec101f69624695eec4ce7c953192d97748254abe78fb01b591f06d529e1952", size = 377844, upload-time = "2025-11-04T18:30:01.389Z" },
{ url = "https://files.pythonhosted.org/packages/d8/71/6f7773e4ffda73a358ce4bba69b3e8bee9d40a7a06315e4c1cd7a3ea9d02/ormsgpack-1.12.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9bbf7896580848326c1f9bd7531f264e561f98db7e08e15aa75963d83832c717", size = 471572, upload-time = "2025-11-04T18:30:02.486Z" },
{ url = "https://files.pythonhosted.org/packages/65/29/af6769a4289c07acc71e7bda1d64fb31800563147d73142686e185e82348/ormsgpack-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7567917da613b8f8d591c1674e411fd3404bea41ef2b9a0e0a1e049c0f9406d7", size = 381842, upload-time = "2025-11-04T18:30:03.799Z" },
{ url = "https://files.pythonhosted.org/packages/0b/dd/0a86195ee7a1a96c088aefc8504385e881cf56f4563ed81bafe21cbf1fb0/ormsgpack-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e418256c5d8622b8bc92861936f7c6a0131355e7bcad88a42102ae8227f8a1c", size = 113008, upload-time = "2025-11-04T18:30:04.777Z" },
{ url = "https://files.pythonhosted.org/packages/4c/57/fafc79e32f3087f6f26f509d80b8167516326bfea38d30502627c01617e0/ormsgpack-1.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:433ace29aa02713554f714c62a4e4dcad0c9e32674ba4f66742c91a4c3b1b969", size = 106648, upload-time = "2025-11-04T18:30:05.708Z" },
{ url = "https://files.pythonhosted.org/packages/b3/cf/5d58d9b132128d2fe5d586355dde76af386554abef00d608f66b913bff1f/ormsgpack-1.12.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e57164be4ca34b64e210ec515059193280ac84df4d6f31a6fcbfb2fc8436de55", size = 369803, upload-time = "2025-11-04T18:30:06.728Z" },
{ url = "https://files.pythonhosted.org/packages/67/42/968a2da361eaff2e4cbb17c82c7599787babf16684110ad70409646cc1e4/ormsgpack-1.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:904f96289deaa92fc6440b122edc27c5bdc28234edd63717f6d853d88c823a83", size = 195991, upload-time = "2025-11-04T18:30:07.713Z" },
{ url = "https://files.pythonhosted.org/packages/03/f0/9696c6c6cf8ad35170f0be8d0ef3523cc258083535f6c8071cb8235ebb8b/ormsgpack-1.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b291d086e524a1062d57d1b7b5a8bcaaf29caebf0212fec12fd86240bd33633", size = 208316, upload-time = "2025-11-04T18:30:08.663Z" },
]
[[package]]
@@ -536,7 +593,7 @@ wheels = [
[[package]]
name = "pydantic"
version = "2.11.9"
version = "2.12.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
@@ -544,96 +601,123 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" }
sdist = { url = "https://files.pythonhosted.org/packages/8d/35/d319ed522433215526689bad428a94058b6dd12190ce7ddd78618ac14b28/pydantic-2.12.2.tar.gz", hash = "sha256:7b8fa15b831a4bbde9d5b84028641ac3080a4ca2cbd4a621a661687e741624fd", size = 816358, upload-time = "2025-10-14T15:02:21.842Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" },
{ url = "https://files.pythonhosted.org/packages/6c/98/468cb649f208a6f1279448e6e5247b37ae79cf5e4041186f1e2ef3d16345/pydantic-2.12.2-py3-none-any.whl", hash = "sha256:25ff718ee909acd82f1ff9b1a4acfd781bb23ab3739adaa7144f19a6a4e231ae", size = 460628, upload-time = "2025-10-14T15:02:19.623Z" },
]
[[package]]
name = "pydantic-core"
version = "2.33.2"
version = "2.41.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" },
{ url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" },
{ url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" },
{ url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" },
{ url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" },
{ url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" },
{ url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" },
{ url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" },
{ url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" },
{ url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" },
{ url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" },
{ url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" },
{ url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" },
{ url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" },
{ url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" },
{ url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" },
{ url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" },
{ url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" },
{ url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" },
{ url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" },
{ url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" },
{ url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" },
{ url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" },
{ url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" },
{ url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" },
{ url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" },
{ url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" },
{ url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
{ url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
{ url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
{ url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" },
{ url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" },
{ url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" },
{ url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" },
{ url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" },
{ url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" },
{ url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" },
{ url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" },
{ url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" },
{ url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" },
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
{ url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" },
{ url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" },
{ url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" },
{ url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" },
{ url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" },
{ url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" },
{ url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" },
{ url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" },
{ url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" },
{ url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" },
{ url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" },
{ url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" },
{ url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" },
{ url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" },
{ url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" },
{ url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" },
{ url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" },
{ url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" },
{ url = "https://files.pythonhosted.org/packages/a7/3d/9b8ca77b0f76fcdbf8bc6b72474e264283f461284ca84ac3fde570c6c49a/pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e", size = 2111197, upload-time = "2025-10-14T10:19:43.303Z" },
{ url = "https://files.pythonhosted.org/packages/59/92/b7b0fe6ed4781642232755cb7e56a86e2041e1292f16d9ae410a0ccee5ac/pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b", size = 1917909, upload-time = "2025-10-14T10:19:45.194Z" },
{ url = "https://files.pythonhosted.org/packages/52/8c/3eb872009274ffa4fb6a9585114e161aa1a0915af2896e2d441642929fe4/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd", size = 1969905, upload-time = "2025-10-14T10:19:46.567Z" },
{ url = "https://files.pythonhosted.org/packages/f4/21/35adf4a753bcfaea22d925214a0c5b880792e3244731b3f3e6fec0d124f7/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945", size = 2051938, upload-time = "2025-10-14T10:19:48.237Z" },
{ url = "https://files.pythonhosted.org/packages/7d/d0/cdf7d126825e36d6e3f1eccf257da8954452934ede275a8f390eac775e89/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706", size = 2250710, upload-time = "2025-10-14T10:19:49.619Z" },
{ url = "https://files.pythonhosted.org/packages/2e/1c/af1e6fd5ea596327308f9c8d1654e1285cc3d8de0d584a3c9d7705bf8a7c/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba", size = 2367445, upload-time = "2025-10-14T10:19:51.269Z" },
{ url = "https://files.pythonhosted.org/packages/d3/81/8cece29a6ef1b3a92f956ea6da6250d5b2d2e7e4d513dd3b4f0c7a83dfea/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b", size = 2072875, upload-time = "2025-10-14T10:19:52.671Z" },
{ url = "https://files.pythonhosted.org/packages/e3/37/a6a579f5fc2cd4d5521284a0ab6a426cc6463a7b3897aeb95b12f1ba607b/pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d", size = 2191329, upload-time = "2025-10-14T10:19:54.214Z" },
{ url = "https://files.pythonhosted.org/packages/ae/03/505020dc5c54ec75ecba9f41119fd1e48f9e41e4629942494c4a8734ded1/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700", size = 2151658, upload-time = "2025-10-14T10:19:55.843Z" },
{ url = "https://files.pythonhosted.org/packages/cb/5d/2c0d09fb53aa03bbd2a214d89ebfa6304be7df9ed86ee3dc7770257f41ee/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6", size = 2316777, upload-time = "2025-10-14T10:19:57.607Z" },
{ url = "https://files.pythonhosted.org/packages/ea/4b/c2c9c8f5e1f9c864b57d08539d9d3db160e00491c9f5ee90e1bfd905e644/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9", size = 2320705, upload-time = "2025-10-14T10:19:59.016Z" },
{ url = "https://files.pythonhosted.org/packages/28/c3/a74c1c37f49c0a02c89c7340fafc0ba816b29bd495d1a31ce1bdeacc6085/pydantic_core-2.41.4-cp310-cp310-win32.whl", hash = "sha256:0cf2a1f599efe57fa0051312774280ee0f650e11152325e41dfd3018ef2c1b57", size = 1975464, upload-time = "2025-10-14T10:20:00.581Z" },
{ url = "https://files.pythonhosted.org/packages/d6/23/5dd5c1324ba80303368f7569e2e2e1a721c7d9eb16acb7eb7b7f85cb1be2/pydantic_core-2.41.4-cp310-cp310-win_amd64.whl", hash = "sha256:a8c2e340d7e454dc3340d3d2e8f23558ebe78c98aa8f68851b04dcb7bc37abdc", size = 2024497, upload-time = "2025-10-14T10:20:03.018Z" },
{ url = "https://files.pythonhosted.org/packages/62/4c/f6cbfa1e8efacd00b846764e8484fe173d25b8dab881e277a619177f3384/pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80", size = 2109062, upload-time = "2025-10-14T10:20:04.486Z" },
{ url = "https://files.pythonhosted.org/packages/21/f8/40b72d3868896bfcd410e1bd7e516e762d326201c48e5b4a06446f6cf9e8/pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae", size = 1916301, upload-time = "2025-10-14T10:20:06.857Z" },
{ url = "https://files.pythonhosted.org/packages/94/4d/d203dce8bee7faeca791671c88519969d98d3b4e8f225da5b96dad226fc8/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827", size = 1968728, upload-time = "2025-10-14T10:20:08.353Z" },
{ url = "https://files.pythonhosted.org/packages/65/f5/6a66187775df87c24d526985b3a5d78d861580ca466fbd9d4d0e792fcf6c/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f", size = 2050238, upload-time = "2025-10-14T10:20:09.766Z" },
{ url = "https://files.pythonhosted.org/packages/5e/b9/78336345de97298cf53236b2f271912ce11f32c1e59de25a374ce12f9cce/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def", size = 2249424, upload-time = "2025-10-14T10:20:11.732Z" },
{ url = "https://files.pythonhosted.org/packages/99/bb/a4584888b70ee594c3d374a71af5075a68654d6c780369df269118af7402/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2", size = 2366047, upload-time = "2025-10-14T10:20:13.647Z" },
{ url = "https://files.pythonhosted.org/packages/5f/8d/17fc5de9d6418e4d2ae8c675f905cdafdc59d3bf3bf9c946b7ab796a992a/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8", size = 2071163, upload-time = "2025-10-14T10:20:15.307Z" },
{ url = "https://files.pythonhosted.org/packages/54/e7/03d2c5c0b8ed37a4617430db68ec5e7dbba66358b629cd69e11b4d564367/pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265", size = 2190585, upload-time = "2025-10-14T10:20:17.3Z" },
{ url = "https://files.pythonhosted.org/packages/be/fc/15d1c9fe5ad9266a5897d9b932b7f53d7e5cfc800573917a2c5d6eea56ec/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c", size = 2150109, upload-time = "2025-10-14T10:20:19.143Z" },
{ url = "https://files.pythonhosted.org/packages/26/ef/e735dd008808226c83ba56972566138665b71477ad580fa5a21f0851df48/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a", size = 2315078, upload-time = "2025-10-14T10:20:20.742Z" },
{ url = "https://files.pythonhosted.org/packages/90/00/806efdcf35ff2ac0f938362350cd9827b8afb116cc814b6b75cf23738c7c/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e", size = 2318737, upload-time = "2025-10-14T10:20:22.306Z" },
{ url = "https://files.pythonhosted.org/packages/41/7e/6ac90673fe6cb36621a2283552897838c020db343fa86e513d3f563b196f/pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03", size = 1974160, upload-time = "2025-10-14T10:20:23.817Z" },
{ url = "https://files.pythonhosted.org/packages/e0/9d/7c5e24ee585c1f8b6356e1d11d40ab807ffde44d2db3b7dfd6d20b09720e/pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e", size = 2021883, upload-time = "2025-10-14T10:20:25.48Z" },
{ url = "https://files.pythonhosted.org/packages/33/90/5c172357460fc28b2871eb4a0fb3843b136b429c6fa827e4b588877bf115/pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db", size = 1968026, upload-time = "2025-10-14T10:20:27.039Z" },
{ url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887", size = 2099043, upload-time = "2025-10-14T10:20:28.561Z" },
{ url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2", size = 1910699, upload-time = "2025-10-14T10:20:30.217Z" },
{ url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999", size = 1952121, upload-time = "2025-10-14T10:20:32.246Z" },
{ url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4", size = 2041590, upload-time = "2025-10-14T10:20:34.332Z" },
{ url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f", size = 2219869, upload-time = "2025-10-14T10:20:35.965Z" },
{ url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b", size = 2345169, upload-time = "2025-10-14T10:20:37.627Z" },
{ url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47", size = 2070165, upload-time = "2025-10-14T10:20:39.246Z" },
{ url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970", size = 2189067, upload-time = "2025-10-14T10:20:41.015Z" },
{ url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed", size = 2132997, upload-time = "2025-10-14T10:20:43.106Z" },
{ url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8", size = 2307187, upload-time = "2025-10-14T10:20:44.849Z" },
{ url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431", size = 2305204, upload-time = "2025-10-14T10:20:46.781Z" },
{ url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd", size = 1972536, upload-time = "2025-10-14T10:20:48.39Z" },
{ url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff", size = 2031132, upload-time = "2025-10-14T10:20:50.421Z" },
{ url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8", size = 1969483, upload-time = "2025-10-14T10:20:52.35Z" },
{ url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" },
{ url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" },
{ url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" },
{ url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" },
{ url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" },
{ url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" },
{ url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" },
{ url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" },
{ url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" },
{ url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" },
{ url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" },
{ url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" },
{ url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" },
{ url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" },
{ url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" },
{ url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" },
{ url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" },
{ url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" },
{ url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" },
{ url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" },
{ url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" },
{ url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" },
{ url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" },
{ url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" },
{ url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" },
{ url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" },
{ url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" },
{ url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" },
{ url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" },
{ url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" },
{ url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" },
{ url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" },
{ url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" },
{ url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" },
{ url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" },
{ url = "https://files.pythonhosted.org/packages/b0/12/5ba58daa7f453454464f92b3ca7b9d7c657d8641c48e370c3ebc9a82dd78/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a1b2cfec3879afb742a7b0bcfa53e4f22ba96571c9e54d6a3afe1052d17d843b", size = 2122139, upload-time = "2025-10-14T10:22:47.288Z" },
{ url = "https://files.pythonhosted.org/packages/21/fb/6860126a77725c3108baecd10fd3d75fec25191d6381b6eb2ac660228eac/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:d175600d975b7c244af6eb9c9041f10059f20b8bbffec9e33fdd5ee3f67cdc42", size = 1936674, upload-time = "2025-10-14T10:22:49.555Z" },
{ url = "https://files.pythonhosted.org/packages/de/be/57dcaa3ed595d81f8757e2b44a38240ac5d37628bce25fb20d02c7018776/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f184d657fa4947ae5ec9c47bd7e917730fa1cbb78195037e32dcbab50aca5ee", size = 1956398, upload-time = "2025-10-14T10:22:52.19Z" },
{ url = "https://files.pythonhosted.org/packages/2f/1d/679a344fadb9695f1a6a294d739fbd21d71fa023286daeea8c0ed49e7c2b/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed810568aeffed3edc78910af32af911c835cc39ebbfacd1f0ab5dd53028e5c", size = 2138674, upload-time = "2025-10-14T10:22:54.499Z" },
{ url = "https://files.pythonhosted.org/packages/c4/48/ae937e5a831b7c0dc646b2ef788c27cd003894882415300ed21927c21efa/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537", size = 2112087, upload-time = "2025-10-14T10:22:56.818Z" },
{ url = "https://files.pythonhosted.org/packages/5e/db/6db8073e3d32dae017da7e0d16a9ecb897d0a4d92e00634916e486097961/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94", size = 1920387, upload-time = "2025-10-14T10:22:59.342Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c1/dd3542d072fcc336030d66834872f0328727e3b8de289c662faa04aa270e/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c", size = 1951495, upload-time = "2025-10-14T10:23:02.089Z" },
{ url = "https://files.pythonhosted.org/packages/2b/c6/db8d13a1f8ab3f1eb08c88bd00fd62d44311e3456d1e85c0e59e0a0376e7/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335", size = 2139008, upload-time = "2025-10-14T10:23:04.539Z" },
{ url = "https://files.pythonhosted.org/packages/5d/d4/912e976a2dd0b49f31c98a060ca90b353f3b73ee3ea2fd0030412f6ac5ec/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00", size = 2106739, upload-time = "2025-10-14T10:23:06.934Z" },
{ url = "https://files.pythonhosted.org/packages/71/f0/66ec5a626c81eba326072d6ee2b127f8c139543f1bf609b4842978d37833/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9", size = 1932549, upload-time = "2025-10-14T10:23:09.24Z" },
{ url = "https://files.pythonhosted.org/packages/c4/af/625626278ca801ea0a658c2dcf290dc9f21bb383098e99e7c6a029fccfc0/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2", size = 2135093, upload-time = "2025-10-14T10:23:11.626Z" },
{ url = "https://files.pythonhosted.org/packages/20/f6/2fba049f54e0f4975fef66be654c597a1d005320fa141863699180c7697d/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258", size = 2187971, upload-time = "2025-10-14T10:23:14.437Z" },
{ url = "https://files.pythonhosted.org/packages/0e/80/65ab839a2dfcd3b949202f9d920c34f9de5a537c3646662bdf2f7d999680/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347", size = 2147939, upload-time = "2025-10-14T10:23:16.831Z" },
{ url = "https://files.pythonhosted.org/packages/44/58/627565d3d182ce6dfda18b8e1c841eede3629d59c9d7cbc1e12a03aeb328/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa", size = 2311400, upload-time = "2025-10-14T10:23:19.234Z" },
{ url = "https://files.pythonhosted.org/packages/24/06/8a84711162ad5a5f19a88cead37cca81b4b1f294f46260ef7334ae4f24d3/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a", size = 2316840, upload-time = "2025-10-14T10:23:21.738Z" },
{ url = "https://files.pythonhosted.org/packages/aa/8b/b7bb512a4682a2f7fbfae152a755d37351743900226d29bd953aaf870eaa/pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3619320641fd212aaf5997b6ca505e97540b7e16418f4a241f44cdf108ffb50d", size = 2149135, upload-time = "2025-10-14T10:23:24.379Z" },
{ url = "https://files.pythonhosted.org/packages/7e/7d/138e902ed6399b866f7cfe4435d22445e16fff888a1c00560d9dc79a780f/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5", size = 2104721, upload-time = "2025-10-14T10:23:26.906Z" },
{ url = "https://files.pythonhosted.org/packages/47/13/0525623cf94627f7b53b4c2034c81edc8491cbfc7c28d5447fa318791479/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2", size = 1931608, upload-time = "2025-10-14T10:23:29.306Z" },
{ url = "https://files.pythonhosted.org/packages/d6/f9/744bc98137d6ef0a233f808bfc9b18cf94624bf30836a18d3b05d08bf418/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd", size = 2132986, upload-time = "2025-10-14T10:23:32.057Z" },
{ url = "https://files.pythonhosted.org/packages/17/c8/629e88920171173f6049386cc71f893dff03209a9ef32b4d2f7e7c264bcf/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c", size = 2187516, upload-time = "2025-10-14T10:23:34.871Z" },
{ url = "https://files.pythonhosted.org/packages/2e/0f/4f2734688d98488782218ca61bcc118329bf5de05bb7fe3adc7dd79b0b86/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405", size = 2146146, upload-time = "2025-10-14T10:23:37.342Z" },
{ url = "https://files.pythonhosted.org/packages/ed/f2/ab385dbd94a052c62224b99cf99002eee99dbec40e10006c78575aead256/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8", size = 2311296, upload-time = "2025-10-14T10:23:40.145Z" },
{ url = "https://files.pythonhosted.org/packages/fc/8e/e4f12afe1beeb9823bba5375f8f258df0cc61b056b0195fb1cf9f62a1a58/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308", size = 2315386, upload-time = "2025-10-14T10:23:42.624Z" },
{ url = "https://files.pythonhosted.org/packages/48/f7/925f65d930802e3ea2eb4d5afa4cb8730c8dc0d2cb89a59dc4ed2fcb2d74/pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f", size = 2147775, upload-time = "2025-10-14T10:23:45.406Z" },
]
[[package]]
@@ -911,14 +995,14 @@ wheels = [
[[package]]
name = "typing-inspection"
version = "0.4.1"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
[[package]]
+3 -1
View File
@@ -38,8 +38,10 @@ Each checkpointer should conform to `langgraph.checkpoint.base.BaseCheckpointSav
- `.put_writes` - Store intermediate writes linked to a checkpoint (i.e. pending writes).
- `.get_tuple` - Fetch a checkpoint tuple using for a given configuration (`thread_id` and `checkpoint_id`).
- `.list` - List checkpoints that match a given configuration and filter criteria.
- `.delete_thread()` - Delete all checkpoints and writes associated with a thread.
- `.get_next_version()` - Generate the next version ID for a channel.
If the checkpointer will be used with asynchronous graph execution (i.e. executing the graph via `.ainvoke`, `.astream`, `.abatch`), checkpointer must implement asynchronous versions of the above methods (`.aput`, `.aput_writes`, `.aget_tuple`, `.alist`).
If the checkpointer will be used with asynchronous graph execution (i.e. executing the graph via `.ainvoke`, `.astream`, `.abatch`), checkpointer must implement asynchronous versions of the above methods (`.aput`, `.aput_writes`, `.aget_tuple`, `.alist`). Similarly, the checkpointer must implement `.adelete_thread()` if asynchronous thread cleanup is desired. The base class provides a default implementation of `.get_next_version()` that generates an integer sequence starting from 1, but this method should be overridden for custom versioning schemes.
## Usage
@@ -60,23 +60,28 @@ class Checkpoint(TypedDict):
"""State snapshot at a given point in time."""
v: int
"""The version of the checkpoint format. Currently 1."""
"""The version of the checkpoint format. Currently `1`."""
id: str
"""The ID of the checkpoint. This is both unique and monotonically
increasing, so can be used for sorting checkpoints from first to last."""
"""The ID of the checkpoint.
This is both unique and monotonically increasing, so can be used for sorting
checkpoints from first to last."""
ts: str
"""The timestamp of the checkpoint in ISO 8601 format."""
channel_values: dict[str, Any]
"""The values of the channels at the time of the checkpoint.
Mapping from channel name to deserialized channel snapshot value.
"""
channel_versions: ChannelVersions
"""The versions of the channels at the time of the checkpoint.
The keys are channel names and the values are monotonically increasing
version strings for each channel.
"""
versions_seen: dict[str, ChannelVersions]
"""Map from node ID to map from channel name to version seen.
This keeps track of the versions of the channels that each node has seen.
Used to determine which nodes to execute next.
"""
@@ -352,11 +357,12 @@ class BaseCheckpointSaver(Generic[V]):
def get_next_version(self, current: V | None, channel: None) -> V:
"""Generate the next version ID for a channel.
Default is to use integer versions, incrementing by 1. If you override, you can use str/int/float versions,
as long as they are monotonically increasing.
Default is to use integer versions, incrementing by `1`.
If you override, you can use `str`/`int`/`float` versions, as long as they are monotonically increasing.
Args:
current: The current version identifier (int, float, or str).
current: The current version identifier (`int`, `float`, or `str`).
channel: Deprecated argument, kept for backwards compatibility.
Returns:
@@ -33,7 +33,7 @@ class InMemorySaver(
):
"""An in-memory checkpoint saver.
This checkpoint saver stores checkpoints in memory using a defaultdict.
This checkpoint saver stores checkpoints in memory using a `defaultdict`.
Note:
Only use `InMemorySaver` for debugging or testing purposes.
@@ -42,24 +42,25 @@ class InMemorySaver(
If you are using LangSmith Deployment, no checkpointer needs to be specified. The correct managed checkpointer will be used automatically.
Args:
serde: The serializer to use for serializing and deserializing checkpoints. Defaults to None.
serde: The serializer to use for serializing and deserializing checkpoints.
Examples:
Example:
```python
import asyncio
import asyncio
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph
builder = StateGraph(int)
builder.add_node("add_one", lambda x: x + 1)
builder.set_entry_point("add_one")
builder.set_finish_point("add_one")
builder = StateGraph(int)
builder.add_node("add_one", lambda x: x + 1)
builder.set_entry_point("add_one")
builder.set_finish_point("add_one")
memory = InMemorySaver()
graph = builder.compile(checkpointer=memory)
coro = graph.ainvoke(1, {"configurable": {"thread_id": "thread-1"}})
asyncio.run(coro) # Output: 2
memory = InMemorySaver()
graph = builder.compile(checkpointer=memory)
coro = graph.ainvoke(1, {"configurable": {"thread_id": "thread-1"}})
asyncio.run(coro) # Output: 2
```
"""
# thread ID -> checkpoint NS -> checkpoint ID -> checkpoint mapping
@@ -96,7 +97,8 @@ class InMemorySaver(
self.stack.enter_context(self.blobs) # type: ignore[arg-type]
def __enter__(self) -> InMemorySaver:
return self.stack.__enter__()
self.stack.__enter__()
return self
def __exit__(
self,
@@ -107,7 +109,8 @@ class InMemorySaver(
return self.stack.__exit__(exc_type, exc_value, traceback)
async def __aenter__(self) -> InMemorySaver:
return self.stack.__enter__()
self.stack.__enter__()
return self
async def __aexit__(
self,
@@ -1,6 +1,6 @@
from __future__ import annotations
from typing import Any, Protocol
from typing import Any, Protocol, runtime_checkable
class UntypedSerializerProtocol(Protocol):
@@ -11,13 +11,12 @@ class UntypedSerializerProtocol(Protocol):
def loads(self, data: bytes) -> Any: ...
class SerializerProtocol(UntypedSerializerProtocol, Protocol):
@runtime_checkable
class SerializerProtocol(Protocol):
"""Protocol for serialization and deserialization of objects.
- `dumps`: Serialize an object to bytes.
- `dumps_typed`: Serialize an object to a tuple (type, bytes).
- `loads`: Deserialize an object from bytes.
- `loads_typed`: Deserialize an object from a tuple (type, bytes).
- `dumps_typed`: Serialize an object to a tuple `(type, bytes)`.
- `loads_typed`: Deserialize an object from a tuple `(type, bytes)`.
Valid implementations include the `pickle`, `json` and `orjson` modules.
"""
@@ -31,12 +30,6 @@ class SerializerCompat(SerializerProtocol):
def __init__(self, serde: UntypedSerializerProtocol) -> None:
self.serde = serde
def dumps(self, obj: Any) -> bytes:
return self.serde.dumps(obj)
def loads(self, data: bytes) -> Any:
return self.serde.loads(data)
def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
return type(obj).__name__, self.serde.dumps(obj)
@@ -49,7 +42,7 @@ def maybe_add_typed_methods(
) -> SerializerProtocol:
"""Wrap serde old serde implementations in a class with loads_typed and dumps_typed for backwards compatibility."""
if not hasattr(serde, "loads_typed") or not hasattr(serde, "dumps_typed"):
if not isinstance(serde, SerializerProtocol):
return SerializerCompat(serde)
return serde
@@ -57,12 +50,13 @@ def maybe_add_typed_methods(
class CipherProtocol(Protocol):
"""Protocol for encryption and decryption of data.
- `encrypt`: Encrypt plaintext.
- `decrypt`: Decrypt ciphertext.
"""
def encrypt(self, plaintext: bytes) -> tuple[str, bytes]:
"""Encrypt plaintext. Returns a tuple (cipher name, ciphertext)."""
"""Encrypt plaintext. Returns a tuple `(cipher name, ciphertext)`."""
...
def decrypt(self, ciphername: str, ciphertext: bytes) -> bytes:
@@ -14,14 +14,8 @@ class EncryptedSerializer(SerializerProtocol):
self.cipher = cipher
self.serde = serde
def dumps(self, obj: Any) -> bytes:
return self.serde.dumps(obj)
def loads(self, data: bytes) -> Any:
return self.serde.loads(data)
def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
"""Serialize an object to a tuple (type, bytes) and encrypt the bytes."""
"""Serialize an object to a tuple `(type, bytes)` and encrypt the bytes."""
# serialize data
typ, data = self.serde.dumps_typed(obj)
# encrypt data
@@ -45,7 +39,7 @@ class EncryptedSerializer(SerializerProtocol):
def from_pycryptodome_aes(
cls, serde: SerializerProtocol = JsonPlusSerializer(), **kwargs: Any
) -> "EncryptedSerializer":
"""Create an EncryptedSerializer using AES encryption."""
"""Create an `EncryptedSerializer` using AES encryption."""
try:
from Crypto.Cipher import AES # type: ignore
except ImportError:
@@ -4,6 +4,7 @@ import dataclasses
import decimal
import importlib
import json
import logging
import pathlib
import pickle
import re
@@ -21,13 +22,12 @@ from ipaddress import (
IPv6Interface,
IPv6Network,
)
from typing import Any, cast
from typing import Any, Literal
from uuid import UUID
from zoneinfo import ZoneInfo
import ormsgpack
from langchain_core.load.load import Reviver
from langchain_core.load.serializable import Serializable
from langgraph.checkpoint.serde.base import SerializerProtocol
from langgraph.checkpoint.serde.types import SendProtocol
@@ -35,18 +35,33 @@ from langgraph.store.base import Item
LC_REVIVER = Reviver()
EMPTY_BYTES = b""
logger = logging.getLogger(__name__)
class JsonPlusSerializer(SerializerProtocol):
"""Serializer that uses ormsgpack, with a fallback to extended JSON serializer."""
"""Serializer that uses ormsgpack, with optional fallbacks.
!!! warning
Security note: This serializer is intended for use within the `BaseCheckpointSaver`
class and called within the Pregel loop. It should not be used on untrusted
python objects. If an attacker can write directly to your checkpoint database,
they may be able to trigger code execution when data is deserialized.
"""
def __init__(
self,
*,
pickle_fallback: bool = False,
allowed_json_modules: Sequence[tuple[str, ...]] | Literal[True] | None = None,
__unpack_ext_hook__: Callable[[int, bytes], Any] | None = None,
) -> None:
self.pickle_fallback = pickle_fallback
self._allowed_modules = (
{mod_and_name for mod_and_name in allowed_json_modules}
if allowed_json_modules and allowed_json_modules is not True
else (allowed_json_modules if allowed_json_modules is True else None)
)
self._unpack_ext_hook = (
__unpack_ext_hook__
if __unpack_ext_hook__ is not None
@@ -74,134 +89,90 @@ class JsonPlusSerializer(SerializerProtocol):
out["kwargs"] = kwargs
return out
def _default(self, obj: Any) -> str | dict[str, Any]:
if isinstance(obj, Serializable):
return cast(dict[str, Any], obj.to_json())
elif hasattr(obj, "model_dump") and callable(obj.model_dump):
return self._encode_constructor_args(
obj.__class__, method=(None, "model_construct"), kwargs=obj.model_dump()
)
elif hasattr(obj, "dict") and callable(obj.dict):
return self._encode_constructor_args(
obj.__class__, method=(None, "construct"), kwargs=obj.dict()
)
elif hasattr(obj, "_asdict") and callable(obj._asdict):
return self._encode_constructor_args(obj.__class__, kwargs=obj._asdict())
elif isinstance(obj, pathlib.Path):
return self._encode_constructor_args(pathlib.Path, args=obj.parts)
elif isinstance(obj, re.Pattern):
return self._encode_constructor_args(
re.compile, args=(obj.pattern, obj.flags)
)
elif isinstance(obj, UUID):
return self._encode_constructor_args(UUID, args=(obj.hex,))
elif isinstance(obj, decimal.Decimal):
return self._encode_constructor_args(decimal.Decimal, args=(str(obj),))
elif isinstance(obj, (set, frozenset, deque)):
return self._encode_constructor_args(type(obj), args=(tuple(obj),))
elif isinstance(obj, (IPv4Address, IPv4Interface, IPv4Network)):
return self._encode_constructor_args(obj.__class__, args=(str(obj),))
elif isinstance(obj, (IPv6Address, IPv6Interface, IPv6Network)):
return self._encode_constructor_args(obj.__class__, args=(str(obj),))
elif isinstance(obj, datetime):
return self._encode_constructor_args(
datetime, method="fromisoformat", args=(obj.isoformat(),)
)
elif isinstance(obj, timezone):
return self._encode_constructor_args(
timezone,
args=obj.__getinitargs__(), # type: ignore[attr-defined]
)
elif isinstance(obj, ZoneInfo):
return self._encode_constructor_args(ZoneInfo, args=(obj.key,))
elif isinstance(obj, timedelta):
return self._encode_constructor_args(
timedelta, args=(obj.days, obj.seconds, obj.microseconds)
)
elif isinstance(obj, date):
return self._encode_constructor_args(
date, args=(obj.year, obj.month, obj.day)
)
elif isinstance(obj, time):
return self._encode_constructor_args(
time,
args=(obj.hour, obj.minute, obj.second, obj.microsecond, obj.tzinfo),
kwargs={"fold": obj.fold},
)
elif dataclasses.is_dataclass(obj):
return self._encode_constructor_args(
obj.__class__,
kwargs={
field.name: getattr(obj, field.name)
for field in dataclasses.fields(obj)
},
)
elif isinstance(obj, Enum):
return self._encode_constructor_args(obj.__class__, args=(obj.value,))
elif isinstance(obj, SendProtocol):
return self._encode_constructor_args(
obj.__class__, kwargs={"node": obj.node, "arg": obj.arg}
)
elif isinstance(obj, (bytes, bytearray)):
return self._encode_constructor_args(
obj.__class__, method="fromhex", args=(obj.hex(),)
)
elif isinstance(obj, BaseException):
return repr(obj)
else:
raise TypeError(
f"Object of type {obj.__class__.__name__} is not JSON serializable"
)
def _reviver(self, value: dict[str, Any]) -> Any:
if (
if self._allowed_modules and (
value.get("lc", None) == 2
and value.get("type", None) == "constructor"
and value.get("id", None) is not None
):
try:
# Get module and class name
[*module, name] = value["id"]
# Import module
mod = importlib.import_module(".".join(module))
# Import class
cls = getattr(mod, name)
# Instantiate class
method = value.get("method")
if isinstance(method, str):
methods = [getattr(cls, method)]
elif isinstance(method, list):
methods = [
cls if method is None else getattr(cls, method)
for method in method
]
else:
methods = [cls]
args = value.get("args")
kwargs = value.get("kwargs")
for method in methods:
try:
if isclass(method) and issubclass(method, BaseException):
return None
if args and kwargs:
return method(*args, **kwargs)
elif args:
return method(*args)
elif kwargs:
return method(**kwargs)
else:
return method()
except Exception:
continue
except Exception:
return None
return self._revive_lc2(value)
except InvalidModuleError as e:
logger.warning(
"Object %s is not in the deserialization allowlist.\n%s",
value["id"],
e.message,
)
return LC_REVIVER(value)
def dumps(self, obj: Any) -> bytes:
return json.dumps(obj, default=self._default, ensure_ascii=False).encode(
"utf-8", "ignore"
def _revive_lc2(self, value: dict[str, Any]) -> Any:
self._check_allowed_modules(value)
[*module, name] = value["id"]
try:
mod = importlib.import_module(".".join(module))
cls = getattr(mod, name)
method = value.get("method")
if isinstance(method, str):
methods = [getattr(cls, method)]
elif isinstance(method, list):
methods = [cls if m is None else getattr(cls, m) for m in method]
else:
methods = [cls]
args = value.get("args")
kwargs = value.get("kwargs")
for method in methods:
try:
if isclass(method) and issubclass(method, BaseException):
return None
if args and kwargs:
return method(*args, **kwargs)
elif args:
return method(*args)
elif kwargs:
return method(**kwargs)
else:
return method()
except Exception:
continue
except Exception:
return None
def _check_allowed_modules(self, value: dict[str, Any]) -> None:
needed = tuple(value["id"])
method = value.get("method")
if isinstance(method, list):
method_display = ",".join(m or "<init>" for m in method)
elif isinstance(method, str):
method_display = method
else:
method_display = "<init>"
dotted = ".".join(needed)
if not self._allowed_modules:
raise InvalidModuleError(
f"Refused to deserialize JSON constructor: {dotted} (method: {method_display}). "
"No allowed_json_modules configured.\n\n"
"Unblock with ONE of:\n"
f" • JsonPlusSerializer(allowed_json_modules=[{needed!r}, ...])\n"
" • (DANGEROUS) JsonPlusSerializer(allowed_json_modules=True)\n\n"
"Note: Prefix allowlists are intentionally unsupported; prefer exact symbols "
"or plain-JSON representations revived without import-time side effects."
)
if self._allowed_modules is True:
return
if needed in self._allowed_modules:
return
raise InvalidModuleError(
f"Refused to deserialize JSON constructor: {dotted} (method: {method_display}). "
"Symbol is not in the deserialization allowlist.\n\n"
"Add exactly this symbol to unblock:\n"
f" JsonPlusSerializer(allowed_json_modules=[{needed!r}, ...])\n"
"Or, as a last resort (DANGEROUS):\n"
" JsonPlusSerializer(allowed_json_modules=True)"
)
def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
@@ -215,15 +186,10 @@ class JsonPlusSerializer(SerializerProtocol):
try:
return "msgpack", _msgpack_enc(obj)
except ormsgpack.MsgpackEncodeError as exc:
if "valid UTF-8" in str(exc):
return "json", self.dumps(obj)
elif self.pickle_fallback:
if self.pickle_fallback:
return "pickle", pickle.dumps(obj)
raise exc
def loads(self, data: bytes) -> Any:
return json.loads(data, object_hook=self._reviver)
def loads_typed(self, data: tuple[str, bytes]) -> Any:
type_, data_ = data
if type_ == "null":
@@ -233,7 +199,7 @@ class JsonPlusSerializer(SerializerProtocol):
elif type_ == "bytearray":
return bytearray(data_)
elif type_ == "json":
return self.loads(data_)
return json.loads(data_, object_hook=self._reviver)
elif type_ == "msgpack":
return ormsgpack.unpackb(
data_, ext_hook=self._unpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
@@ -663,12 +629,20 @@ def _msgpack_ext_hook_to_json(code: int, data: bytes) -> Any:
return
class InvalidModuleError(Exception):
"""Exception raised when a module is not in the allowlist."""
def __init__(self, message: str):
self.message = message
_option = (
ormsgpack.OPT_NON_STR_KEYS
| ormsgpack.OPT_PASSTHROUGH_DATACLASS
| ormsgpack.OPT_PASSTHROUGH_DATETIME
| ormsgpack.OPT_PASSTHROUGH_ENUM
| ormsgpack.OPT_PASSTHROUGH_UUID
| ormsgpack.OPT_REPLACE_SURROGATES
)
+104 -54
View File
@@ -4,9 +4,9 @@ Stores provide long-term memory that persists across threads and conversations.
Supports hierarchical namespaces, key-value storage, and optional vector search.
Core types:
- BaseStore: Store interface with sync/async operations
- Item: Stored key-value pairs with metadata
- Op: Get/Put/Search/List operations
- `BaseStore`: Store interface with sync/async operations
- `Item`: Stored key-value pairs with metadata
- `Op`: Get/Put/Search/List operations
"""
from __future__ import annotations
@@ -163,6 +163,7 @@ class GetOp(NamedTuple):
???+ example "Examples"
Basic item retrieval:
```python
GetOp(namespace=("users", "profiles"), key="user123")
GetOp(namespace=("cache", "embeddings"), key="doc456")
@@ -206,11 +207,14 @@ class SearchOp(NamedTuple):
within a given namespace prefix. It provides pagination through limit and offset
parameters.
Note:
!!! note
Natural language search support depends on your store implementation.
???+ example "Examples"
Search with filters and pagination:
```python
SearchOp(
namespace_prefix=("documents",),
@@ -221,6 +225,7 @@ class SearchOp(NamedTuple):
```
Natural language search:
```python
SearchOp(
namespace_prefix=("users", "content"),
@@ -256,6 +261,7 @@ class SearchOp(NamedTuple):
- `$lte`: Less than or equal to
???+ example "Examples"
Simple exact match:
```python
@@ -288,6 +294,7 @@ class SearchOp(NamedTuple):
"""Natural language search query for semantic search capabilities.
???+ example "Examples"
- "technical documentation about REST APIs"
- "machine learning papers from 2023"
"""
@@ -305,6 +312,7 @@ NamespacePath = tuple[str | Literal["*"], ...]
"""A tuple representing a namespace path that can include wildcards.
???+ example "Examples"
```python
("users",) # Exact users namespace
("documents", "*") # Any sub-namespace under documents
@@ -330,17 +338,21 @@ class MatchCondition(NamedTuple):
hierarchies.
???+ example "Examples"
Prefix matching:
```python
MatchCondition(match_type="prefix", path=("users", "profiles"))
```
Suffix matching with wildcard:
```python
MatchCondition(match_type="suffix", path=("cache", "*"))
```
Simple suffix matching:
```python
MatchCondition(match_type="suffix", path=("v1",))
```
@@ -361,7 +373,8 @@ class ListNamespacesOp(NamedTuple):
???+ example "Examples"
List all namespaces under the "documents" path:
List all namespaces under the `"documents"` path:
```python
ListNamespacesOp(
match_conditions=(MatchCondition(match_type="prefix", path=("documents",)),),
@@ -369,7 +382,8 @@ class ListNamespacesOp(NamedTuple):
)
```
List all namespaces that end with "v1":
List all namespaces that end with `"v1"`:
```python
ListNamespacesOp(
match_conditions=(MatchCondition(match_type="suffix", path=("v1",)),),
@@ -383,12 +397,15 @@ class ListNamespacesOp(NamedTuple):
"""Optional conditions for filtering namespaces.
???+ example "Examples"
All user namespaces:
```python
(MatchCondition(match_type="prefix", path=("users",)),)
```
All namespaces that start with "docs" and end with "draft":
All namespaces that start with `"docs"` and end with `"draft"`:
```python
(
MatchCondition(match_type="prefix", path=("docs",)),
@@ -425,17 +442,21 @@ class PutOp(NamedTuple):
Each element in the tuple represents one level in the hierarchy.
???+ example "Examples"
Root level documents
Root level documents:
```python
("documents",)
```
User-specific documents
User-specific documents:
```python
("documents", "user123")
```
Nested cache structure
Nested cache structure:
```python
("cache", "embeddings", "v1")
```
@@ -448,15 +469,15 @@ class PutOp(NamedTuple):
Together with the namespace, it forms a complete path to the item.
Example:
If namespace is ("documents", "user123") and key is "report1",
the full path would effectively be "documents/user123/report1"
If namespace is `("documents", "user123")` and key is `"report1"`,
the full path would effectively be `"documents/user123/report1"`
"""
value: dict[str, Any] | None
"""The data to store, or None to mark the item for deletion.
"""The data to store, or `None` to mark the item for deletion.
The value must be a dictionary with string keys and JSON-serializable values.
Setting this to None signals that the item should be deleted.
Setting this to `None` signals that the item should be deleted.
Example:
{
@@ -470,9 +491,9 @@ class PutOp(NamedTuple):
"""Controls how the item's fields are indexed for search operations.
Indexing configuration determines how the item can be found through search:
- None (default): Uses the store's default indexing configuration (if provided)
- False: Disables indexing for this item
- list[str]: Specifies which json path fields to index for search
- `None` (default): Uses the store's default indexing configuration (if provided)
- `False`: Disables indexing for this item
- `list[str]`: Specifies which json path fields to index for search
The item remains accessible through direct get() operations regardless of indexing.
When indexed, fields can be searched using natural language queries through
@@ -487,8 +508,9 @@ class PutOp(NamedTuple):
- All elements (each individually): `"array[*]"`
???+ example "Examples"
- None - Use store defaults (whole item)
- list[str] - List of fields to index
- `None` - Use store defaults (whole item)
- `list[str]` - List of fields to index
```python
[
@@ -569,13 +591,15 @@ class IndexConfig(TypedDict, total=False):
"""Optional function to generate embeddings from text.
Can be specified in three ways:
1. A LangChain Embeddings instance
1. A LangChain `Embeddings` instance
2. A synchronous embedding function (`EmbeddingsFunc`)
3. An asynchronous embedding function (`AEmbeddingsFunc`)
4. A provider string (e.g., `"openai:text-embedding-3-small"`)
???+ example "Examples"
Using LangChain's initialization with `InMemoryStore`:
```python
from langchain.embeddings import init_embeddings
from langgraph.store.memory import InMemoryStore
@@ -589,6 +613,7 @@ class IndexConfig(TypedDict, total=False):
```
Using a custom embedding function with `InMemoryStore`:
```python
from openai import OpenAI
from langgraph.store.memory import InMemoryStore
@@ -611,6 +636,7 @@ class IndexConfig(TypedDict, total=False):
```
Using an asynchronous embedding function with `InMemoryStore`:
```python
from openai import AsyncOpenAI
from langgraph.store.memory import InMemoryStore
@@ -648,6 +674,7 @@ class IndexConfig(TypedDict, total=False):
`index` parameter in the `put` or `aput` operations.
???+ example "Examples"
```python
# Embed entire document (default)
fields=["$"]
@@ -731,7 +758,7 @@ 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`, uses the store's default refresh_ttl setting.
If `None`, uses the store's default `refresh_ttl` setting.
If no TTL is specified, this argument is ignored.
Returns:
@@ -767,7 +794,9 @@ class BaseStore(ABC):
List of items matching the search criteria.
???+ example "Examples"
Basic filtering:
```python
# Search for documents with specific metadata
results = store.search(
@@ -777,6 +806,7 @@ class BaseStore(ABC):
```
Natural language search (requires vector store implementation):
```python
# Initialize store with embedding configuration
store = YourStore( # e.g., InMemoryStore, AsyncPostgresStore
@@ -788,6 +818,7 @@ class BaseStore(ABC):
)
# Search for semantically similar documents
results = store.search(
("docs",),
query="machine learning applications in healthcare",
@@ -796,8 +827,10 @@ class BaseStore(ABC):
)
```
Note: Natural language search support depends on your store implementation
and requires proper embedding configuration.
!!! note
Natural language search support depends on your store implementation
and requires proper embedding configuration.
"""
return self.batch(
[
@@ -825,7 +858,7 @@ class BaseStore(ABC):
Args:
namespace: Hierarchical path for the item, represented as a tuple of strings.
Example: ("documents", "user123")
Example: `("documents", "user123")`
key: Unique identifier within the namespace. Together with namespace forms
the complete path to the item.
value: Dictionary containing the item's data. Must contain string keys
@@ -836,10 +869,10 @@ class BaseStore(ABC):
If you do not initialize the store with indexing capabilities,
the `index` parameter will be ignored
- False: Disable indexing for this item
- list[str]: List of field paths to index, supporting:
- Nested fields: "metadata.title"
- Array access: "chapters[*].content" (each indexed separately)
- Specific indices: "authors[0].name"
- `list[str]`: List of field paths to index, supporting:
- Nested fields: `"metadata.title"`
- Array access: `"chapters[*].content"` (each indexed separately)
- Specific indices: `"authors[0].name"`
ttl: Time to live in minutes. Support for this argument depends on your store adapter.
If specified, the item will expire after this many minutes from when it was last accessed.
None means no expiration. Expired runs will be deleted opportunistically.
@@ -855,18 +888,22 @@ class BaseStore(ABC):
Some implementations may not support expiration of items.
???+ example "Examples"
Store item. Indexing depends on how you configure the store.
Store item. Indexing depends on how you configure the store:
```python
store.put(("docs",), "report", {"memory": "Will likes ai"})
```
Do not index item for semantic search. Still accessible through get()
and search() operations but won't have a vector representation.
Do not index item for semantic search. Still accessible through `get()`
and `search()` operations but won't have a vector representation.
```python
store.put(("docs",), "report", {"memory": "Will likes ai"}, index=False)
```
Index specific fields for search.
Index specific fields for search:
```python
store.put(("docs",), "report", {"memory": "Will likes ai"}, index=["memory"])
```
@@ -917,15 +954,17 @@ class BaseStore(ABC):
suffix: Filter namespaces that end with this path.
max_depth: Return namespaces up to this depth in the hierarchy.
Namespaces deeper than this level will be truncated.
limit: Maximum number of namespaces to return (default 100).
offset: Number of namespaces to skip for pagination (default 0).
limit: Maximum number of namespaces to return.
offset: Number of namespaces to skip for pagination.
Returns:
List[Tuple[str, ...]]: A list of namespace tuples that match the criteria.
Each tuple represents a full namespace path up to `max_depth`.
A list of namespace tuples that match the criteria. Each tuple represents a
full namespace path up to `max_depth`.
???+ example "Examples":
Setting max_depth=3. Given the namespaces:
Setting `max_depth=3`. Given the namespaces:
```python
# Example if you have the following namespaces:
# ("a", "b", "c")
@@ -999,14 +1038,16 @@ 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.
If `None`, uses the store's TTLConfig.refresh_default setting.
If TTLConfig is not provided or no TTL is specified, this argument is ignored.
If `None`, 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.
???+ example "Examples"
Basic filtering:
```python
# Search for documents with specific metadata
results = await store.asearch(
@@ -1016,6 +1057,7 @@ class BaseStore(ABC):
```
Natural language search (requires vector store implementation):
```python
# Initialize store with embedding configuration
store = YourStore( # e.g., InMemoryStore, AsyncPostgresStore
@@ -1027,6 +1069,7 @@ class BaseStore(ABC):
)
# Search for semantically similar documents
results = await store.asearch(
("docs",),
query="machine learning applications in healthcare",
@@ -1035,8 +1078,10 @@ class BaseStore(ABC):
)
```
Note: Natural language search support depends on your store implementation
and requires proper embedding configuration.
!!! note
Natural language search support depends on your store implementation
and requires proper embedding configuration.
"""
return (
await self.abatch(
@@ -1066,7 +1111,7 @@ class BaseStore(ABC):
Args:
namespace: Hierarchical path for the item, represented as a tuple of strings.
Example: ("documents", "user123")
Example: `("documents", "user123")`
key: Unique identifier within the namespace. Together with namespace forms
the complete path to the item.
value: Dictionary containing the item's data. Must contain string keys
@@ -1077,10 +1122,10 @@ class BaseStore(ABC):
If you do not initialize the store with indexing capabilities,
the `index` parameter will be ignored
- False: Disable indexing for this item
- list[str]: List of field paths to index, supporting:
- Nested fields: "metadata.title"
- Array access: "chapters[*].content" (each indexed separately)
- Specific indices: "authors[0].name"
- `list[str]`: List of field paths to index, supporting:
- Nested fields: `"metadata.title"`
- Array access: `"chapters[*].content"` (each indexed separately)
- Specific indices: `"authors[0].name"`
ttl: Time to live in minutes. Support for this argument depends on your store adapter.
If specified, the item will expire after this many minutes from when it was last accessed.
None means no expiration. Expired runs will be deleted opportunistically.
@@ -1096,18 +1141,22 @@ class BaseStore(ABC):
Some implementations may not support expiration of items.
???+ example "Examples"
Store item. Indexing depends on how you configure the store.
Store item. Indexing depends on how you configure the store:
```python
await store.aput(("docs",), "report", {"memory": "Will likes ai"})
```
Do not index item for semantic search. Still accessible through get()
and search() operations but won't have a vector representation.
Do not index item for semantic search. Still accessible through `get()`
and `search()` operations but won't have a vector representation.
```python
await store.aput(("docs",), "report", {"memory": "Will likes ai"}, index=False)
```
Index specific fields for search (if store configured to index items):
```python
await store.aput(
("docs",),
@@ -1166,15 +1215,16 @@ class BaseStore(ABC):
suffix: Filter namespaces that end with this path.
max_depth: Return namespaces up to this depth in the hierarchy.
Namespaces deeper than this level will be truncated to this depth.
limit: Maximum number of namespaces to return (default 100).
offset: Number of namespaces to skip for pagination (default 0).
limit: Maximum number of namespaces to return.
offset: Number of namespaces to skip for pagination.
Returns:
List[Tuple[str, ...]]: A list of namespace tuples that match the criteria.
Each tuple represents a full namespace path up to `max_depth`.
A list of namespace tuples that match the criteria. Each tuple represents a
full namespace path up to `max_depth`.
???+ example "Examples"
Setting max_depth=3 with existing namespaces:
Setting `max_depth=3` with existing namespaces:
```python
# Given the following namespaces:
# ("a", "b", "c")
@@ -49,7 +49,9 @@ def ensure_embeddings(
An Embeddings instance that wraps the provided function(s).
??? example "Examples"
Wrap a synchronous embedding function:
```python
def my_embed_fn(texts):
return [[0.1, 0.2] for _ in texts]
@@ -59,6 +61,7 @@ def ensure_embeddings(
```
Wrap an asynchronous embedding function:
```python
async def my_async_fn(texts):
return [[0.1, 0.2] for _ in texts]
@@ -68,6 +71,7 @@ def ensure_embeddings(
```
Initialize embeddings using a provider string:
```python
# Requires langchain>=0.3.9 and langgraph-checkpoint>=2.0.11
embeddings = ensure_embeddings("openai:text-embedding-3-small")
@@ -119,7 +123,9 @@ class EmbeddingsLambda(Embeddings):
will raise an error. If sync, it will be used for both sync and async operations.
??? example "Examples"
With a sync function:
```python
def my_embed_fn(texts):
# Return 2D embeddings for each text
@@ -131,6 +137,7 @@ class EmbeddingsLambda(Embeddings):
```
With an async function:
```python
async def my_async_fn(texts):
return [[0.1, 0.2] for _ in texts]
+16 -7
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-checkpoint"
version = "2.1.2"
version = "3.0.1"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
requires-python = ">=3.10"
@@ -13,27 +13,36 @@ license = "MIT"
license-files = ['LICENSE']
dependencies = [
"langchain-core>=0.2.38",
"ormsgpack>=1.10.0",
"ormsgpack>=1.12.0",
]
[project.urls]
Repository = "https://www.github.com/langchain-ai/langgraph"
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint"
Twitter = "https://x.com/LangChainAI"
Slack = "https://www.langchain.com/join-community"
Reddit = "https://www.reddit.com/r/LangChain/"
[dependency-groups]
dev = [
"ruff",
"codespell",
test = [
"pytest",
"pytest-asyncio",
"pytest-mock",
"pytest-watcher",
"mypy",
"dataclasses-json",
"numpy",
"pandas",
"pandas-stubs>=2.2.2.240807",
"redis",
]
lint = [
"ruff",
"codespell",
"mypy",
]
dev = [
{include-group = "test"},
{include-group = "lint"},
]
[tool.hatch.build.targets.wheel]
include = ["langgraph"]
+82 -30
View File
@@ -1,4 +1,5 @@
import dataclasses
import json
import pathlib
import re
import sys
@@ -19,6 +20,7 @@ from pydantic.v1 import BaseModel as BaseModelV1
from pydantic.v1 import SecretStr as SecretStrV1
from langgraph.checkpoint.serde.jsonplus import (
InvalidModuleError,
JsonPlusSerializer,
_msgpack_ext_hook_to_json,
)
@@ -108,11 +110,7 @@ def test_serde_jsonplus() -> None:
"my_dataclass": MyDataclass("foo", 1, InnerDataclass("hello")),
"my_enum": MyEnum.FOO,
"my_pydantic": MyPydantic(foo="foo", bar=1, inner=InnerPydantic(hello="hello")),
"my_pydantic_v1": MyPydanticV1(
foo="foo", bar=1, inner=InnerPydanticV1(hello="hello")
),
"my_secret_str": SecretStr("meow"),
"my_secret_str_v1": SecretStrV1("meow"),
"person": Person(name="foo"),
"a_bool": True,
"a_none": None,
@@ -134,6 +132,12 @@ def test_serde_jsonplus() -> None:
),
}
if sys.version_info < (3, 14):
to_serialize["my_pydantic_v1"] = MyPydanticV1(
foo="foo", bar=1, inner=InnerPydanticV1(hello="hello")
)
to_serialize["my_secret_str_v1"] = SecretStrV1("meow")
serde = JsonPlusSerializer()
dumped = serde.dumps_typed(to_serialize)
@@ -145,23 +149,22 @@ def test_serde_jsonplus() -> None:
assert serde.loads_typed(serde.dumps_typed(value)) == value
surrogates = [
"Hello\ud83d\ude00",
"Python\ud83d\udc0d",
"Surrogate\ud834\udd1e",
"Example\ud83c\udf89",
"String\ud83c\udfa7",
"With\ud83c\udf08",
"Surrogates\ud83d\ude0e",
"Embedded\ud83d\udcbb",
"In\ud83c\udf0e",
"The\ud83d\udcd6",
"Text\ud83d\udcac",
"Hello??",
"Python??",
"Surrogate??",
"Example??",
"String??",
"With??",
"Surrogates??",
"Embedded??",
"In??",
"The??",
"Text??",
"收花🙄·到",
]
serde = JsonPlusSerializer(pickle_fallback=False)
assert serde.loads_typed(serde.dumps_typed(surrogates)) == [
v.encode("utf-8", "ignore").decode() for v in surrogates
]
assert serde.loads_typed(serde.dumps_typed(surrogates)) == surrogates
def test_serde_jsonplus_json_mode() -> None:
@@ -190,11 +193,7 @@ def test_serde_jsonplus_json_mode() -> None:
"my_dataclass": MyDataclass("foo", 1, InnerDataclass("hello")),
"my_enum": MyEnum.FOO,
"my_pydantic": MyPydantic(foo="foo", bar=1, inner=InnerPydantic(hello="hello")),
"my_pydantic_v1": MyPydanticV1(
foo="foo", bar=1, inner=InnerPydanticV1(hello="hello")
),
"my_secret_str": SecretStr("meow"),
"my_secret_str_v1": SecretStrV1("meow"),
"person": Person(name="foo"),
"a_bool": True,
"a_none": None,
@@ -216,13 +215,20 @@ def test_serde_jsonplus_json_mode() -> None:
),
}
if sys.version_info < (3, 14):
to_serialize["my_pydantic_v1"] = MyPydanticV1(
foo="foo", bar=1, inner=InnerPydanticV1(hello="hello")
)
to_serialize["my_secret_str_v1"] = SecretStrV1("meow")
serde = JsonPlusSerializer(__unpack_ext_hook__=_msgpack_ext_hook_to_json)
dumped = serde.dumps_typed(to_serialize)
assert dumped[0] == "msgpack"
result = serde.loads_typed(dumped)
assert result == {
expected_result = {
"path": ["foo", "bar"],
"re": ["foo", 48],
"decimal": "1.10101",
@@ -246,9 +252,7 @@ def test_serde_jsonplus_json_mode() -> None:
"my_dataclass": {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}},
"my_enum": "foo",
"my_pydantic": {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}},
"my_pydantic_v1": {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}},
"my_secret_str": "meow",
"my_secret_str_v1": "meow",
"person": {"name": "foo"},
"a_bool": True,
"a_none": None,
@@ -270,6 +274,16 @@ def test_serde_jsonplus_json_mode() -> None:
},
}
if sys.version_info < (3, 14):
expected_result["my_pydantic_v1"] = {
"foo": "foo",
"bar": 1,
"inner": {"hello": "hello"},
}
expected_result["my_secret_str_v1"] = "meow"
assert result == expected_result
def test_serde_jsonplus_bytes() -> None:
serde = JsonPlusSerializer()
@@ -281,6 +295,20 @@ def test_serde_jsonplus_bytes() -> None:
assert serde.loads_typed(dumped) == some_bytes
def test_deserde_invalid_module() -> None:
serde = JsonPlusSerializer()
load = {
"lc": 2,
"type": "constructor",
"id": ["pprint", "pprint"],
"kwargs": {"object": "HELLO"},
}
with pytest.raises(InvalidModuleError):
serde._revive_lc2(load)
serde = JsonPlusSerializer(allowed_json_modules=[("pprint", "pprint")])
serde.loads_typed(("json", json.dumps(load).encode("utf-8")))
def test_serde_jsonplus_bytearray() -> None:
serde = JsonPlusSerializer()
@@ -357,7 +385,12 @@ def test_serde_jsonplus_numpy_array_json_hook(arr: np.ndarray) -> None:
"str_col": ["a", None, "c"],
}
),
pd.DataFrame({"cat_col": pd.Categorical(["a", "b", "a", "c"])}),
pytest.param(
pd.DataFrame({"cat_col": pd.Categorical(["a", "b", "a", "c"])}),
marks=pytest.mark.skipif(
sys.version_info >= (3, 14), reason="NotImplementedError on Python 3.14"
),
),
pd.DataFrame(
{
"int8": pd.array([1, 2, 3], dtype="int8"),
@@ -385,11 +418,25 @@ def test_serde_jsonplus_numpy_array_json_hook(arr: np.ndarray) -> None:
"col3": np.random.rand(1000),
}
),
pd.DataFrame(
{"tz_datetime": pd.date_range("2024-01-01", periods=3, freq="D", tz="UTC")}
pytest.param(
pd.DataFrame(
{
"tz_datetime": pd.date_range(
"2024-01-01", periods=3, freq="D", tz="UTC"
)
}
),
marks=pytest.mark.skipif(
sys.version_info >= (3, 14), reason="NotImplementedError on Python 3.14"
),
),
pd.DataFrame({"timedelta": pd.to_timedelta([1, 2, 3], unit="D")}),
pd.DataFrame({"period": pd.period_range("2024-01", periods=3, freq="M")}),
pytest.param(
pd.DataFrame({"period": pd.period_range("2024-01", periods=3, freq="M")}),
marks=pytest.mark.skipif(
sys.version_info >= (3, 14), reason="NotImplementedError on Python 3.14"
),
),
pd.DataFrame({"interval": pd.interval_range(start=0, end=3, periods=3)}),
pd.DataFrame({"unicode": ["Hello 🌍", "Python 🐍", "Data 📊"]}),
pd.DataFrame({"mixed": [1, "string", [1, 2, 3], {"key": "value"}]}),
@@ -426,7 +473,12 @@ def test_serde_jsonplus_pandas_dataframe(df: pd.DataFrame) -> None:
pd.Series([1, 2, None]),
pd.Series([1.1, None, 3.3]),
pd.Series(["a", None, "c"]),
pd.Series(pd.Categorical(["a", "b", "a", "c"])),
pytest.param(
pd.Series(pd.Categorical(["a", "b", "a", "c"])),
marks=pytest.mark.skipif(
sys.version_info >= (3, 14), reason="NotImplementedError on Python 3.14"
),
),
pd.Series([1, 2, 3], dtype="int8"),
pd.Series([10, 20, 30], dtype="int16"),
pd.Series([100, 200, 300], dtype="int32"),
+9 -2
View File
@@ -188,7 +188,14 @@ class TestMemorySaver:
assert len(search_results_4) == 0
def test_memory_saver() -> None:
async def test_memory_saver() -> None:
from langgraph.checkpoint.memory import InMemorySaver
assert isinstance(InMemorySaver(), InMemorySaver)
memory_saver = InMemorySaver()
assert isinstance(memory_saver, InMemorySaver)
async with memory_saver as async_memory_saver:
assert async_memory_saver is memory_saver
with memory_saver as sync_memory_saver:
assert sync_memory_saver is memory_saver
+562 -435
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
.langgraph_api/
+4 -1
View File
@@ -1,4 +1,4 @@
.PHONY: test lint format test-integration update-schema
.PHONY: test lint format test-integration update-schema bump-version
######################
# TESTING AND COVERAGE
@@ -35,3 +35,6 @@ format format_diff:
update-schema:
uv run python generate_schema.py
bump-version:
uv run hatch version patch
@@ -1,13 +1,12 @@
from collections.abc import Sequence
from typing import Annotated, Literal, TypedDict
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.messages import BaseMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import END, StateGraph, add_messages
from langgraph.prebuilt import ToolNode
tools = [TavilySearchResults(max_results=1)]
tools = []
model_oai = ChatOpenAI(temperature=0)
@@ -5,5 +5,5 @@ description = "Test for prerelease stuff"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"langgraph==0.6.0"
"langgraph==1.0.2"
]
@@ -5,5 +5,5 @@ description = "Test for prerelease stuff"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"langchain-openai==0.3.0"
"langchain-openai==1.0.1"
]
@@ -6,9 +6,9 @@ readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"langchain-openai==1.0.0a2",
"langgraph==1.0.0a2",
"langchain_community>=0.3.0",
"langchain-anthropic==1.0.0a5",
"langgraph==1.0.2"
]
[tool.uv]
prerelease = "allow"
prerelease = "allow"
+9 -1
View File
@@ -13,8 +13,9 @@ from pathlib import Path
import msgspec
from langgraph_cli.config import (
from langgraph_cli.schemas import (
AuthConfig,
CacheConfig,
CheckpointerConfig,
Config,
ConfigurableHeaderConfig,
@@ -22,9 +23,12 @@ from langgraph_cli.config import (
HttpConfig,
IndexConfig,
SecurityConfig,
SerdeConfig,
StoreConfig,
ThreadTTLConfig,
TTLConfig,
WebhooksConfig,
WebhookUrlPolicy,
)
@@ -110,10 +114,14 @@ def add_descriptions_to_schema(schema, cls):
SecurityConfig,
HttpConfig,
CorsConfig,
CacheConfig,
ThreadTTLConfig,
CheckpointerConfig,
SerdeConfig,
TTLConfig,
ConfigurableHeaderConfig,
WebhooksConfig,
WebhookUrlPolicy,
]:
if potential_cls.__name__ == def_name:
add_descriptions_to_schema(def_schema, potential_cls)
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.4.4"
__version__ = "0.4.9"
+1
View File
@@ -760,6 +760,7 @@ def dev(
http=config_json.get("http"),
ui=config_json.get("ui"),
ui_config=config_json.get("ui_config"),
webhooks=config_json.get("webhooks"),
studio_url=studio_url,
allow_blocking=allow_blocking,
tunnel=tunnel,
+112 -522
View File
@@ -4,10 +4,12 @@ import pathlib
import re
import textwrap
from collections import Counter
from typing import Any, Literal, NamedTuple, TypedDict
from typing import Literal, NamedTuple
import click
from langgraph_cli.schemas import Config, Distros
MIN_NODE_VERSION = "20"
DEFAULT_NODE_VERSION = "20"
@@ -17,504 +19,6 @@ DEFAULT_PYTHON_VERSION = "3.11"
DEFAULT_IMAGE_DISTRO = "debian"
Distros = Literal["debian", "wolfi", "bullseye", "bookworm"]
MiddlewareOrders = Literal["auth_first", "middleware_first"]
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: float | None
"""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.
"""
sweep_interval_minutes: int | None
"""Optional. Interval in minutes between TTL sweep iterations.
If provided, the store will periodically delete expired items based on the TTL.
If omitted, no automatic sweeping will occur.
"""
class IndexConfig(TypedDict, total=False):
"""Configuration for indexing documents for semantic search in the store.
This governs how text is converted into embeddings and stored for vector-based lookups.
"""
dims: int
"""Required. Dimensionality of the embedding vectors you will store.
Must match the output dimension of your selected embedding model or custom embed function.
If mismatched, you will likely encounter shape/size errors when inserting or querying vectors.
Common embedding model output dimensions:
- openai:text-embedding-3-large: 3072
- openai:text-embedding-3-small: 1536
- openai:text-embedding-ada-002: 1536
- cohere:embed-english-v3.0: 1024
- cohere:embed-english-light-v3.0: 384
- cohere:embed-multilingual-v3.0: 1024
- cohere:embed-multilingual-light-v3.0: 384
"""
embed: str
"""Required. Identifier or reference to the embedding model or a custom embedding function.
The format can vary:
- "<provider>:<model_name>" for recognized providers (e.g., "openai:text-embedding-3-large")
- "path/to/module.py:function_name" for your own local embedding function
- "my_custom_embed" if it's a known alias in your system
Examples:
- "openai:text-embedding-3-large"
- "cohere:embed-multilingual-v3.0"
- "src/app.py:embeddings"
Note: Must return embeddings of dimension `dims`.
"""
fields: list[str] | None
"""Optional. List of JSON fields to extract before generating embeddings.
Defaults to ["$"], which means the entire JSON object is embedded as one piece of text.
If you provide multiple fields (e.g. ["title", "content"]), each is extracted and embedded separately,
often saving token usage if you only care about certain parts of the data.
Example:
fields=["title", "abstract", "author.biography"]
"""
class StoreConfig(TypedDict, total=False):
"""Configuration for the built-in long-term memory store.
This store can optionally perform semantic search. If you omit `index`,
the store will just handle traditional (non-embedded) data without vector lookups.
"""
index: IndexConfig | None
"""Optional. Defines the vector-based semantic search configuration.
If provided, the store will:
- Generate embeddings according to `index.embed`
- Enforce the embedding dimension given by `index.dims`
- Embed only specified JSON fields (if any) from `index.fields`
If omitted, no vector index is initialized.
"""
ttl: TTLConfig | None
"""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 ThreadTTLConfig(TypedDict, total=False):
"""Configure a default TTL for checkpointed data within threads."""
strategy: Literal["delete"]
"""Strategy to use for deleting checkpointed data.
Choices:
- "delete": Delete all checkpoints for a thread after TTL expires.
"""
default_ttl: float | None
"""Default TTL (time-to-live) in minutes for checkpointed data."""
sweep_interval_minutes: int | None
"""Interval in minutes between sweep iterations.
If omitted, a default interval will be used (typically ~ 5 minutes)."""
class CheckpointerConfig(TypedDict, total=False):
"""Configuration for the built-in checkpointer, which handles checkpointing of state.
If omitted, no checkpointer is set up (the object store will still be present, however).
"""
ttl: ThreadTTLConfig | None
"""Optional. Defines the TTL (time-to-live) behavior configuration.
If provided, the checkpointer 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.
Useful for specifying global or path-level authentication and authorization flows
(e.g., OAuth2, API key headers, etc.).
"""
securitySchemes: dict[str, dict[str, Any]]
"""Describe each security scheme recognized by your OpenAPI spec.
Keys are scheme names (e.g. "OAuth2", "ApiKeyAuth") and values are their definitions.
Example:
{
"OAuth2": {
"type": "oauth2",
"flows": {
"password": {
"tokenUrl": "/token",
"scopes": {"read": "Read data", "write": "Write data"}
}
}
}
}
"""
security: list[dict[str, list[str]]]
"""Global security requirements across all endpoints.
Each element in the list maps a security scheme (e.g. "OAuth2") to a list of scopes (e.g. ["read", "write"]).
Example:
[
{"OAuth2": ["read", "write"]},
{"ApiKeyAuth": []}
]
"""
# path => {method => security}
paths: dict[str, dict[str, list[dict[str, list[str]]]]]
"""Path-specific security overrides.
Keys are path templates (e.g., "/items/{item_id}"), mapping to:
- Keys that are HTTP methods (e.g., "GET", "POST"),
- Values are lists of security definitions (just like `security`) for that method.
Example:
{
"/private_data": {
"GET": [{"OAuth2": ["read"]}],
"POST": [{"OAuth2": ["write"]}]
}
}
"""
class AuthConfig(TypedDict, total=False):
"""Configuration for custom authentication logic and how it integrates into the OpenAPI spec."""
path: str
"""Required. Path to an instance of the Auth() class that implements custom authentication.
Format: "path/to/file.py:my_auth"
"""
disable_studio_auth: bool
"""Optional. Whether to disable LangSmith API-key authentication for requests originating the Studio.
Defaults to False, meaning that if a particular header is set, the server will verify the `x-api-key` header
value is a valid API key for the deployment's workspace. If `True`, all requests will go through your custom
authentication logic, regardless of origin of the request.
"""
openapi: SecurityConfig
"""The security configuration to include in your server's OpenAPI spec.
Example (OAuth2):
{
"securitySchemes": {
"OAuth2": {
"type": "oauth2",
"flows": {
"password": {
"tokenUrl": "/token",
"scopes": {"me": "Read user info", "items": "Manage items"}
}
}
}
},
"security": [
{"OAuth2": ["me"]}
]
}
"""
class CorsConfig(TypedDict, total=False):
"""Specifies Cross-Origin Resource Sharing (CORS) rules for your server.
If omitted, defaults are typically very restrictive (often no cross-origin requests).
Configure carefully if you want to allow usage from browsers hosted on other domains.
"""
allow_origins: list[str]
"""Optional. List of allowed origins (e.g., "https://example.com").
Default is often an empty list (no external origins).
Use "*" only if you trust all origins, as that bypasses most restrictions.
"""
allow_methods: list[str]
"""Optional. HTTP methods permitted for cross-origin requests (e.g. ["GET", "POST"]).
Default might be ["GET", "POST", "OPTIONS"] depending on your server framework.
"""
allow_headers: list[str]
"""Optional. HTTP headers that can be used in cross-origin requests (e.g. ["Content-Type", "Authorization"])."""
allow_credentials: bool
"""Optional. If `True`, cross-origin requests can include credentials (cookies, auth headers).
Default False to avoid accidentally exposing secured endpoints to untrusted sites.
"""
allow_origin_regex: str
"""Optional. A regex pattern for matching allowed origins, used if you have dynamic subdomains.
Example: "^https://.*\\.mycompany\\.com$"
"""
expose_headers: list[str]
"""Optional. List of headers that browsers are allowed to read from the response in cross-origin contexts."""
max_age: int
"""Optional. How many seconds the browser may cache preflight responses.
Default might be 600 (10 minutes). Larger values reduce preflight requests but can cause stale configurations.
"""
class ConfigurableHeaderConfig(TypedDict):
"""Customize which headers to include as configurable values in your runs.
By default, omits x-api-key, x-tenant-id, and x-service-key.
Exclusions (if provided) take precedence.
Each value can be a raw string with an optional wildcard.
"""
includes: list[str] | None
"""Headers to include (if not also matches against an 'exludes' pattern.
Examples:
- 'user-agent'
- 'x-configurable-*'
"""
excludes: list[str] | None
"""Headers to exclude. Applied before the 'includes' checks.
Examples:
- 'x-api-key'
- '*key*'
- '*token*'
"""
class HttpConfig(TypedDict, total=False):
"""Configuration for the built-in HTTP server that powers your deployment's routes and endpoints."""
app: str
"""Optional. Import path to a custom Starlette/FastAPI application to mount.
Format: "path/to/module.py:app_var"
If provided, it can override or extend the default routes.
"""
disable_assistants: bool
"""Optional. If `True`, /assistants routes are removed from the server.
Default is False (meaning /assistants is enabled).
"""
disable_threads: bool
"""Optional. If `True`, /threads routes are removed.
Default is False.
"""
disable_runs: bool
"""Optional. If `True`, /runs routes are removed.
Default is False.
"""
disable_store: bool
"""Optional. If `True`, /store routes are removed, disabling direct store interactions via HTTP.
Default is False.
"""
disable_mcp: bool
"""Optional. If `True`, /mcp routes are removed, disabling the MCP server.
Default is False.
"""
disable_meta: bool
"""Optional. Remove meta endpoints.
Set to True to disable the following endpoints: /openapi.json, /info, /metrics, /docs.
This will also make the /ok endpoint skip any DB or other checks, always returning {"ok": True}.
Default is False.
"""
cors: CorsConfig | None
"""Optional. Defines CORS restrictions. If omitted, no special rules are set and
cross-origin behavior depends on default server settings.
"""
configurable_headers: ConfigurableHeaderConfig | None
"""Optional. Defines how headers are treated for a run's configuration.
You can include or exclude headers as configurable values to condition your
agent's behavior or permissions on a request's headers."""
logging_headers: ConfigurableHeaderConfig | None
"""Optional. Defines which headers are excluded from logging."""
middleware_order: MiddlewareOrders | None
"""Optional. Defines the order in which to apply server customizations.
Choices:
- "auth_first": Authentication hooks (custom or default) are evaluated
before custom middleware.
- "middleware_first": Custom middleware is evaluated
before authentication hooks (custom or default).
Default is `middleware_first`.
"""
enable_custom_route_auth: bool
"""Optional. If `True`, authentication is enabled for custom routes,
not just the routes that are protected by default.
(Routes protected by default include /assistants, /threads, and /runs).
Default is False. This flag only affects authentication behavior
if `app` is provided and contains custom routes.
"""
class Config(TypedDict, total=False):
"""Top-level config for langgraph-cli or similar deployment tooling."""
python_version: str
"""Optional. Python version in 'major.minor' format (e.g. '3.11').
Must be at least 3.11 or greater for this deployment to function properly.
"""
node_version: str | None
"""Optional. Node.js version as a major version (e.g. '20'), if your deployment needs Node.
Must be >= 20 if provided.
"""
api_version: str | None
"""Optional. Which semantic version of the LangGraph API server to use.
Defaults to latest. Check the
[changelog](https://docs.langchain.com/langgraph-platform/langgraph-server-changelog)
for more information."""
_INTERNAL_docker_tag: str | None
"""Optional. Internal use only.
"""
base_image: str | None
"""Optional. Base image to use for the LangGraph API server.
Defaults to langchain/langgraph-api or langchain/langgraphjs-api."""
image_distro: Distros | None
"""Optional. Linux distribution for the base image.
Must be one of 'wolfi', 'debian', 'bullseye', or 'bookworm'.
If omitted, defaults to 'debian' ('latest').
"""
pip_config_file: str | None
"""Optional. Path to a pip config file (e.g., "/etc/pip.conf" or "pip.ini") for controlling
package installation (custom indices, credentials, etc.).
Only relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.
"""
pip_installer: str | None
"""Optional. Python package installer to use ('auto', 'pip', 'uv').
- 'auto' (default): Use uv for supported base images, otherwise pip
- 'pip': Force use of pip regardless of base image support
- 'uv': Force use of uv (will fail if base image doesn't support it)
"""
dockerfile_lines: list[str]
"""Optional. Additional Docker instructions that will be appended to your base Dockerfile.
Useful for installing OS packages, setting environment variables, etc.
Example:
dockerfile_lines=[
"RUN apt-get update && apt-get install -y libmagic-dev",
"ENV MY_CUSTOM_VAR=hello_world"
]
"""
dependencies: list[str]
"""List of Python dependencies to install, either from PyPI or local paths.
Examples:
- "." or "./src" if you have a local Python package
- str (aka "anthropic") for a PyPI package
- "git+https://github.com/org/repo.git@main" for a Git-based package
Defaults to an empty list, meaning no additional packages installed beyond your base environment.
"""
graphs: dict[str, str]
"""Optional. Named definitions of graphs, each pointing to a Python object.
Graphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context
managers that accept a single configuration argument (of type RunnableConfig) and return a pregel object
(instance of Stategraph, etc.).
Keys are graph names, values are "path/to/file.py:object_name".
Example:
{
"mygraph": "graphs/my_graph.py:graph_definition",
"anothergraph": "graphs/another.py:get_graph"
}
"""
env: dict[str, str] | str
"""Optional. Environment variables to set for your deployment.
- If given as a dict, keys are variable names and values are their values.
- If given as a string, it must be a path to a file containing lines in KEY=VALUE format.
Example as a dict:
env={"API_TOKEN": "abc123", "DEBUG": "true"}
Example as a file path:
env=".env"
"""
store: StoreConfig | None
"""Optional. Configuration for the built-in long-term memory store, including semantic search indexing.
If omitted, no vector index is set up (the object store will still be present, however).
"""
checkpointer: CheckpointerConfig | None
"""Optional. Configuration for the built-in checkpointer, which handles checkpointing of state.
If omitted, no checkpointer is set up (the object store will still be present, however).
"""
auth: AuthConfig | None
"""Optional. Custom authentication config, including the path to your Python auth logic and
the OpenAPI security definitions it uses.
"""
http: HttpConfig | None
"""Optional. Configuration for the built-in HTTP server, controlling which custom routes are exposed
and how cross-origin requests are handled.
"""
ui: dict[str, str] | None
"""Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.
"""
keep_pkg_tools: bool | list[str] | None
"""Optional. Control whether to retain Python packaging tools in the final image.
Allowed tools are: "pip", "setuptools", "wheel".
You can also set to true to include all packaging tools.
"""
_BUILD_TOOLS = ("pip", "setuptools", "wheel")
@@ -650,7 +154,10 @@ def validate_config(config: Config) -> Config:
"env": config.get("env", {}),
"store": config.get("store"),
"auth": config.get("auth"),
"encryption": config.get("encryption"),
"http": config.get("http"),
# Pass through webhooks config so it can be injected into the image
"webhooks": config.get("webhooks"),
"checkpointer": config.get("checkpointer"),
"ui": config.get("ui"),
"ui_config": config.get("ui_config"),
@@ -724,6 +231,14 @@ def validate_config(config: Config) -> Config:
f"Invalid auth.path format: '{auth_conf['path']}'. "
"Must be in format './path/to/file.py:attribute_name'"
)
# Validate encryption config
if encryption_conf := config.get("encryption"):
if "path" in encryption_conf:
if ":" not in encryption_conf["path"]:
raise ValueError(
f"Invalid encryption.path format: '{encryption_conf['path']}'. "
"Must be in format './path/to/file.py:attribute_name'"
)
if http_conf := config.get("http"):
if "app" in http_conf:
if ":" not in http_conf["app"]:
@@ -1110,6 +625,49 @@ def _update_auth_path(
)
def _update_encryption_path(
config_path: pathlib.Path, config: Config, local_deps: LocalDeps
) -> None:
"""Update encryption.path to use Docker container paths."""
encryption_conf = config.get("encryption")
if not encryption_conf or not (path_str := encryption_conf.get("path")):
return
module_str, sep, attr_str = path_str.partition(":")
if not sep or not module_str.startswith("."):
return # Already validated or absolute path
resolved = config_path.parent / module_str
if not resolved.exists():
raise FileNotFoundError(
f"Encryption file not found: {resolved} (from {path_str})"
)
if not resolved.is_file():
raise IsADirectoryError(f"Encryption path must be a file: {resolved}")
# Check faux packages first (higher priority)
for faux_path, (_, destpath) in local_deps.faux_pkgs.items():
if resolved.is_relative_to(faux_path):
new_path = f"{destpath}/{resolved.relative_to(faux_path)}:{attr_str}"
encryption_conf["path"] = new_path
return
# Check real packages
for real_path in local_deps.real_pkgs:
if resolved.is_relative_to(real_path):
new_path = (
f"/deps/{real_path.name}/{resolved.relative_to(real_path)}:{attr_str}"
)
encryption_conf["path"] = new_path
return
raise ValueError(
f"Encryption file '{resolved}' not covered by dependencies.\n"
"Add its parent directory to the 'dependencies' array in your config.\n"
f"Current dependencies: {config['dependencies']}"
)
def _update_http_app_path(
config_path: pathlib.Path, config: Config, local_deps: LocalDeps
) -> None:
@@ -1305,6 +863,8 @@ def python_config_to_docker(
_update_graph_paths(config_path, config, local_deps)
# Rewrite auth path, so it points to the correct location in the Docker container
_update_auth_path(config_path, config, local_deps)
# Rewrite encryption path, so it points to the correct location in the Docker container
_update_encryption_path(config_path, config, local_deps)
# Rewrite HTTP app path, so it points to the correct location in the Docker container
_update_http_app_path(config_path, config, local_deps)
@@ -1395,9 +955,16 @@ ADD {relpath} /deps/{name}
if (auth_config := config.get("auth")) is not None:
env_vars.append(f"ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'")
if (encryption_config := config.get("encryption")) is not None:
env_vars.append(f"ENV LANGGRAPH_ENCRYPTION='{json.dumps(encryption_config)}'")
if (http_config := config.get("http")) is not None:
env_vars.append(f"ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'")
# Inject webhooks configuration if provided
if (webhooks_config := config.get("webhooks")) is not None:
env_vars.append(f"ENV LANGGRAPH_WEBHOOKS='{json.dumps(webhooks_config)}'")
if (checkpointer_config := config.get("checkpointer")) is not None:
env_vars.append(
f"ENV LANGGRAPH_CHECKPOINTER='{json.dumps(checkpointer_config)}'"
@@ -1422,35 +989,51 @@ ADD {relpath} /deps/{name}
]
)
image_str = docker_tag(config, base_image, api_version)
docker_file_contents = [
f"FROM {image_str}",
"",
os.linesep.join(config["dockerfile_lines"]),
"",
installs,
"",
"# -- Installing all local dependencies --",
f"""RUN for dep in /deps/*; do \
# Prepare docker file contents
docker_file_contents = []
# Add syntax directive if we have additional contexts (requires BuildKit frontend.contexts capability)
if local_deps.additional_contexts:
docker_file_contents.extend(
[
"# syntax=docker/dockerfile:1.4",
"",
]
)
# Add main dockerfile content
docker_file_contents.extend(
[
f"FROM {image_str}",
"",
os.linesep.join(config["dockerfile_lines"]),
"",
installs,
"",
"# -- Installing all local dependencies --",
f"""RUN for dep in /deps/*; do \
echo "Installing $dep"; \
if [ -d "$dep" ]; then \
echo "Installing $dep"; \
(cd "$dep" && {global_reqs_pip_install} -e .); \
fi; \
done""",
"# -- End of local dependencies install --",
os.linesep.join(env_vars),
"",
js_inst_str,
"",
# Add pip cleanup after all installations are complete
_get_pip_cleanup_lines(
install_cmd=install_cmd,
to_uninstall=build_tools_to_uninstall,
pip_installer=pip_installer,
),
"",
f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else "",
]
"# -- End of local dependencies install --",
os.linesep.join(env_vars),
"",
js_inst_str,
"",
# Add pip cleanup after all installations are complete
_get_pip_cleanup_lines(
install_cmd=install_cmd,
to_uninstall=build_tools_to_uninstall,
pip_installer=pip_installer,
),
"",
f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else "",
]
)
additional_contexts: dict[str, str] = {}
for p in local_deps.additional_contexts:
@@ -1502,9 +1085,16 @@ def node_config_to_docker(
if (auth_config := config.get("auth")) is not None:
env_vars.append(f"ENV LANGGRAPH_AUTH='{json.dumps(auth_config)}'")
if (encryption_config := config.get("encryption")) is not None:
env_vars.append(f"ENV LANGGRAPH_ENCRYPTION='{json.dumps(encryption_config)}'")
if (http_config := config.get("http")) is not None:
env_vars.append(f"ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'")
# Inject webhooks configuration if provided
if (webhooks_config := config.get("webhooks")) is not None:
env_vars.append(f"ENV LANGGRAPH_WEBHOOKS='{json.dumps(webhooks_config)}'")
if (checkpointer_config := config.get("checkpointer")) is not None:
env_vars.append(
f"ENV LANGGRAPH_CHECKPOINTER='{json.dumps(checkpointer_config)}'"
+686
View File
@@ -0,0 +1,686 @@
from typing import Any, Literal, TypedDict
Distros = Literal["debian", "wolfi", "bullseye", "bookworm"]
MiddlewareOrders = Literal["auth_first", "middleware_first"]
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: float | None
"""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.
"""
sweep_interval_minutes: int | None
"""Optional. Interval in minutes between TTL sweep iterations.
If provided, the store will periodically delete expired items based on the TTL.
If omitted, no automatic sweeping will occur.
"""
class IndexConfig(TypedDict, total=False):
"""Configuration for indexing documents for semantic search in the store.
This governs how text is converted into embeddings and stored for vector-based lookups.
"""
dims: int
"""Required. Dimensionality of the embedding vectors you will store.
Must match the output dimension of your selected embedding model or custom embed function.
If mismatched, you will likely encounter shape/size errors when inserting or querying vectors.
Common embedding model output dimensions:
- openai:text-embedding-3-large: 3072
- openai:text-embedding-3-small: 1536
- openai:text-embedding-ada-002: 1536
- cohere:embed-english-v3.0: 1024
- cohere:embed-english-light-v3.0: 384
- cohere:embed-multilingual-v3.0: 1024
- cohere:embed-multilingual-light-v3.0: 384
"""
embed: str
"""Required. Identifier or reference to the embedding model or a custom embedding function.
The format can vary:
- "<provider>:<model_name>" for recognized providers (e.g., "openai:text-embedding-3-large")
- "path/to/module.py:function_name" for your own local embedding function
- "my_custom_embed" if it's a known alias in your system
Examples:
- "openai:text-embedding-3-large"
- "cohere:embed-multilingual-v3.0"
- "src/app.py:embeddings"
Note: Must return embeddings of dimension `dims`.
"""
fields: list[str] | None
"""Optional. List of JSON fields to extract before generating embeddings.
Defaults to ["$"], which means the entire JSON object is embedded as one piece of text.
If you provide multiple fields (e.g. ["title", "content"]), each is extracted and embedded separately,
often saving token usage if you only care about certain parts of the data.
Example:
fields=["title", "abstract", "author.biography"]
"""
class StoreConfig(TypedDict, total=False):
"""Configuration for the built-in long-term memory store.
This store can optionally perform semantic search. If you omit `index`,
the store will just handle traditional (non-embedded) data without vector lookups.
"""
index: IndexConfig | None
"""Optional. Defines the vector-based semantic search configuration.
If provided, the store will:
- Generate embeddings according to `index.embed`
- Enforce the embedding dimension given by `index.dims`
- Embed only specified JSON fields (if any) from `index.fields`
If omitted, no vector index is initialized.
"""
ttl: TTLConfig | None
"""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 ThreadTTLConfig(TypedDict, total=False):
"""Configure a default TTL for checkpointed data within threads."""
strategy: Literal["delete"]
"""Strategy to use for deleting checkpointed data.
Choices:
- "delete": Delete all checkpoints for a thread after TTL expires.
"""
default_ttl: float | None
"""Default TTL (time-to-live) in minutes for checkpointed data."""
sweep_interval_minutes: int | None
"""Interval in minutes between sweep iterations.
If omitted, a default interval will be used (typically ~ 5 minutes)."""
class SerdeConfig(TypedDict, total=False):
"""Configuration for the built-in serde, which handles checkpointing of state.
If omitted, no serde is set up (the object store will still be present, however)."""
allowed_json_modules: list[list[str]] | bool | None
"""Optional. List of allowed python modules to de-serialize custom objects from.
If provided, only the specified modules will be allowed to be deserialized.
If omitted, no modules are allowed, and the object returned will simply be a json object OR
a deserialized langchain object.
Example:
{...
"serde": {
"allowed_json_modules": [
["my_agent", "my_file", "SomeType"],
]
}
}
If you set this to True, any module will be allowed to be deserialized.
Example:
{...
"serde": {
"allowed_json_modules": true
}
}
"""
pickle_fallback: bool
"""Optional. Whether to allow pickling as a fallback for deserialization.
If True, pickling will be allowed as a fallback for deserialization.
If False, pickling will not be allowed as a fallback for deserialization.
Defaults to True if not configured."""
class CheckpointerConfig(TypedDict, total=False):
"""Configuration for the built-in checkpointer, which handles checkpointing of state.
If omitted, no checkpointer is set up (the object store will still be present, however).
"""
ttl: ThreadTTLConfig | None
"""Optional. Defines the TTL (time-to-live) behavior configuration.
If provided, the checkpointer will apply TTL settings according to the configuration.
If omitted, no TTL behavior is configured.
"""
serde: SerdeConfig | None
"""Optional. Defines the serde configuration.
If provided, the checkpointer will apply serde settings according to the configuration.
If omitted, no serde behavior is configured.
This configuration requires server version 0.5 or later to take effect.
"""
class SecurityConfig(TypedDict, total=False):
"""Configuration for OpenAPI security definitions and requirements.
Useful for specifying global or path-level authentication and authorization flows
(e.g., OAuth2, API key headers, etc.).
"""
securitySchemes: dict[str, dict[str, Any]]
"""Describe each security scheme recognized by your OpenAPI spec.
Keys are scheme names (e.g. "OAuth2", "ApiKeyAuth") and values are their definitions.
Example:
{
"OAuth2": {
"type": "oauth2",
"flows": {
"password": {
"tokenUrl": "/token",
"scopes": {"read": "Read data", "write": "Write data"}
}
}
}
}
"""
security: list[dict[str, list[str]]]
"""Global security requirements across all endpoints.
Each element in the list maps a security scheme (e.g. "OAuth2") to a list of scopes (e.g. ["read", "write"]).
Example:
[
{"OAuth2": ["read", "write"]},
{"ApiKeyAuth": []}
]
"""
# path => {method => security}
paths: dict[str, dict[str, list[dict[str, list[str]]]]]
"""Path-specific security overrides.
Keys are path templates (e.g., "/items/{item_id}"), mapping to:
- Keys that are HTTP methods (e.g., "GET", "POST"),
- Values are lists of security definitions (just like `security`) for that method.
Example:
{
"/private_data": {
"GET": [{"OAuth2": ["read"]}],
"POST": [{"OAuth2": ["write"]}]
}
}
"""
class CacheConfig(TypedDict, total=False):
cache_keys: list[str]
"""Optional. List of header keys to use for caching.
Example:
["user_id", "workspace_id"]
"""
ttl_seconds: int
"""Optional. Time-to-live in seconds for cached items.
Example:
3600
"""
max_size: int
"""Optional. Maximum size of the cache.
Example:
100
"""
class AuthConfig(TypedDict, total=False):
"""Configuration for custom authentication logic and how it integrates into the OpenAPI spec."""
path: str
"""Required. Path to an instance of the Auth() class that implements custom authentication.
Format: "path/to/file.py:my_auth"
"""
disable_studio_auth: bool
"""Optional. Whether to disable LangSmith API-key authentication for requests originating the Studio.
Defaults to False, meaning that if a particular header is set, the server will verify the `x-api-key` header
value is a valid API key for the deployment's workspace. If `True`, all requests will go through your custom
authentication logic, regardless of origin of the request.
"""
openapi: SecurityConfig
"""The security configuration to include in your server's OpenAPI spec.
Example (OAuth2):
{
"securitySchemes": {
"OAuth2": {
"type": "oauth2",
"flows": {
"password": {
"tokenUrl": "/token",
"scopes": {"me": "Read user info", "items": "Manage items"}
}
}
}
},
"security": [
{"OAuth2": ["me"]}
]
}
"""
cache: CacheConfig
"""Optional. Cache configuration for the server.
Example:
{
"cache_keys": ["user_id", "workspace_id"],
"ttl_seconds": 3600,
"max_size": 100
}
"""
class EncryptionConfig(TypedDict, total=False):
"""Configuration for custom at-rest encryption logic.
Allows you to implement custom encryption for sensitive data stored in the database,
including metadata fields and checkpoint blobs.
"""
path: str
"""Required. Path to an instance of the Encryption() class that implements custom encryption handlers.
Format: "path/to/file.py:my_encryption"
Example:
{
"encryption": {
"path": "./encryption.py:my_encryption"
}
}
"""
class CorsConfig(TypedDict, total=False):
"""Specifies Cross-Origin Resource Sharing (CORS) rules for your server.
If omitted, defaults are typically very restrictive (often no cross-origin requests).
Configure carefully if you want to allow usage from browsers hosted on other domains.
"""
allow_origins: list[str]
"""Optional. List of allowed origins (e.g., "https://example.com").
Default is often an empty list (no external origins).
Use "*" only if you trust all origins, as that bypasses most restrictions.
"""
allow_methods: list[str]
"""Optional. HTTP methods permitted for cross-origin requests (e.g. ["GET", "POST"]).
Default might be ["GET", "POST", "OPTIONS"] depending on your server framework.
"""
allow_headers: list[str]
"""Optional. HTTP headers that can be used in cross-origin requests (e.g. ["Content-Type", "Authorization"])."""
allow_credentials: bool
"""Optional. If `True`, cross-origin requests can include credentials (cookies, auth headers).
Default False to avoid accidentally exposing secured endpoints to untrusted sites.
"""
allow_origin_regex: str
"""Optional. A regex pattern for matching allowed origins, used if you have dynamic subdomains.
Example: "^https://.*\\.mycompany\\.com$"
"""
expose_headers: list[str]
"""Optional. List of headers that browsers are allowed to read from the response in cross-origin contexts."""
max_age: int
"""Optional. How many seconds the browser may cache preflight responses.
Default might be 600 (10 minutes). Larger values reduce preflight requests but can cause stale configurations.
"""
class ConfigurableHeaderConfig(TypedDict, total=False):
"""Customize which headers to include as configurable values in your runs.
By default, omits x-api-key, x-tenant-id, and x-service-key.
Exclusions (if provided) take precedence.
Each value can be a raw string with an optional wildcard.
"""
includes: list[str] | None
"""Headers to include (if not also matched against an 'excludes' pattern).
Examples:
- 'user-agent'
- 'x-configurable-*'
"""
excludes: list[str] | None
"""Headers to exclude. Applied before the 'includes' checks.
Examples:
- 'x-api-key'
- '*key*'
- '*token*'
"""
class HttpConfig(TypedDict, total=False):
"""Configuration for the built-in HTTP server that powers your deployment's routes and endpoints."""
app: str
"""Optional. Import path to a custom Starlette/FastAPI application to mount.
Format: "path/to/module.py:app_var"
If provided, it can override or extend the default routes.
"""
disable_assistants: bool
"""Optional. If `True`, /assistants routes are removed from the server.
Default is False (meaning /assistants is enabled).
"""
disable_threads: bool
"""Optional. If `True`, /threads routes are removed.
Default is False.
"""
disable_runs: bool
"""Optional. If `True`, /runs routes are removed.
Default is False.
"""
disable_store: bool
"""Optional. If `True`, /store routes are removed, disabling direct store interactions via HTTP.
Default is False.
"""
disable_mcp: bool
"""Optional. If `True`, /mcp routes are removed, disabling default support to expose the deployment as an MCP server.
Default is False.
"""
disable_a2a: bool
"""Optional. If `True`, /a2a routes are removed, disabling default support to expose the deployment as an agent-to-agent (A2A) server.
Default is False.
"""
disable_meta: bool
"""Optional. Remove meta endpoints.
Set to True to disable the following endpoints: /openapi.json, /info, /metrics, /docs.
This will also make the /ok endpoint skip any DB or other checks, always returning {"ok": True}.
Default is False.
"""
disable_ui: bool
"""Optional. If `True`, /ui routes are removed, disabling the UI server.
Default is False.
"""
disable_webhooks: bool
"""Optional. If `True`, webhooks are disabled. Runs created with an associated webhook will
still be executed, but the webhook event will not be sent.
Default is False.
"""
cors: CorsConfig | None
"""Optional. Defines CORS restrictions. If omitted, no special rules are set and
cross-origin behavior depends on default server settings.
"""
configurable_headers: ConfigurableHeaderConfig | None
"""Optional. Defines how headers are treated for a run's configuration.
You can include or exclude headers as configurable values to condition your
agent's behavior or permissions on a request's headers."""
logging_headers: ConfigurableHeaderConfig | None
"""Optional. Defines which headers are excluded from logging."""
middleware_order: MiddlewareOrders | None
"""Optional. Defines the order in which to apply server customizations.
Choices:
- "auth_first": Authentication hooks (custom or default) are evaluated
before custom middleware.
- "middleware_first": Custom middleware is evaluated
before authentication hooks (custom or default).
Default is `middleware_first`.
"""
enable_custom_route_auth: bool
"""Optional. If `True`, authentication is enabled for custom routes,
not just the routes that are protected by default.
(Routes protected by default include /assistants, /threads, and /runs).
Default is False. This flag only affects authentication behavior
if `app` is provided and contains custom routes.
"""
mount_prefix: str
"""Optional. URL prefix to prepend to all the routes.
Example:
"/api"
"""
class WebhookUrlPolicy(TypedDict, total=False):
require_https: bool
"""Enforce HTTPS scheme for absolute URLs; reject `http://` when true."""
allowed_domains: list[str]
"""Hostname allowlist. Supports exact hosts and wildcard subdomains.
Use entries like "hooks.example.com" or "*.mycorp.com". The wildcard only
matches subdomains ("foo.mycorp.com"), not the apex ("mycorp.com"). When
empty or omitted, any public host is allowed (subject to SSRF IP checks).
"""
allowed_ports: list[int]
"""Explicit port allowlist for absolute URLs.
If set, requests must use one of these ports. Defaults are respected when
a port is not present in the URL (443 for https, 80 for http).
"""
max_url_length: int
"""Maximum permitted URL length in characters; longer inputs are rejected early."""
disable_loopback: bool
"""Disallow relative URLs (internal loopback calls) when true."""
class WebhooksConfig(TypedDict, total=False):
env_prefix: str
"""Required prefix for environment variables referenced in header templates.
Acts as an allowlist boundary to prevent leaking arbitrary environment
variables. Defaults to "LG_WEBHOOK_" when omitted.
"""
url: WebhookUrlPolicy
"""URL validation policy for user-supplied webhook endpoints."""
headers: dict[str, str]
"""Static headers to include with webhook requests.
Values may contain templates of the form "${{ env.VAR }}". On startup, these
are resolved via the process environment after verifying `VAR` starts with
`env_prefix`. Mixed literals and multiple templates are allowed.
"""
class Config(TypedDict, total=False):
"""Top-level config for langgraph-cli or similar deployment tooling."""
python_version: str
"""Optional. Python version in 'major.minor' format (e.g. '3.11').
Must be at least 3.11 or greater for this deployment to function properly.
"""
node_version: str | None
"""Optional. Node.js version as a major version (e.g. '20'), if your deployment needs Node.
Must be >= 20 if provided.
"""
api_version: str | None
"""Optional. Which semantic version of the LangGraph API server to use.
Defaults to latest. Check the
[changelog](https://docs.langchain.com/langgraph-platform/langgraph-server-changelog)
for more information."""
_INTERNAL_docker_tag: str | None
"""Optional. Internal use only.
"""
base_image: str | None
"""Optional. Base image to use for the LangGraph API server.
Defaults to langchain/langgraph-api or langchain/langgraphjs-api."""
image_distro: Distros | None
"""Optional. Linux distribution for the base image.
Must be one of 'wolfi', 'debian', 'bullseye', or 'bookworm'.
If omitted, defaults to 'debian' ('latest').
"""
pip_config_file: str | None
"""Optional. Path to a pip config file (e.g., "/etc/pip.conf" or "pip.ini") for controlling
package installation (custom indices, credentials, etc.).
Only relevant if Python dependencies are installed via pip. If omitted, default pip settings are used.
"""
pip_installer: str | None
"""Optional. Python package installer to use ('auto', 'pip', 'uv').
- 'auto' (default): Use uv for supported base images, otherwise pip
- 'pip': Force use of pip regardless of base image support
- 'uv': Force use of uv (will fail if base image doesn't support it)
"""
dockerfile_lines: list[str]
"""Optional. Additional Docker instructions that will be appended to your base Dockerfile.
Useful for installing OS packages, setting environment variables, etc.
Example:
dockerfile_lines=[
"RUN apt-get update && apt-get install -y libmagic-dev",
"ENV MY_CUSTOM_VAR=hello_world"
]
"""
dependencies: list[str]
"""List of Python dependencies to install, either from PyPI or local paths.
Examples:
- "." or "./src" if you have a local Python package
- str (aka "anthropic") for a PyPI package
- "git+https://github.com/org/repo.git@main" for a Git-based package
Defaults to an empty list, meaning no additional packages installed beyond your base environment.
"""
graphs: dict[str, str]
"""Optional. Named definitions of graphs, each pointing to a Python object.
Graphs can be StateGraph, @entrypoint, or any other Pregel object OR they can point to (async) context
managers that accept a single configuration argument (of type RunnableConfig) and return a pregel object
(instance of Stategraph, etc.).
Keys are graph names, values are "path/to/file.py:object_name".
Example:
{
"mygraph": "graphs/my_graph.py:graph_definition",
"anothergraph": "graphs/another.py:get_graph"
}
"""
env: dict[str, str] | str
"""Optional. Environment variables to set for your deployment.
- If given as a dict, keys are variable names and values are their values.
- If given as a string, it must be a path to a file containing lines in KEY=VALUE format.
Example as a dict:
env={"API_TOKEN": "abc123", "DEBUG": "true"}
Example as a file path:
env=".env"
"""
store: StoreConfig | None
"""Optional. Configuration for the built-in long-term memory store, including semantic search indexing.
If omitted, no vector index is set up (the object store will still be present, however).
"""
checkpointer: CheckpointerConfig | None
"""Optional. Configuration for the built-in checkpointer, which handles checkpointing of state.
If omitted, no checkpointer is set up (the object store will still be present, however).
"""
auth: AuthConfig | None
"""Optional. Custom authentication config, including the path to your Python auth logic and
the OpenAPI security definitions it uses.
"""
encryption: EncryptionConfig | None
"""Optional. Custom at-rest encryption config, including the path to your Python encryption logic.
Allows you to implement custom encryption for sensitive data stored in the database.
"""
http: HttpConfig | None
"""Optional. Configuration for the built-in HTTP server, controlling which custom routes are exposed
and how cross-origin requests are handled.
"""
webhooks: WebhooksConfig | None
"""Optional. Webhooks configuration for outbound event delivery.
Forwarded into the container as `LANGGRAPH_WEBHOOKS`. See `WebhooksConfig`
for URL policy and header templating details.
"""
ui: dict[str, str] | None
"""Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.
"""
keep_pkg_tools: bool | list[str] | None
"""Optional. Control whether to retain Python packaging tools in the final image.
Allowed tools are: "pip", "setuptools", "wheel".
You can also set to true to include all packaging tools.
"""
__all__ = [
"Config",
"StoreConfig",
"CheckpointerConfig",
"AuthConfig",
"EncryptionConfig",
"HttpConfig",
"MiddlewareOrders",
"Distros",
"TTLConfig",
"IndexConfig",
]
+17 -7
View File
@@ -19,28 +19,38 @@ dependencies = [
path = "langgraph_cli/__init__.py"
[project.optional-dependencies]
inmem = [
"langgraph-api>=0.3,<0.5.0 ; python_version >= '3.11'",
"langgraph-api>=0.5.35,<0.6.0 ; python_version >= '3.11'",
"langgraph-runtime-inmem>=0.7 ; python_version >= '3.11'",
"python-dotenv>=0.8.0",
]
[project.urls]
Repository = "https://www.github.com/langchain-ai/langgraph"
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/cli"
Twitter = "https://x.com/LangChainAI"
Slack = "https://www.langchain.com/join-community"
Reddit = "https://www.reddit.com/r/LangChain/"
[project.scripts]
langgraph = "langgraph_cli.cli:cli"
[dependency-groups]
dev = [
"ruff",
"codespell",
test = [
"pytest",
"pytest-asyncio",
"pytest-mock",
"pytest-watch",
"mypy",
"msgspec",
]
lint = [
"ruff",
"codespell",
"mypy",
]
dev = [
{include-group = "test"},
{include-group = "lint"},
"hatch>=1.16.2",
]
[tool.uv]
default-groups = ['dev']
@@ -62,4 +72,4 @@ lint.select = [
"UP", # pyupgrade
]
lint.ignore = ["E501", "B008"]
target-version = "py310"
target-version = "py310"
@@ -8,7 +8,7 @@ authors = [
license = { text = "MIT" }
requires-python = ">=3.11,<4.0"
dependencies = [
"langgraph>=0.6.0,<0.7.0",
"langgraph>=0.6.0,<2",
"langchain-core>=0.2.14",
]
+201 -6
View File
@@ -99,6 +99,17 @@
},
"description": "Optional. Additional Docker instructions that will be appended to your base Dockerfile.\n\nUseful for installing OS packages, setting environment variables, etc."
},
"encryption": {
"anyOf": [
{
"$ref": "#/$defs/EncryptionConfig"
},
{
"type": "null"
}
],
"description": "Optional. Custom at-rest encryption config, including the path to your Python encryption logic.\n\nAllows you to implement custom encryption for sensitive data stored in the database.\n"
},
"env": {
"anyOf": [
{
@@ -199,6 +210,17 @@
}
],
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
},
"webhooks": {
"anyOf": [
{
"$ref": "#/$defs/WebhooksConfig"
},
{
"type": "null"
}
],
"description": "Optional. Webhooks configuration for outbound event delivery.\n\nForwarded into the container as `LANGGRAPH_WEBHOOKS`. See `WebhooksConfig`\nfor URL policy and header templating details.\n"
}
},
"required": [
@@ -292,6 +314,17 @@
},
"description": "Optional. Additional Docker instructions that will be appended to your base Dockerfile.\n\nUseful for installing OS packages, setting environment variables, etc."
},
"encryption": {
"anyOf": [
{
"$ref": "#/$defs/EncryptionConfig"
},
{
"type": "null"
}
],
"description": "Optional. Custom at-rest encryption config, including the path to your Python encryption logic.\n\nAllows you to implement custom encryption for sensitive data stored in the database.\n"
},
"env": {
"anyOf": [
{
@@ -391,6 +424,17 @@
}
],
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
},
"webhooks": {
"anyOf": [
{
"$ref": "#/$defs/WebhooksConfig"
},
{
"type": "null"
}
],
"description": "Optional. Webhooks configuration for outbound event delivery.\n\nForwarded into the container as `LANGGRAPH_WEBHOOKS`. See `WebhooksConfig`\nfor URL policy and header templating details.\n"
}
},
"required": [
@@ -405,6 +449,10 @@
"description": "Configuration for custom authentication logic and how it integrates into the OpenAPI spec.",
"type": "object",
"properties": {
"cache": {
"$ref": "#/$defs/CacheConfig",
"description": "Optional. Cache configuration for the server.\n"
},
"disable_studio_auth": {
"type": "boolean",
"description": "Optional. Whether to disable LangSmith API-key authentication for requests originating the Studio.\n\nDefaults to False, meaning that if a particular header is set, the server will verify the `x-api-key` header\nvalue is a valid API key for the deployment's workspace. If `True`, all requests will go through your custom\nauthentication logic, regardless of origin of the request.\n"
@@ -420,6 +468,29 @@
},
"required": []
},
"CacheConfig": {
"title": "CacheConfig",
"type": "object",
"properties": {
"cache_keys": {
"type": "array",
"items": {
"type": "string"
},
"description": "Optional. List of header keys to use for caching.\n"
},
"max_size": {
"type": "integer",
"description": "Optional. Maximum size of the cache.\n"
},
"ttl_seconds": {
"type": "integer",
"description": "Optional. Time-to-live in seconds for cached items.\n"
}
},
"required": [],
"description": "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n d = {}\n for k, v in iterable:\n d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n in the keyword argument list. For example: dict(one=1, two=2)"
},
"SecurityConfig": {
"title": "SecurityConfig",
"description": "Configuration for OpenAPI security definitions and requirements.\n\nUseful for specifying global or path-level authentication and authorization flows\n(e.g., OAuth2, API key headers, etc.).",
@@ -472,6 +543,17 @@
"description": "Configuration for the built-in checkpointer, which handles checkpointing of state.\n\nIf omitted, no checkpointer is set up (the object store will still be present, however).",
"type": "object",
"properties": {
"serde": {
"anyOf": [
{
"$ref": "#/$defs/SerdeConfig"
},
{
"type": "null"
}
],
"description": "Optional. Defines the serde configuration.\n\nIf provided, the checkpointer will apply serde settings according to the configuration.\nIf omitted, no serde behavior is configured.\n\nThis configuration requires server version 0.5 or later to take effect.\n"
},
"ttl": {
"anyOf": [
{
@@ -486,6 +568,38 @@
},
"required": []
},
"SerdeConfig": {
"title": "SerdeConfig",
"description": "Configuration for the built-in serde, which handles checkpointing of state.\n\nIf omitted, no serde is set up (the object store will still be present, however).",
"type": "object",
"properties": {
"allowed_json_modules": {
"anyOf": [
{
"type": "array",
"items": {
"type": "array",
"items": {
"type": "string"
}
}
},
{
"type": "boolean"
},
{
"type": "null"
}
],
"description": "Optional. List of allowed python modules to de-serialize custom objects from.\n\nIf provided, only the specified modules will be allowed to be deserialized.\nIf omitted, no modules are allowed, and the object returned will simply be a json object OR\na deserialized langchain object.\n"
},
"pickle_fallback": {
"type": "boolean",
"description": "Optional. Whether to allow pickling as a fallback for deserialization.\n\nIf True, pickling will be allowed as a fallback for deserialization.\nIf False, pickling will not be allowed as a fallback for deserialization.\nDefaults to True if not configured."
}
},
"required": []
},
"ThreadTTLConfig": {
"title": "ThreadTTLConfig",
"description": "Configure a default TTL for checkpointed data within threads.",
@@ -522,6 +636,17 @@
},
"required": []
},
"EncryptionConfig": {
"title": "EncryptionConfig",
"description": "Configuration for custom at-rest encryption logic.\n\nAllows you to implement custom encryption for sensitive data stored in the database,\nincluding metadata fields and checkpoint blobs.",
"type": "object",
"properties": {
"path": {
"type": "string"
}
},
"required": []
},
"HttpConfig": {
"title": "HttpConfig",
"description": "Configuration for the built-in HTTP server that powers your deployment's routes and endpoints.",
@@ -553,13 +678,17 @@
],
"description": "Optional. Defines CORS restrictions. If omitted, no special rules are set and\ncross-origin behavior depends on default server settings.\n"
},
"disable_a2a": {
"type": "boolean",
"description": "Optional. If `True`, /a2a routes are removed, disabling default support to expose the deployment as an agent-to-agent (A2A) server.\n\nDefault is False.\n"
},
"disable_assistants": {
"type": "boolean",
"description": "Optional. If `True`, /assistants routes are removed from the server.\n\nDefault is False (meaning /assistants is enabled).\n"
},
"disable_mcp": {
"type": "boolean",
"description": "Optional. If `True`, /mcp routes are removed, disabling the MCP server.\n\nDefault is False.\n"
"description": "Optional. If `True`, /mcp routes are removed, disabling default support to expose the deployment as an MCP server.\n\nDefault is False.\n"
},
"disable_meta": {
"type": "boolean",
@@ -577,6 +706,14 @@
"type": "boolean",
"description": "Optional. If `True`, /threads routes are removed.\n\nDefault is False.\n"
},
"disable_ui": {
"type": "boolean",
"description": "Optional. If `True`, /ui routes are removed, disabling the UI server.\n\nDefault is False.\n"
},
"disable_webhooks": {
"type": "boolean",
"description": "Optional. If `True`, webhooks are disabled. Runs created with an associated webhook will\nstill be executed, but the webhook event will not be sent.\n\nDefault is False.\n"
},
"enable_custom_route_auth": {
"type": "boolean",
"description": "Optional. If `True`, authentication is enabled for custom routes,\nnot just the routes that are protected by default.\n(Routes protected by default include /assistants, /threads, and /runs).\n\nDefault is False. This flag only affects authentication behavior\nif `app` is provided and contains custom routes.\n"
@@ -605,6 +742,10 @@
}
],
"description": "Optional. Defines the order in which to apply server customizations.\n"
},
"mount_prefix": {
"type": "string",
"description": "Optional. URL prefix to prepend to all the routes.\n"
}
},
"required": []
@@ -640,13 +781,10 @@
"type": "null"
}
],
"description": "Headers to include (if not also matches against an 'exludes' pattern.\n"
"description": "Headers to include (if not also matched against an 'excludes' pattern).\n"
}
},
"required": [
"excludes",
"includes"
]
"required": []
},
"CorsConfig": {
"title": "CorsConfig",
@@ -789,6 +927,63 @@
}
},
"required": []
},
"WebhooksConfig": {
"title": "WebhooksConfig",
"type": "object",
"properties": {
"env_prefix": {
"type": "string",
"description": "Required prefix for environment variables referenced in header templates.\n\nActs as an allowlist boundary to prevent leaking arbitrary environment\nvariables. Defaults to \"LG_WEBHOOK_\" when omitted.\n"
},
"headers": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Static headers to include with webhook requests.\n\nValues may contain templates of the form \"${{ env.VAR }}\". On startup, these\nare resolved via the process environment after verifying `VAR` starts with\n`env_prefix`. Mixed literals and multiple templates are allowed.\n"
},
"url": {
"$ref": "#/$defs/WebhookUrlPolicy",
"description": "URL validation policy for user-supplied webhook endpoints."
}
},
"required": [],
"description": "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n d = {}\n for k, v in iterable:\n d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n in the keyword argument list. For example: dict(one=1, two=2)"
},
"WebhookUrlPolicy": {
"title": "WebhookUrlPolicy",
"type": "object",
"properties": {
"allowed_domains": {
"type": "array",
"items": {
"type": "string"
},
"description": "Hostname allowlist. Supports exact hosts and wildcard subdomains.\n\nUse entries like \"hooks.example.com\" or \"*.mycorp.com\". The wildcard only\nmatches subdomains (\"foo.mycorp.com\"), not the apex (\"mycorp.com\"). When\nempty or omitted, any public host is allowed (subject to SSRF IP checks).\n"
},
"allowed_ports": {
"type": "array",
"items": {
"type": "integer"
},
"description": "Explicit port allowlist for absolute URLs.\n\nIf set, requests must use one of these ports. Defaults are respected when\na port is not present in the URL (443 for https, 80 for http).\n"
},
"disable_loopback": {
"type": "boolean",
"description": "Disallow relative URLs (internal loopback calls) when true."
},
"max_url_length": {
"type": "integer",
"description": "Maximum permitted URL length in characters; longer inputs are rejected early."
},
"require_https": {
"type": "boolean",
"description": "Enforce HTTPS scheme for absolute URLs; reject `http://` when true."
}
},
"required": [],
"description": "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n d = {}\n for k, v in iterable:\n d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n in the keyword argument list. For example: dict(one=1, two=2)"
}
},
"title": "LangGraph CLI Configuration",
+201 -6
View File
@@ -99,6 +99,17 @@
},
"description": "Optional. Additional Docker instructions that will be appended to your base Dockerfile.\n\nUseful for installing OS packages, setting environment variables, etc."
},
"encryption": {
"anyOf": [
{
"$ref": "#/$defs/EncryptionConfig"
},
{
"type": "null"
}
],
"description": "Optional. Custom at-rest encryption config, including the path to your Python encryption logic.\n\nAllows you to implement custom encryption for sensitive data stored in the database.\n"
},
"env": {
"anyOf": [
{
@@ -199,6 +210,17 @@
}
],
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
},
"webhooks": {
"anyOf": [
{
"$ref": "#/$defs/WebhooksConfig"
},
{
"type": "null"
}
],
"description": "Optional. Webhooks configuration for outbound event delivery.\n\nForwarded into the container as `LANGGRAPH_WEBHOOKS`. See `WebhooksConfig`\nfor URL policy and header templating details.\n"
}
},
"required": [
@@ -292,6 +314,17 @@
},
"description": "Optional. Additional Docker instructions that will be appended to your base Dockerfile.\n\nUseful for installing OS packages, setting environment variables, etc."
},
"encryption": {
"anyOf": [
{
"$ref": "#/$defs/EncryptionConfig"
},
{
"type": "null"
}
],
"description": "Optional. Custom at-rest encryption config, including the path to your Python encryption logic.\n\nAllows you to implement custom encryption for sensitive data stored in the database.\n"
},
"env": {
"anyOf": [
{
@@ -391,6 +424,17 @@
}
],
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
},
"webhooks": {
"anyOf": [
{
"$ref": "#/$defs/WebhooksConfig"
},
{
"type": "null"
}
],
"description": "Optional. Webhooks configuration for outbound event delivery.\n\nForwarded into the container as `LANGGRAPH_WEBHOOKS`. See `WebhooksConfig`\nfor URL policy and header templating details.\n"
}
},
"required": [
@@ -405,6 +449,10 @@
"description": "Configuration for custom authentication logic and how it integrates into the OpenAPI spec.",
"type": "object",
"properties": {
"cache": {
"$ref": "#/$defs/CacheConfig",
"description": "Optional. Cache configuration for the server.\n"
},
"disable_studio_auth": {
"type": "boolean",
"description": "Optional. Whether to disable LangSmith API-key authentication for requests originating the Studio.\n\nDefaults to False, meaning that if a particular header is set, the server will verify the `x-api-key` header\nvalue is a valid API key for the deployment's workspace. If `True`, all requests will go through your custom\nauthentication logic, regardless of origin of the request.\n"
@@ -420,6 +468,29 @@
},
"required": []
},
"CacheConfig": {
"title": "CacheConfig",
"type": "object",
"properties": {
"cache_keys": {
"type": "array",
"items": {
"type": "string"
},
"description": "Optional. List of header keys to use for caching.\n"
},
"max_size": {
"type": "integer",
"description": "Optional. Maximum size of the cache.\n"
},
"ttl_seconds": {
"type": "integer",
"description": "Optional. Time-to-live in seconds for cached items.\n"
}
},
"required": [],
"description": "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n d = {}\n for k, v in iterable:\n d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n in the keyword argument list. For example: dict(one=1, two=2)"
},
"SecurityConfig": {
"title": "SecurityConfig",
"description": "Configuration for OpenAPI security definitions and requirements.\n\nUseful for specifying global or path-level authentication and authorization flows\n(e.g., OAuth2, API key headers, etc.).",
@@ -472,6 +543,17 @@
"description": "Configuration for the built-in checkpointer, which handles checkpointing of state.\n\nIf omitted, no checkpointer is set up (the object store will still be present, however).",
"type": "object",
"properties": {
"serde": {
"anyOf": [
{
"$ref": "#/$defs/SerdeConfig"
},
{
"type": "null"
}
],
"description": "Optional. Defines the serde configuration.\n\nIf provided, the checkpointer will apply serde settings according to the configuration.\nIf omitted, no serde behavior is configured.\n\nThis configuration requires server version 0.5 or later to take effect.\n"
},
"ttl": {
"anyOf": [
{
@@ -486,6 +568,38 @@
},
"required": []
},
"SerdeConfig": {
"title": "SerdeConfig",
"description": "Configuration for the built-in serde, which handles checkpointing of state.\n\nIf omitted, no serde is set up (the object store will still be present, however).",
"type": "object",
"properties": {
"allowed_json_modules": {
"anyOf": [
{
"type": "array",
"items": {
"type": "array",
"items": {
"type": "string"
}
}
},
{
"type": "boolean"
},
{
"type": "null"
}
],
"description": "Optional. List of allowed python modules to de-serialize custom objects from.\n\nIf provided, only the specified modules will be allowed to be deserialized.\nIf omitted, no modules are allowed, and the object returned will simply be a json object OR\na deserialized langchain object.\n"
},
"pickle_fallback": {
"type": "boolean",
"description": "Optional. Whether to allow pickling as a fallback for deserialization.\n\nIf True, pickling will be allowed as a fallback for deserialization.\nIf False, pickling will not be allowed as a fallback for deserialization.\nDefaults to True if not configured."
}
},
"required": []
},
"ThreadTTLConfig": {
"title": "ThreadTTLConfig",
"description": "Configure a default TTL for checkpointed data within threads.",
@@ -522,6 +636,17 @@
},
"required": []
},
"EncryptionConfig": {
"title": "EncryptionConfig",
"description": "Configuration for custom at-rest encryption logic.\n\nAllows you to implement custom encryption for sensitive data stored in the database,\nincluding metadata fields and checkpoint blobs.",
"type": "object",
"properties": {
"path": {
"type": "string"
}
},
"required": []
},
"HttpConfig": {
"title": "HttpConfig",
"description": "Configuration for the built-in HTTP server that powers your deployment's routes and endpoints.",
@@ -553,13 +678,17 @@
],
"description": "Optional. Defines CORS restrictions. If omitted, no special rules are set and\ncross-origin behavior depends on default server settings.\n"
},
"disable_a2a": {
"type": "boolean",
"description": "Optional. If `True`, /a2a routes are removed, disabling default support to expose the deployment as an agent-to-agent (A2A) server.\n\nDefault is False.\n"
},
"disable_assistants": {
"type": "boolean",
"description": "Optional. If `True`, /assistants routes are removed from the server.\n\nDefault is False (meaning /assistants is enabled).\n"
},
"disable_mcp": {
"type": "boolean",
"description": "Optional. If `True`, /mcp routes are removed, disabling the MCP server.\n\nDefault is False.\n"
"description": "Optional. If `True`, /mcp routes are removed, disabling default support to expose the deployment as an MCP server.\n\nDefault is False.\n"
},
"disable_meta": {
"type": "boolean",
@@ -577,6 +706,14 @@
"type": "boolean",
"description": "Optional. If `True`, /threads routes are removed.\n\nDefault is False.\n"
},
"disable_ui": {
"type": "boolean",
"description": "Optional. If `True`, /ui routes are removed, disabling the UI server.\n\nDefault is False.\n"
},
"disable_webhooks": {
"type": "boolean",
"description": "Optional. If `True`, webhooks are disabled. Runs created with an associated webhook will\nstill be executed, but the webhook event will not be sent.\n\nDefault is False.\n"
},
"enable_custom_route_auth": {
"type": "boolean",
"description": "Optional. If `True`, authentication is enabled for custom routes,\nnot just the routes that are protected by default.\n(Routes protected by default include /assistants, /threads, and /runs).\n\nDefault is False. This flag only affects authentication behavior\nif `app` is provided and contains custom routes.\n"
@@ -605,6 +742,10 @@
}
],
"description": "Optional. Defines the order in which to apply server customizations.\n"
},
"mount_prefix": {
"type": "string",
"description": "Optional. URL prefix to prepend to all the routes.\n"
}
},
"required": []
@@ -640,13 +781,10 @@
"type": "null"
}
],
"description": "Headers to include (if not also matches against an 'exludes' pattern.\n"
"description": "Headers to include (if not also matched against an 'excludes' pattern).\n"
}
},
"required": [
"excludes",
"includes"
]
"required": []
},
"CorsConfig": {
"title": "CorsConfig",
@@ -789,6 +927,63 @@
}
},
"required": []
},
"WebhooksConfig": {
"title": "WebhooksConfig",
"type": "object",
"properties": {
"env_prefix": {
"type": "string",
"description": "Required prefix for environment variables referenced in header templates.\n\nActs as an allowlist boundary to prevent leaking arbitrary environment\nvariables. Defaults to \"LG_WEBHOOK_\" when omitted.\n"
},
"headers": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Static headers to include with webhook requests.\n\nValues may contain templates of the form \"${{ env.VAR }}\". On startup, these\nare resolved via the process environment after verifying `VAR` starts with\n`env_prefix`. Mixed literals and multiple templates are allowed.\n"
},
"url": {
"$ref": "#/$defs/WebhookUrlPolicy",
"description": "URL validation policy for user-supplied webhook endpoints."
}
},
"required": [],
"description": "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n d = {}\n for k, v in iterable:\n d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n in the keyword argument list. For example: dict(one=1, two=2)"
},
"WebhookUrlPolicy": {
"title": "WebhookUrlPolicy",
"type": "object",
"properties": {
"allowed_domains": {
"type": "array",
"items": {
"type": "string"
},
"description": "Hostname allowlist. Supports exact hosts and wildcard subdomains.\n\nUse entries like \"hooks.example.com\" or \"*.mycorp.com\". The wildcard only\nmatches subdomains (\"foo.mycorp.com\"), not the apex (\"mycorp.com\"). When\nempty or omitted, any public host is allowed (subject to SSRF IP checks).\n"
},
"allowed_ports": {
"type": "array",
"items": {
"type": "integer"
},
"description": "Explicit port allowlist for absolute URLs.\n\nIf set, requests must use one of these ports. Defaults are respected when\na port is not present in the URL (443 for https, 80 for http).\n"
},
"disable_loopback": {
"type": "boolean",
"description": "Disallow relative URLs (internal loopback calls) when true."
},
"max_url_length": {
"type": "integer",
"description": "Maximum permitted URL length in characters; longer inputs are rejected early."
},
"require_https": {
"type": "boolean",
"description": "Enforce HTTPS scheme for absolute URLs; reject `http://` when true."
}
},
"required": [],
"description": "dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n d = {}\n for k, v in iterable:\n d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n in the keyword argument list. For example: dict(one=1, two=2)"
}
},
"title": "LangGraph CLI Configuration",
@@ -141,6 +141,7 @@ services:
additional_contexts:
- cli_1: {str(pathlib.Path(__file__).parent.parent.parent.parent.absolute())}
dockerfile_inline: |
# syntax=docker/dockerfile:1.4
FROM langchain/langgraph-api:3.11
# -- Adding local package . --
ADD . /deps/cli
+139
View File
@@ -48,6 +48,8 @@ def test_validate_config():
"env": {},
"store": None,
"auth": None,
"encryption": None,
"webhooks": None,
"checkpointer": None,
"http": None,
"ui": None,
@@ -74,6 +76,8 @@ def test_validate_config():
"env": env,
"store": None,
"auth": None,
"encryption": None,
"webhooks": None,
"checkpointer": None,
"http": None,
"ui": None,
@@ -419,6 +423,7 @@ def test_config_to_docker_simple():
"langchain/langgraph-api",
)
expected_docker_stdin = f"""\
# syntax=docker/dockerfile:1.4
FROM langchain/langgraph-api:3.11
# -- Installing local requirements --
COPY --from=outer-requirements.txt requirements.txt /deps/outer-graphs_reqs_a/graphs_reqs_a/requirements.txt
@@ -482,6 +487,7 @@ def test_config_to_docker_outside_path():
)
expected_docker_stdin = (
"""\
# syntax=docker/dockerfile:1.4
FROM langchain/langgraph-api:3.11
# -- Adding non-package dependency unit_tests --
ADD . /deps/outer-unit_tests/unit_tests
@@ -746,6 +752,60 @@ RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not foun
assert additional_contexts == {}
def test_config_to_docker_python_encryption():
# Test that encryption config is included in validation
graphs = {"agent": "./agent.py:graph"}
validated = validate_config(
{
"python_version": "3.11",
"graphs": graphs,
"dependencies": ["."],
"encryption": {"path": "./encryption.py:encryption"},
}
)
# Verify that encryption config is preserved after validation
assert validated.get("encryption") is not None
assert validated["encryption"]["path"] == "./encryption.py:encryption"
def test_config_to_docker_python_encryption_bad_path():
# Test that invalid encryption path format raises ValueError
graphs = {"agent": "./agent.py:graph"}
with pytest.raises(ValueError, match="Invalid encryption.path format"):
validate_config(
{
"python_version": "3.11",
"graphs": graphs,
"dependencies": ["."],
"encryption": {"path": "./encryption.py"}, # Missing :attribute
}
)
def test_config_to_docker_python_encryption_formatted():
# Test that encryption config is properly formatted in Docker output
graphs = {"agent": "./graphs/agent.py:graph"}
actual_docker_stdin, additional_contexts = config_to_docker(
PATH_TO_CONFIG,
validate_config(
{
"python_version": "3.11",
"dependencies": ["."],
"graphs": graphs,
"encryption": {"path": "./agent.py:my_encryption"},
}
),
"langchain/langgraph-api",
)
# Verify that LANGGRAPH_ENCRYPTION is in the docker output with the correct path
assert "LANGGRAPH_ENCRYPTION=" in actual_docker_stdin
assert (
"/deps/outer-unit_tests/unit_tests/agent.py:my_encryption"
in actual_docker_stdin
)
def test_config_to_docker_nodejs_internal_docker_tag():
graphs = {"agent": "./graphs/agent.js:graph"}
actual_docker_stdin, additional_contexts = config_to_docker(
@@ -779,6 +839,85 @@ RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not foun
assert additional_contexts == {}
def _extract_env_json(dockerfile: str, var_name: str) -> dict:
"""Helper to extract and parse a JSON value from an ENV line in a Dockerfile."""
line_prefix = f"ENV {var_name}='"
for line in dockerfile.splitlines():
if line.startswith(line_prefix) and line.endswith("'"):
json_str = line[len(line_prefix) : -1]
return json.loads(json_str)
raise AssertionError(f"{var_name} not found in Dockerfile env lines")
def test_config_to_docker_webhooks_python():
graphs = {"agent": "./agent.py:graph"}
webhooks = {
"env_prefix": "LG_WEBHOOK_",
"url": {
"require_https": True,
"allowed_domains": ["hooks.example.com", "*.example.org"],
"allowed_ports": [443],
"max_url_length": 1024,
"disable_loopback": False,
},
"headers": {
"x-auth": "${{ env.LG_WEBHOOK_TOKEN }}",
"x-mixed": "Bearer ${{ env.LG_WEBHOOK_TOKEN }}-suffix",
},
}
dockerfile, _ = config_to_docker(
PATH_TO_CONFIG,
validate_config(
{
"dependencies": ["."],
"graphs": graphs,
"webhooks": webhooks,
}
),
"langchain/langgraph-api",
)
# Ensure the ENV line is present and the payload round-trips via JSON
parsed = _extract_env_json(dockerfile, "LANGGRAPH_WEBHOOKS")
assert parsed == webhooks
def test_config_to_docker_webhooks_node():
graphs = {"agent": "./graphs/agent.js:graph"}
webhooks = {
"env_prefix": "LG_WEBHOOK_",
"url": {"require_https": True},
"headers": {"x-auth": "${{ env.LG_WEBHOOK_TOKEN }}"},
}
dockerfile, _ = config_to_docker(
PATH_TO_CONFIG,
validate_config(
{
"node_version": "20",
"graphs": graphs,
"webhooks": webhooks,
}
),
"langchain/langgraphjs-api",
)
parsed = _extract_env_json(dockerfile, "LANGGRAPH_WEBHOOKS")
assert parsed == webhooks
def test_config_to_docker_no_webhooks():
graphs = {"agent": "./agent.py:graph"}
dockerfile, _ = config_to_docker(
PATH_TO_CONFIG,
validate_config({"dependencies": ["."], "graphs": graphs}),
"langchain/langgraph-api",
)
assert "ENV LANGGRAPH_WEBHOOKS=" not in dockerfile
def test_config_to_docker_gen_ui_python():
graphs = {"agent": "./agent.py:graph"}
actual_docker_stdin, additional_contexts = config_to_docker(
+1509 -670
View File
File diff suppressed because it is too large Load Diff
@@ -1,14 +0,0 @@
{
"permissions": {
"allow": [
"Bash(rg:*)",
"Bash(python:*)",
"Bash(grep:*)",
"Bash(sed:*)",
"Bash(awk:*)",
"Bash(uv run mypy:*)",
"Bash(uv run:*)"
],
"deny": []
}
}
+3 -3
View File
@@ -64,10 +64,10 @@ While LangGraph can be used standalone, it also integrates seamlessly with any L
- [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
- [LangSmith Deployment](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
- [LangChain](https://python.langchain.com/docs/introduction/) Provides integrations and composable components to streamline LLM application development.
- [LangChain](https://docs.langchain.com/oss/python/langchain/overview) Provides integrations and composable components to streamline LLM application development.
> [!NOTE]
> Looking for the JS version of LangGraph? See the [JS repo](https://github.com/langchain-ai/langgraphjs) and the [JS docs](https://langchain-ai.github.io/langgraphjs/).
> Looking for the JS version of LangGraph? See the [JS repo](https://github.com/langchain-ai/langgraphjs) and the [JS docs](https://docs.langchain.com/oss/javascript/langgraph/overview).
## Additional resources
@@ -81,4 +81,4 @@ While LangGraph can be used standalone, it also integrates seamlessly with any L
## Acknowledgements
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
LangGraph is inspired by [Pregel](https://research.google/pubs/pub37252/) and [Apache Beam](https://beam.apache.org/). The public interface draws inspiration from [NetworkX](https://networkx.org/documentation/latest/). LangGraph is built by LangChain Inc, the creators of LangChain, but can be used without LangChain.
@@ -77,6 +77,8 @@ CONF = cast(Literal["configurable"], sys.intern("configurable"))
# key for the configurable dict in RunnableConfig
NULL_TASK_ID = sys.intern("00000000-0000-0000-0000-000000000000")
# the task_id to use for writes that are not associated with a task
OVERWRITE = sys.intern("__overwrite__")
# dict key for the overwrite value, used as `{'__overwrite__': value}`
# redefined to avoid circular import with langgraph.constants
_TAG_HIDDEN = sys.intern("langsmith:hidden")
@@ -201,6 +201,10 @@ def get_cached_annotated_keys(obj: type[Any]) -> tuple[str, ...]:
keys: list[str] = []
for base in reversed(obj.__mro__):
ann = base.__dict__.get("__annotations__")
# In Python 3.14+, Pydantic models use descriptors for __annotations__
# so we need to fall back to getattr if __dict__.get returns None
if ann is None:
ann = getattr(base, "__annotations__", None)
if ann is None or isinstance(ann, types.GetSetDescriptorType):
continue
keys.extend(ann.keys())
@@ -481,9 +481,9 @@ def is_async_callable(
) -> TypeGuard[Callable[..., Awaitable]]:
"""Check if a function is async."""
return (
asyncio.iscoroutinefunction(func)
inspect.iscoroutinefunction(func)
or hasattr(func, "__call__")
and asyncio.iscoroutinefunction(func.__call__)
and inspect.iscoroutinefunction(func.__call__)
)
+24 -8
View File
@@ -39,14 +39,19 @@ class BaseChannel(Generic[Value, Update, Checkpoint], ABC):
def copy(self) -> Self:
"""Return a copy of the channel.
By default, delegates to `checkpoint()` and `from_checkpoint()`.
Subclasses can override this method with a more efficient implementation."""
Subclasses can override this method with a more efficient implementation.
"""
return self.from_checkpoint(self.checkpoint())
def checkpoint(self) -> Checkpoint | Any:
"""Return a serializable representation of the channel's current state.
Raises `EmptyChannelError` if the channel is empty (never updated yet),
or doesn't support checkpoints."""
or doesn't support checkpoints.
"""
try:
return self.get()
except EmptyChannelError:
@@ -55,7 +60,9 @@ class BaseChannel(Generic[Value, Update, Checkpoint], ABC):
@abstractmethod
def from_checkpoint(self, checkpoint: Checkpoint | Any) -> Self:
"""Return a new identical channel, optionally initialized from a checkpoint.
If the checkpoint contains complex data structures, they should be copied."""
If the checkpoint contains complex data structures, they should be copied.
"""
# read methods
@@ -67,8 +74,9 @@ class BaseChannel(Generic[Value, Update, Checkpoint], ABC):
def is_available(self) -> bool:
"""Return `True` if the channel is available (not empty), `False` otherwise.
Subclasses should override this method to provide a more efficient
implementation than calling get() and catching `EmptyChannelError`.
implementation than calling `get()` and catching `EmptyChannelError`.
"""
try:
self.get()
@@ -83,21 +91,29 @@ class BaseChannel(Generic[Value, Update, Checkpoint], ABC):
"""Update the channel's value with the given sequence of updates.
The order of the updates in the sequence is arbitrary.
This method is called by Pregel for all channels at the end of each step.
If there are no updates, it is called with an empty sequence.
Raises `InvalidUpdateError` if the sequence of updates is invalid.
Returns `True` if the channel was updated, `False` otherwise."""
def consume(self) -> bool:
"""Notify the channel that a subscribed task ran. By default, no-op.
A channel can use this method to modify its state, preventing the value
from being consumed again.
"""Notify the channel that a subscribed task ran.
By default, no-op.
A channel can use this method to modify its state, preventing the value from being consumed again.
Returns `True` if the channel was updated, `False` otherwise.
"""
return False
def finish(self) -> bool:
"""Notify the channel that the Pregel run is finishing. By default, no-op.
"""Notify the channel that the Pregel run is finishing.
By default, no-op.
A channel can use this method to modify its state, preventing finish.
Returns `True` if the channel was updated, `False` otherwise.
+32 -3
View File
@@ -1,12 +1,19 @@
import collections.abc
from collections.abc import Callable, Sequence
from typing import Generic
from typing import Any, Generic
from typing_extensions import NotRequired, Required, Self
from langgraph._internal._constants import OVERWRITE
from langgraph._internal._typing import MISSING
from langgraph.channels.base import BaseChannel, Value
from langgraph.errors import EmptyChannelError
from langgraph.errors import (
EmptyChannelError,
ErrorCode,
InvalidUpdateError,
create_error_message,
)
from langgraph.types import Overwrite
__all__ = ("BinaryOperatorAggregate",)
@@ -22,6 +29,15 @@ def _strip_extras(t): # type: ignore[no-untyped-def]
return t
def _get_overwrite(value: Any) -> tuple[bool, Any]:
"""Inspects the given value and returns (is_overwrite, overwrite_value)."""
if isinstance(value, Overwrite):
return True, value.value
if isinstance(value, dict) and set(value.keys()) == {OVERWRITE}:
return True, value[OVERWRITE]
return False, None
class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]):
"""Stores the result of applying a binary operator to the current value and each new value.
@@ -89,8 +105,21 @@ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]):
if self.value is MISSING:
self.value = values[0]
values = values[1:]
seen_overwrite: bool = False
for value in values:
self.value = self.operator(self.value, value)
is_overwrite, overwrite_value = _get_overwrite(value)
if is_overwrite:
if seen_overwrite:
msg = create_error_message(
message="Can receive only one Overwrite value per super-step.",
error_code=ErrorCode.INVALID_CONCURRENT_GRAPH_UPDATE,
)
raise InvalidUpdateError(msg)
self.value = overwrite_value
seen_overwrite = True
continue
if not seen_overwrite:
self.value = self.operator(self.value, value)
return True
def get(self) -> Value:
+9 -9
View File
@@ -32,8 +32,8 @@ def get_config() -> RunnableConfig:
def get_store() -> BaseStore:
"""Access LangGraph store from inside a graph node or entrypoint task at runtime.
Can be called from inside any [StateGraph][langgraph.graph.StateGraph] node or
functional API [task][langgraph.func.task], as long as the StateGraph or the [entrypoint][langgraph.func.entrypoint]
Can be called from inside any [`StateGraph`][langgraph.graph.StateGraph] node or
functional API [`task`][langgraph.func.task], as long as the `StateGraph` or the [`entrypoint`][langgraph.func.entrypoint]
was initialized with a store, e.g.:
```python
@@ -53,10 +53,10 @@ def get_store() -> BaseStore:
!!! warning "Async with Python < 3.11"
If you are using Python < 3.11 and are running LangGraph asynchronously,
`get_store()` won't work since it uses [contextvar](https://docs.python.org/3/library/contextvars.html) propagation (only available in [Python >= 3.11](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task)).
`get_store()` won't work since it uses [`contextvar`](https://docs.python.org/3/library/contextvars.html) propagation (only available in [Python >= 3.11](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task)).
Example: Using with StateGraph
Example: Using with `StateGraph`
```python
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START
@@ -124,17 +124,17 @@ def get_store() -> BaseStore:
def get_stream_writer() -> StreamWriter:
"""Access LangGraph [StreamWriter][langgraph.types.StreamWriter] from inside a graph node or entrypoint task at runtime.
"""Access LangGraph [`StreamWriter`][langgraph.types.StreamWriter] from inside a graph node or entrypoint task at runtime.
Can be called from inside any [StateGraph][langgraph.graph.StateGraph] node or
functional API [task][langgraph.func.task].
Can be called from inside any [`StateGraph`][langgraph.graph.StateGraph] node or
functional API [`task`][langgraph.func.task].
!!! warning "Async with Python < 3.11"
If you are using Python < 3.11 and are running LangGraph asynchronously,
`get_stream_writer()` won't work since it uses [contextvar](https://docs.python.org/3/library/contextvars.html) propagation (only available in [Python >= 3.11](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task)).
`get_stream_writer()` won't work since it uses [`contextvar`](https://docs.python.org/3/library/contextvars.html) propagation (only available in [Python >= 3.11](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task)).
Example: Using with StateGraph
Example: Using with `StateGraph`
```python
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START
+7 -7
View File
@@ -37,8 +37,8 @@ class ErrorCode(Enum):
def create_error_message(*, message: str, error_code: ErrorCode) -> str:
return (
f"{message}\n"
"For troubleshooting, visit: https://python.langchain.com/docs/"
f"troubleshooting/errors/{error_code.value}"
"For troubleshooting, visit: https://docs.langchain.com/oss/python/langgraph/"
f"errors/{error_code.value}"
)
@@ -48,9 +48,9 @@ class GraphRecursionError(RecursionError):
This prevents infinite loops. To increase the maximum number of steps,
run your graph with a config specifying a higher `recursion_limit`.
Troubleshooting Guides:
Troubleshooting guides:
- [GRAPH_RECURSION_LIMIT](https://docs.langchain.com/oss/python/langgraph/GRAPH_RECURSION_LIMIT)
- [`GRAPH_RECURSION_LIMIT`](https://docs.langchain.com/oss/python/langgraph/GRAPH_RECURSION_LIMIT)
Examples:
@@ -68,10 +68,10 @@ class GraphRecursionError(RecursionError):
class InvalidUpdateError(Exception):
"""Raised when attempting to update a channel with an invalid set of updates.
Troubleshooting Guides:
Troubleshooting guides:
- [INVALID_CONCURRENT_GRAPH_UPDATE](https://docs.langchain.com/oss/python/langgraph/INVALID_CONCURRENT_GRAPH_UPDATE)
- [INVALID_GRAPH_NODE_RETURN_VALUE](https://docs.langchain.com/oss/python/langgraph/INVALID_GRAPH_NODE_RETURN_VALUE)
- [`INVALID_CONCURRENT_GRAPH_UPDATE`](https://docs.langchain.com/oss/python/langgraph/INVALID_CONCURRENT_GRAPH_UPDATE)
- [`INVALID_GRAPH_NODE_RETURN_VALUE`](https://docs.langchain.com/oss/python/langgraph/INVALID_GRAPH_NODE_RETURN_VALUE)
"""
pass
+5 -5
View File
@@ -151,13 +151,13 @@ def task(
@task
def add_one(a: int) -> int:
def add_one_task(a: int) -> int:
return a + 1
@entrypoint()
def add_one(numbers: list[int]) -> list[int]:
futures = [add_one(n) for n in numbers]
futures = [add_one_task(n) for n in numbers]
results = [f.result() for f in futures]
return results
@@ -173,13 +173,13 @@ def task(
@task
async def add_one(a: int) -> int:
async def add_one_task(a: int) -> int:
return a + 1
@entrypoint()
async def add_one(numbers: list[int]) -> list[int]:
futures = [add_one(n) for n in numbers]
futures = [add_one_task(n) for n in numbers]
return asyncio.gather(*futures)
@@ -357,7 +357,7 @@ class entrypoint(Generic[ContextT]):
my_workflow.invoke("hello", config)
```
Example: Using entrypoint.final to save a value
Example: Using `entrypoint.final` to save a value
The `entrypoint.final` object allows you to return a value while saving
a different value to the checkpoint. This value will be accessible
in the next invocation of the entrypoint via the `previous` parameter, as
+14 -10
View File
@@ -30,6 +30,7 @@ __all__ = (
"add_messages",
"MessagesState",
"MessageGraph",
"REMOVE_ALL_MESSAGES",
)
Messages = list[MessageLikeRepresentation] | MessageLikeRepresentation
@@ -69,11 +70,11 @@ def add_messages(
new message has the same ID as an existing message.
Args:
left: The base list of messages.
right: The list of messages (or single message) to merge
left: The base list of `Messages`.
right: The list of `Messages` (or single `Message`) to merge
into the base list.
format: The format to return messages in. If `None` then messages will be
returned as is. If `langchain-openai` then messages will be returned as
format: The format to return messages in. If `None` then `Messages` will be
returned as is. If `langchain-openai` then `Messages` will be returned as
`BaseMessage` objects with their contents formatted to match OpenAI message
format, meaning contents can be string, `'text'` blocks, or `'image_url'` blocks
and tool responses are returned as their own `ToolMessage` objects.
@@ -85,10 +86,10 @@ def add_messages(
Returns:
A new list of messages with the messages from `right` merged into `left`.
If a message in `right` has the same ID as a message in `left`, the
message from `right` will replace the message from `left`.
message from `right` will replace the message from `left`.
Example:
```python title="Basic usage"
Example: Basic usage
```python
from langchain_core.messages import AIMessage, HumanMessage
msgs1 = [HumanMessage(content="Hello", id="1")]
@@ -97,14 +98,16 @@ def add_messages(
# [HumanMessage(content='Hello', id='1'), AIMessage(content='Hi there!', id='2')]
```
```python title="Overwrite existing message"
Example: Overwrite existing message
```python
msgs1 = [HumanMessage(content="Hello", id="1")]
msgs2 = [HumanMessage(content="Hello again", id="1")]
add_messages(msgs1, msgs2)
# [HumanMessage(content='Hello again', id='1')]
```
```python title="Use in a StateGraph"
Example: Use in a StateGraph
```python
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph
@@ -123,7 +126,8 @@ def add_messages(
# {'messages': [AIMessage(content='Hello', id=...)]}
```
```python title="Use OpenAI message format"
Example: Use OpenAI message format
```python
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, add_messages
+53 -27
View File
@@ -110,15 +110,24 @@ def _get_node_name(node: StateNode[Any, ContextT]) -> str:
class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
"""A graph whose nodes communicate by reading and writing to a shared state.
The signature of each node is State -> Partial<State>.
The signature of each node is `State -> Partial<State>`.
Each state key can optionally be annotated with a reducer function that
will be used to aggregate the values of that key received from multiple nodes.
The signature of a reducer function is (Value, Value) -> Value.
The signature of a reducer function is `(Value, Value) -> Value`.
!!! warning
`StateGraph` is a builder class and cannot be used directly for execution.
You must first call `.compile()` to create an executable graph that supports
methods like `invoke()`, `stream()`, `astream()`, and `ainvoke()`. See the
`CompiledStateGraph` documentation for more details.
Args:
state_schema: The schema class that defines the state.
context_schema: The schema class that defines the runtime context.
Use this to expose immutable context data to your nodes, like `user_id`, `db_conn`, etc.
input_schema: The schema class that defines the input to the graph.
output_schema: The schema class that defines the output from the graph.
@@ -289,7 +298,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
destinations: dict[str, str] | tuple[str, ...] | None = None,
**kwargs: Unpack[DeprecatedKwargs],
) -> Self:
"""Add a new node to the state graph, input schema is inferred as the state schema.
"""Add a new node to the `StateGraph`, input schema is inferred as the state schema.
Will take the name of the function/runnable as the node name.
"""
...
@@ -307,7 +316,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
destinations: dict[str, str] | tuple[str, ...] | None = None,
**kwargs: Unpack[DeprecatedKwargs],
) -> Self:
"""Add a new node to the state graph, input schema is specified.
"""Add a new node to the `StateGraph`, input schema is specified.
Will take the name of the function/runnable as the node name.
"""
...
@@ -326,7 +335,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
destinations: dict[str, str] | tuple[str, ...] | None = None,
**kwargs: Unpack[DeprecatedKwargs],
) -> Self:
"""Add a new node to the state graph, input schema is inferred as the state schema."""
"""Add a new node to the `StateGraph`, input schema is inferred as the state schema."""
...
@overload
@@ -343,7 +352,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
destinations: dict[str, str] | tuple[str, ...] | None = None,
**kwargs: Unpack[DeprecatedKwargs],
) -> Self:
"""Add a new node to the state graph, input schema is specified."""
"""Add a new node to the `StateGraph`, input schema is specified."""
...
def add_node(
@@ -359,25 +368,31 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
destinations: dict[str, str] | tuple[str, ...] | None = None,
**kwargs: Unpack[DeprecatedKwargs],
) -> Self:
"""Add a new node to the state graph.
"""Add a new node to the `StateGraph`.
Args:
node: The function or runnable this node will run.
If a string is provided, it will be used as the node name, and action will be used as the function or runnable.
action: The action associated with the node.
Will be used as the node function or runnable if `node` is a string (node name).
defer: Whether to defer the execution of the node until the run is about to end.
metadata: The metadata associated with the node.
input_schema: The input schema for the node. (default: the graph's state schema)
input_schema: The input schema for the node. (Default: the graph's state schema)
retry_policy: The retry policy for the node.
If a sequence is provided, the first matching policy will be applied.
cache_policy: The cache policy for the node.
destinations: Destinations that indicate where a node can route to.
This is useful for edgeless graphs with nodes that return `Command` objects.
If a dict is provided, the keys will be used as the target node names and the values will be used as the labels for the edges.
If a tuple is provided, the values will be used as the target node names.
Useful for edgeless graphs with nodes that return `Command` objects.
If a `dict` is provided, the keys will be used as the target node names and the values will be used as the labels for the edges.
If a `tuple` is provided, the values will be used as the target node names.
!!! note
This is only used for graph rendering and doesn't have any effect on the graph execution.
Example:
@@ -415,7 +430,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
```
Returns:
Self: The instance of the state graph, allowing for method chaining.
Self: The instance of the `StateGraph`, allowing for method chaining.
"""
if (retry := kwargs.get("retry", MISSING)) is not MISSING:
warnings.warn(
@@ -570,7 +585,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
ValueError: If the start key is `'END'` or if the start key or end key is not present in the graph.
Returns:
Self: The instance of the state graph, allowing for method chaining.
Self: The instance of the `StateGraph`, allowing for method chaining.
"""
if self.compiled:
logger.warning(
@@ -622,17 +637,20 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
Args:
source: The starting node. This conditional edge will run when
exiting this node.
path: 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 mapping of paths to node
names. If omitted the paths returned by `path` should be node names.
path: 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 mapping of paths to node names.
If omitted the paths returned by `path` should be node names.
Returns:
Self: The instance of the graph, allowing for method chaining.
!!! warning
Without typehints on the `path` function's return value (e.g., `-> Literal["foo", "__end__"]:`)
Without type hints 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
@@ -667,7 +685,9 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
Args:
nodes: A sequence of `StateNode` (callables that accept a `state` arg) or `(name, StateNode)` tuples.
If no names are provided, the name will be inferred from the node object (e.g. a `Runnable` or a `Callable` name).
Each node will be executed in the order provided.
Raises:
@@ -675,7 +695,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
ValueError: If the sequence contains duplicate node names.
Returns:
Self: The instance of the state graph, allowing for method chaining.
Self: The instance of the `StateGraph`, allowing for method chaining.
"""
if len(nodes) < 1:
raise ValueError("Sequence requires at least one node.")
@@ -724,11 +744,14 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
"""Sets a conditional entry point in the graph.
Args:
path: 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 mapping of paths to node
names. If omitted the paths returned by `path` should be node names.
path: 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 mapping of paths to node names.
If omitted the paths returned by `path` should be node names.
Returns:
Self: The instance of the graph, allowing for method chaining.
@@ -808,16 +831,19 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
debug: bool = False,
name: str | None = None,
) -> CompiledStateGraph[StateT, ContextT, InputT, OutputT]:
"""Compiles the state graph into a `CompiledStateGraph` object.
"""Compiles the `StateGraph` into a `CompiledStateGraph` object.
The compiled graph implements the `Runnable` interface and can be invoked,
streamed, batched, and run asynchronously.
Args:
checkpointer: A checkpoint saver object or flag.
If provided, this `Checkpointer` serves as a fully versioned "short-term memory" for the graph,
allowing it to be paused, resumed, and replayed from any point.
If `None`, it may inherit the parent graph's checkpointer when used as a subgraph.
If `False`, it will not use or inherit any checkpointer.
interrupt_before: An optional list of node names to interrupt before.
interrupt_after: An optional list of node names to interrupt after.
@@ -825,7 +851,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
name: The name to use for the compiled graph.
Returns:
CompiledStateGraph: The compiled state graph.
CompiledStateGraph: The compiled `StateGraph`.
"""
# assign default values
interrupt_before = interrupt_before or []
+1 -2
View File
@@ -81,9 +81,8 @@ def push_ui_message(
metadata: Optional additional metadata about the UI message.
message: Optional message object to associate with the UI message.
state_key: Key in the graph state where the UI messages are stored.
Defaults to "ui".
merge: Whether to merge props with existing UI message (True) or replace
them (False). Defaults to False.
them (False).
Returns:
The created UI message.
+374 -249
View File
@@ -63,6 +63,7 @@ from langgraph._internal._scratchpad import PregelScratchpad
from langgraph._internal._typing import EMPTY_SEQ, MISSING
from langgraph.channels.base import BaseChannel
from langgraph.channels.topic import Topic
from langgraph.channels.untracked_value import UntrackedValue
from langgraph.constants import TAG_HIDDEN
from langgraph.managed.base import ManagedValueMapping
from langgraph.pregel._call import get_runnable_for_task, identifier
@@ -257,9 +258,11 @@ def apply_writes(
next_version = None
else:
next_version = get_next_version(
max(checkpoint["channel_versions"].values())
if checkpoint["channel_versions"]
else None,
(
max(checkpoint["channel_versions"].values())
if checkpoint["channel_versions"]
else None
),
None,
)
@@ -490,6 +493,11 @@ def prepare_next_tasks(
PUSH_TRIGGER = (PUSH,)
class _TaskIDFn(Protocol):
def __call__(self, namespace: bytes, *parts: str | bytes) -> str:
pass
def prepare_single_task(
task_path: tuple[Any, ...],
task_id_checksum: str | None,
@@ -519,249 +527,50 @@ def prepare_single_task(
task_id_func = _xxhash_str if checkpoint["v"] > 1 else _uuid5_str
if task_path[0] == PUSH and isinstance(task_path[-1], Call):
# (PUSH, parent task path, idx of PUSH write, id of parent task, Call)
task_path_t = cast(tuple[str, tuple, int, str, Call], task_path)
call = task_path_t[-1]
proc_ = get_runnable_for_task(call.func)
name = proc_.name
if name is None:
raise ValueError("`call` functions must have a `__name__` attribute")
# create task id
triggers: Sequence[str] = PUSH_TRIGGER
checkpoint_ns = f"{parent_ns}{NS_SEP}{name}" if parent_ns else name
task_id = task_id_func(
checkpoint_id_bytes,
checkpoint_ns,
str(step),
name,
PUSH,
task_path_str(task_path[1]),
str(task_path[2]),
return prepare_push_task_functional(
cast(tuple[str, tuple, int, str, Call], task_path),
task_id_checksum,
checkpoint=checkpoint,
checkpoint_id_bytes=checkpoint_id_bytes,
pending_writes=pending_writes,
channels=channels,
managed=managed,
config=config,
step=step,
stop=stop,
for_execution=for_execution,
store=store,
checkpointer=checkpointer,
manager=manager,
cache_policy=cache_policy,
retry_policy=retry_policy,
parent_ns=parent_ns,
task_id_func=task_id_func,
)
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
# we append True to the task path to indicate that a call is being
# made, so we should not return interrupts from this task (responsibility lies with the parent)
task_path = (*task_path[:3], True)
metadata = {
"langgraph_step": step,
"langgraph_node": name,
"langgraph_triggers": triggers,
"langgraph_path": task_path,
"langgraph_checkpoint_ns": task_checkpoint_ns,
}
if task_id_checksum is not None:
assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
if for_execution:
writes: deque[tuple[str, Any]] = deque()
cache_policy = call.cache_policy or cache_policy
if cache_policy:
args_key = cache_policy.key_func(*call.input[0], **call.input[1])
cache_key: CacheKey | None = CacheKey(
(
CACHE_NS_WRITES,
(identifier(call.func) or "__dynamic__"),
),
xxh3_128_hexdigest(
args_key.encode() if isinstance(args_key, str) else args_key,
),
cache_policy.ttl,
)
else:
cache_key = None
scratchpad = _scratchpad(
config[CONF].get(CONFIG_KEY_SCRATCHPAD),
pending_writes,
task_id,
xxh3_128_hexdigest(task_checkpoint_ns.encode()),
config[CONF].get(CONFIG_KEY_RESUME_MAP),
step,
stop,
)
runtime = cast(
Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME)
)
runtime = runtime.override(store=store)
return PregelExecutableTask(
name,
call.input,
proc_,
writes,
patch_config(
merge_configs(config, {"metadata": metadata}),
run_name=name,
callbacks=call.callbacks
or (manager.get_child(f"graph:step:{step}") if manager else None),
configurable={
CONFIG_KEY_TASK_ID: task_id,
# deque.extend is thread-safe
CONFIG_KEY_SEND: writes.extend,
CONFIG_KEY_READ: partial(
local_read,
scratchpad,
channels,
managed,
PregelTaskWrites(task_path, name, writes, triggers),
),
CONFIG_KEY_CHECKPOINTER: (
checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
),
CONFIG_KEY_CHECKPOINT_MAP: {
**configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
parent_ns: checkpoint["id"],
},
CONFIG_KEY_CHECKPOINT_ID: None,
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
CONFIG_KEY_SCRATCHPAD: scratchpad,
CONFIG_KEY_RUNTIME: runtime,
},
),
triggers,
call.retry_policy or retry_policy,
cache_key,
task_id,
task_path,
)
else:
return PregelTask(task_id, name, task_path)
elif task_path[0] == PUSH:
if len(task_path) == 2:
# SEND tasks, executed in superstep n+1
# (PUSH, idx of pending send)
idx = cast(int, task_path[1])
if not channels[TASKS].is_available():
return
sends: Sequence[Send] = channels[TASKS].get()
if idx < 0 or idx >= len(sends):
return
packet = sends[idx]
if not isinstance(packet, Send):
logger.warning(
f"Ignoring invalid packet type {type(packet)} in pending sends"
)
return
if packet.node not in processes:
logger.warning(
f"Ignoring unknown node name {packet.node} in pending sends"
)
return
# find process
proc = processes[packet.node]
proc_node = proc.node
if proc_node is None:
return
# create task id
triggers = PUSH_TRIGGER
checkpoint_ns = (
f"{parent_ns}{NS_SEP}{packet.node}" if parent_ns else packet.node
)
task_id = task_id_func(
checkpoint_id_bytes,
checkpoint_ns,
str(step),
packet.node,
PUSH,
str(idx),
)
else:
logger.warning(f"Ignoring invalid PUSH task path {task_path}")
return
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
# we append False to the task path to indicate that a call is not being made
# so we should return interrupts from this task
task_path = (*task_path[:3], False)
metadata = {
"langgraph_step": step,
"langgraph_node": packet.node,
"langgraph_triggers": triggers,
"langgraph_path": task_path,
"langgraph_checkpoint_ns": task_checkpoint_ns,
}
if task_id_checksum is not None:
assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
if for_execution:
if proc.metadata:
metadata.update(proc.metadata)
writes = deque()
cache_policy = proc.cache_policy or cache_policy
if cache_policy:
args_key = cache_policy.key_func(packet.arg)
cache_key = CacheKey(
(
CACHE_NS_WRITES,
(identifier(proc) or "__dynamic__"),
packet.node,
),
xxh3_128_hexdigest(
args_key.encode() if isinstance(args_key, str) else args_key,
),
cache_policy.ttl,
)
else:
cache_key = None
scratchpad = _scratchpad(
config[CONF].get(CONFIG_KEY_SCRATCHPAD),
pending_writes,
task_id,
xxh3_128_hexdigest(task_checkpoint_ns.encode()),
config[CONF].get(CONFIG_KEY_RESUME_MAP),
step,
stop,
)
runtime = cast(
Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME)
)
runtime = runtime.override(
store=store, previous=checkpoint["channel_values"].get(PREVIOUS, None)
)
additional_config: RunnableConfig = {
"metadata": metadata,
"tags": proc.tags,
}
return PregelExecutableTask(
packet.node,
packet.arg,
proc_node,
writes,
patch_config(
merge_configs(config, additional_config),
run_name=packet.node,
callbacks=(
manager.get_child(f"graph:step:{step}") if manager else None
),
configurable={
CONFIG_KEY_TASK_ID: task_id,
# deque.extend is thread-safe
CONFIG_KEY_SEND: writes.extend,
CONFIG_KEY_READ: partial(
local_read,
scratchpad,
channels,
managed,
PregelTaskWrites(task_path, packet.node, writes, triggers),
),
CONFIG_KEY_CHECKPOINTER: (
checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
),
CONFIG_KEY_CHECKPOINT_MAP: {
**configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
parent_ns: checkpoint["id"],
},
CONFIG_KEY_CHECKPOINT_ID: None,
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
CONFIG_KEY_SCRATCHPAD: scratchpad,
CONFIG_KEY_RUNTIME: runtime,
},
),
triggers,
proc.retry_policy or retry_policy,
cache_key,
task_id,
task_path,
writers=proc.flat_writers,
subgraphs=proc.subgraphs,
)
else:
return PregelTask(task_id, packet.node, task_path)
return prepare_push_task_send(
cast(tuple[str, tuple], task_path),
task_id_checksum,
checkpoint=checkpoint,
checkpoint_id_bytes=checkpoint_id_bytes,
pending_writes=pending_writes,
channels=channels,
managed=managed,
config=config,
step=step,
processes=processes,
stop=stop,
for_execution=for_execution,
store=store,
checkpointer=checkpointer,
manager=manager,
cache_policy=cache_policy,
retry_policy=retry_policy,
parent_ns=parent_ns,
task_id_func=task_id_func,
)
elif task_path[0] == PULL:
# (PULL, node name)
name = cast(str, task_path[1])
@@ -832,7 +641,7 @@ def prepare_single_task(
if node := proc.node:
if proc.metadata:
metadata.update(proc.metadata)
writes = deque()
writes: deque[tuple[str, Any]] = deque()
cache_policy = proc.cache_policy or cache_policy
if cache_policy:
args_key = cache_policy.key_func(val)
@@ -843,9 +652,11 @@ def prepare_single_task(
name,
),
xxh3_128_hexdigest(
args_key.encode()
if isinstance(args_key, str)
else args_key,
(
args_key.encode()
if isinstance(args_key, str)
else args_key
),
),
cache_policy.ttl,
)
@@ -868,7 +679,9 @@ def prepare_single_task(
node,
writes,
patch_config(
merge_configs(config, additional_config),
merge_configs(
config, cast(RunnableConfig, additional_config)
),
run_name=name,
callbacks=(
manager.get_child(f"graph:step:{step}")
@@ -917,6 +730,297 @@ def prepare_single_task(
return PregelTask(task_id, name, task_path[:3])
def prepare_push_task_functional(
task_path: tuple[str, tuple, int, str, Call],
# (PUSH, parent task path, idx of PUSH write, id of parent task, Call)
task_id_checksum: str | None,
*,
checkpoint: Checkpoint,
checkpoint_id_bytes: bytes,
pending_writes: list[PendingWrite],
channels: Mapping[str, BaseChannel],
managed: ManagedValueMapping,
config: RunnableConfig,
step: int,
stop: int,
for_execution: bool,
store: BaseStore | None = None,
checkpointer: BaseCheckpointSaver | None = None,
manager: None | ParentRunManager | AsyncParentRunManager = None,
cache_policy: CachePolicy | None = None,
retry_policy: Sequence[RetryPolicy] = (),
parent_ns: str,
# namespace: bytes, *parts: str | bytes
task_id_func: _TaskIDFn,
) -> PregelTask | PregelExecutableTask:
"""Prepare a push task with an attached caller. Used for the functional API."""
configurable = config.get(CONF, {})
call = task_path[-1]
proc_ = get_runnable_for_task(call.func)
name = proc_.name
if name is None:
raise ValueError("`call` functions must have a `__name__` attribute")
# create task id
triggers: Sequence[str] = PUSH_TRIGGER
checkpoint_ns = f"{parent_ns}{NS_SEP}{name}" if parent_ns else name
task_id = task_id_func(
checkpoint_id_bytes,
checkpoint_ns,
str(step),
name,
PUSH,
task_path_str(task_path[1]),
str(task_path[2]),
)
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
# we append True to the task path to indicate that a call is being
# made, so we should not return interrupts from this task (responsibility lies with the parent)
in_progress_task_path = (*task_path[:3], True)
metadata = {
"langgraph_step": step,
"langgraph_node": name,
"langgraph_triggers": triggers,
"langgraph_path": in_progress_task_path,
"langgraph_checkpoint_ns": task_checkpoint_ns,
}
if task_id_checksum is not None:
assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
if for_execution:
writes: deque[tuple[str, Any]] = deque()
cache_policy = call.cache_policy or cache_policy
if cache_policy:
args_key = cache_policy.key_func(*call.input[0], **call.input[1])
cache_key: CacheKey | None = CacheKey(
(
CACHE_NS_WRITES,
(identifier(call.func) or "__dynamic__"),
),
xxh3_128_hexdigest(
args_key.encode() if isinstance(args_key, str) else args_key,
),
cache_policy.ttl,
)
else:
cache_key = None
scratchpad = _scratchpad(
configurable.get(CONFIG_KEY_SCRATCHPAD),
pending_writes,
task_id,
xxh3_128_hexdigest(task_checkpoint_ns.encode()),
configurable.get(CONFIG_KEY_RESUME_MAP),
step,
stop,
)
runtime = cast(Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME))
runtime = runtime.override(store=store)
return PregelExecutableTask(
name,
call.input,
proc_,
writes,
patch_config(
merge_configs(config, {"metadata": metadata}),
run_name=name,
callbacks=call.callbacks
or (manager.get_child(f"graph:step:{step}") if manager else None),
configurable={
CONFIG_KEY_TASK_ID: task_id,
# deque.extend is thread-safe
CONFIG_KEY_SEND: writes.extend,
CONFIG_KEY_READ: partial(
local_read,
scratchpad,
channels,
managed,
PregelTaskWrites(in_progress_task_path, name, writes, triggers),
),
CONFIG_KEY_CHECKPOINTER: (
checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
),
CONFIG_KEY_CHECKPOINT_MAP: {
**configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
parent_ns: checkpoint["id"],
},
CONFIG_KEY_CHECKPOINT_ID: None,
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
CONFIG_KEY_SCRATCHPAD: scratchpad,
CONFIG_KEY_RUNTIME: runtime,
},
),
triggers,
call.retry_policy or retry_policy,
cache_key,
task_id,
in_progress_task_path,
)
else:
return PregelTask(task_id, name, in_progress_task_path)
def prepare_push_task_send(
task_path: tuple[str, tuple],
# (PUSH, parent task path)
task_id_checksum: str | None,
*,
checkpoint: Checkpoint,
checkpoint_id_bytes: bytes,
pending_writes: list[PendingWrite],
channels: Mapping[str, BaseChannel],
managed: ManagedValueMapping,
config: RunnableConfig,
step: int,
stop: int,
for_execution: bool,
store: BaseStore | None = None,
checkpointer: BaseCheckpointSaver | None = None,
manager: None | ParentRunManager | AsyncParentRunManager = None,
cache_policy: CachePolicy | None = None,
retry_policy: Sequence[RetryPolicy] = (),
parent_ns: str,
task_id_func: _TaskIDFn,
processes: Mapping[str, PregelNode],
) -> PregelTask | PregelExecutableTask | None:
if len(task_path) == 2:
# SEND tasks, executed in superstep n+1
# (PUSH, idx of pending send)
idx = cast(int, task_path[1])
if not channels[TASKS].is_available():
return
sends: Sequence[Send] = channels[TASKS].get()
if idx < 0 or idx >= len(sends):
return
packet = sends[idx]
if not isinstance(packet, Send):
logger.warning(
f"Ignoring invalid packet type {type(packet)} in pending sends"
)
return
if packet.node not in processes:
logger.warning(f"Ignoring unknown node name {packet.node} in pending sends")
return
# find process
proc = processes[packet.node]
proc_node = proc.node
if proc_node is None:
return
# create task id
triggers = PUSH_TRIGGER
checkpoint_ns = (
f"{parent_ns}{NS_SEP}{packet.node}" if parent_ns else packet.node
)
task_id = task_id_func(
checkpoint_id_bytes,
checkpoint_ns,
str(step),
packet.node,
PUSH,
str(idx),
)
else:
logger.warning(f"Ignoring invalid PUSH task path {task_path}")
return
configurable = config.get(CONF, {})
task_checkpoint_ns = f"{checkpoint_ns}:{task_id}"
# we append False to the task path to indicate that a call is not being made
# so we should return interrupts from this task
translated_task_path = (*task_path[:3], False)
metadata = {
"langgraph_step": step,
"langgraph_node": packet.node,
"langgraph_triggers": triggers,
"langgraph_path": translated_task_path,
"langgraph_checkpoint_ns": task_checkpoint_ns,
}
if task_id_checksum is not None:
assert task_id == task_id_checksum, f"{task_id} != {task_id_checksum}"
if for_execution:
if proc.metadata:
metadata.update(proc.metadata)
writes: deque[tuple[str, Any]] = deque()
cache_policy = proc.cache_policy or cache_policy
if cache_policy:
args_key = cache_policy.key_func(packet.arg)
cache_key = CacheKey(
(
CACHE_NS_WRITES,
(identifier(proc) or "__dynamic__"),
packet.node,
),
xxh3_128_hexdigest(
args_key.encode() if isinstance(args_key, str) else args_key,
),
cache_policy.ttl,
)
else:
cache_key = None
scratchpad = _scratchpad(
config[CONF].get(CONFIG_KEY_SCRATCHPAD),
pending_writes,
task_id,
xxh3_128_hexdigest(task_checkpoint_ns.encode()),
config[CONF].get(CONFIG_KEY_RESUME_MAP),
step,
stop,
)
runtime = cast(Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME))
runtime = runtime.override(
store=store, previous=checkpoint["channel_values"].get(PREVIOUS, None)
)
additional_config: RunnableConfig = {
"metadata": metadata,
"tags": proc.tags,
}
return PregelExecutableTask(
packet.node,
packet.arg,
proc_node,
writes,
patch_config(
merge_configs(config, additional_config),
run_name=packet.node,
callbacks=(
manager.get_child(f"graph:step:{step}") if manager else None
),
configurable={
CONFIG_KEY_TASK_ID: task_id,
# deque.extend is thread-safe
CONFIG_KEY_SEND: writes.extend,
CONFIG_KEY_READ: partial(
local_read,
scratchpad,
channels,
managed,
PregelTaskWrites(
translated_task_path, packet.node, writes, triggers
),
),
CONFIG_KEY_CHECKPOINTER: (
checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER)
),
CONFIG_KEY_CHECKPOINT_MAP: {
**configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}),
parent_ns: checkpoint["id"],
},
CONFIG_KEY_CHECKPOINT_ID: None,
CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns,
CONFIG_KEY_SCRATCHPAD: scratchpad,
CONFIG_KEY_RUNTIME: runtime,
},
),
triggers,
proc.retry_policy or retry_policy,
cache_key,
task_id,
translated_task_path,
writers=proc.flat_writers,
subgraphs=proc.subgraphs,
)
else:
return PregelTask(task_id, packet.node, translated_task_path)
def checkpoint_null_version(
checkpoint: Checkpoint,
) -> V | None:
@@ -1106,3 +1210,24 @@ class LazyAtomicCounter:
if self._counter is None:
self._counter = itertools.count(0).__next__
return self._counter()
def sanitize_untracked_values_in_send(
packet: Send, channels: Mapping[str, BaseChannel]
) -> Send:
"""Pop any values belonging to UntrackedValue channels in Send.arg for safe checkpointing.
Send is often called with state to be passed to the dest node, which may contain
UntrackedValues at the top level. Send is not typed and arg may be a nested dict."""
if not isinstance(packet.arg, dict):
# Command
return packet
# top level keys should be the channel names
sanitized_arg = {
k: v
for k, v in packet.arg.items()
if not isinstance(channels.get(k), UntrackedValue)
}
return Send(node=packet.node, arg=sanitized_arg)
+43 -5
View File
@@ -56,10 +56,12 @@ from langgraph._internal._constants import (
NULL_TASK_ID,
PUSH,
RESUME,
TASKS,
)
from langgraph._internal._scratchpad import PregelScratchpad
from langgraph._internal._typing import EMPTY_SEQ, MISSING
from langgraph.channels.base import BaseChannel
from langgraph.channels.untracked_value import UntrackedValue
from langgraph.constants import TAG_HIDDEN
from langgraph.errors import (
EmptyInputError,
@@ -78,6 +80,7 @@ from langgraph.pregel._algo import (
increment,
prepare_next_tasks,
prepare_single_task,
sanitize_untracked_values_in_send,
should_interrupt,
task_path_str,
)
@@ -114,6 +117,7 @@ from langgraph.types import (
Durability,
PregelExecutableTask,
RetryPolicy,
Send,
StreamMode,
)
@@ -320,6 +324,24 @@ class PregelLoop:
w for w in self.checkpoint_pending_writes if w[0] != task_id
]
writes_to_save = writes
# check if any writes are to an UntrackedValue channel
if any(
isinstance(channel, UntrackedValue) for channel in self.channels.values()
):
# we do not persist untracked values in checkpoints
writes_to_save = [
# sanitize UntrackedValues that are nested within Send packets
(
(c, sanitize_untracked_values_in_send(v, self.channels))
if c == TASKS and isinstance(v, Send)
else (c, v)
)
for c, v in writes_to_save
# dont persist UntrackedValue channel writes
if not isinstance(self.specs.get(c), UntrackedValue)
]
# save writes
self.checkpoint_pending_writes.extend((task_id, c, v) for c, v in writes)
if self.durability != "exit" and self.checkpointer_put_writes is not None:
@@ -437,9 +459,6 @@ class PregelLoop:
def tick(self) -> bool:
"""Execute a single iteration of the Pregel loop.
Args:
input_keys: The key(s) to read input from.
Returns:
True if more iterations are needed.
"""
@@ -735,7 +754,19 @@ class PregelLoop:
id=self.checkpoint["id"] if exiting else None,
updated_channels=self.updated_channels,
)
# sanitize TASK channel in the checkpoint before saving (durability=="exit")
if TASKS in self.checkpoint["channel_values"] and any(
isinstance(channel, UntrackedValue) for channel in self.channels.values()
):
sanitized_tasks = [
sanitize_untracked_values_in_send(value, self.channels)
if isinstance(value, Send)
else value
for value in self.checkpoint["channel_values"][TASKS]
]
self.checkpoint["channel_values"][TASKS] = sanitized_tasks
# bail if no checkpointer
if do_checkpoint and self._checkpointer_put_after_previous is not None:
self.prev_checkpoint_config = (
self.checkpoint_config
@@ -905,8 +936,15 @@ class PregelLoop:
stream_modes = self.stream.modes if self.stream else []
if "updates" in stream_modes:
self._emit("updates", lambda: iter(interrupts))
elif "values" in stream_modes:
self._emit("values", lambda: iter(interrupts))
if "values" in stream_modes:
current_values = read_channels(self.channels, self.output_keys)
# self.output_keys is a sequence, stream chunk contains entire state and interrupts
if isinstance(current_values, dict):
current_values[INTERRUPT] = interrupts[0][INTERRUPT]
self._emit("values", lambda: iter([current_values]))
# self.output_keys is a string, stream chunk contains only interrupts
else:
self._emit("values", lambda: iter(interrupts))
elif writes[0][0] != ERROR:
self._emit(
"updates",
+7 -2
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio
import concurrent.futures
import inspect
import threading
import time
import weakref
@@ -281,7 +282,11 @@ class PregelRunner:
Awaitable[PregelExecutableTask | None],
],
) -> AsyncIterator[None]:
loop = asyncio.get_event_loop()
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
tasks = tuple(tasks)
futures = FuturesDict(
callback=weakref.WeakMethod(self.commit),
@@ -539,7 +544,7 @@ def _call(
],
submit: weakref.ref[Submit],
) -> concurrent.futures.Future[Any]:
if asyncio.iscoroutinefunction(func):
if inspect.iscoroutinefunction(func):
raise RuntimeError("In an sync context async tasks cannot be called")
fut: concurrent.futures.Future | None = None
+81 -97
View File
@@ -210,8 +210,10 @@ class NodeBuilder:
*channels: str,
read: bool = True,
) -> Self:
"""Add channels to subscribe to. Node will be invoked when any of these
channels are updated, with a dict of the channel values as input.
"""Add channels to subscribe to.
Node will be invoked when any of these channels are updated, with a dict of the
channel values as input.
Args:
channels: Channel name(s) to subscribe to
@@ -270,8 +272,8 @@ class NodeBuilder:
"""Add channel writes.
Args:
*channels: Channel names to write to
**kwargs: Channel name and value mappings
*channels: Channel names to write to.
**kwargs: Channel name and value mappings.
Returns:
Self for chaining
@@ -375,12 +377,12 @@ class Pregel(
### Advanced channels: Context and BinaryOperatorAggregate
- `Context`: exposes the value of a context manager, managing its lifecycle.
Useful for accessing external resources that require setup and/or teardown. eg.
`client = Context(httpx.Client)`
Useful for accessing external resources that require setup and/or teardown. e.g.
`client = Context(httpx.Client)`
- `BinaryOperatorAggregate`: stores a persistent value, updated by applying
a binary operator to the current value and each update
sent to the channel, useful for computing aggregates over multiple steps. eg.
`total = BinaryOperatorAggregate(int, operator.add)`
a binary operator to the current value and each update
sent to the channel, useful for computing aggregates over multiple steps. e.g.
`total = BinaryOperatorAggregate(int, operator.add)`
## Examples
@@ -495,7 +497,7 @@ class Pregel(
{"c": ["foofoo", "foofoofoofoo"]}
```
Example: Using a BinaryOperatorAggregate channel
Example: Using a `BinaryOperatorAggregate` channel
```python
from langgraph.channels import EphemeralValue, BinaryOperatorAggregate
from langgraph.pregel import Pregel, NodeBuilder
@@ -541,8 +543,9 @@ class Pregel(
Example: Introducing a cycle
This example demonstrates how to introduce a cycle in the graph, by having
a chain write to a channel it subscribes to. Execution will continue
until a None value is written to the channel.
a chain write to a channel it subscribes to.
Execution will continue until a `None` value is written to the channel.
```python
from langgraph.channels import EphemeralValue
@@ -934,7 +937,7 @@ class Pregel(
If `False`, only the immediate subgraphs will be returned.
Returns:
An iterator of the (namespace, subgraph) pairs.
An iterator of the `(namespace, subgraph)` pairs.
"""
for name, node in self.nodes.items():
# filter by prefix
@@ -973,7 +976,7 @@ class Pregel(
If `False`, only the immediate subgraphs will be returned.
Returns:
An iterator of the (namespace, subgraph) pairs.
An iterator of the `(namespace, subgraph)` pairs.
"""
for name, node in self.get_subgraphs(namespace=namespace, recurse=recurse):
yield name, node
@@ -1426,7 +1429,8 @@ class Pregel(
Args:
config: The config to apply the updates to.
supersteps: A list of supersteps, each including a list of updates to apply sequentially to a graph state.
Each update is a tuple of the form `(values, as_node, task_id)` where `task_id` is optional.
Each update is a tuple of the form `(values, as_node, task_id)` where `task_id` is optional.
Raises:
ValueError: If no checkpointer is set or no updates are provided.
@@ -1702,7 +1706,9 @@ class Pregel(
# we use the task id generated by prepare_next_tasks
node_to_task_ids: dict[str, deque[str]] = defaultdict(deque)
if saved is not None and saved.pending_writes is not None:
# tasks for this checkpoint
# we call prepare_next_tasks to discover the task IDs that
# would have been generated, so we can reuse them and
# properly populate task.result in state history
next_tasks = prepare_next_tasks(
checkpoint,
saved.pending_writes,
@@ -1721,32 +1727,6 @@ class Pregel(
for t in next_tasks.values():
node_to_task_ids[t.name].append(t.id)
# apply null writes
if null_writes := [
w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID
]:
apply_writes(
checkpoint,
channels,
[PregelTaskWrites((), INPUT, null_writes, [])],
checkpointer.get_next_version,
self.trigger_to_nodes,
)
# apply writes
for tid, k, v in saved.pending_writes:
if k in (ERROR, INTERRUPT):
continue
if tid not in next_tasks:
continue
next_tasks[tid].writes.append((k, v))
if tasks := [t for t in next_tasks.values() if t.writes]:
apply_writes(
checkpoint,
channels,
tasks,
checkpointer.get_next_version,
self.trigger_to_nodes,
)
valid_updates: list[tuple[str, dict[str, Any] | None, str | None]] = []
if len(updates) == 1:
values, as_node, task_id = updates[0]
@@ -1893,7 +1873,8 @@ class Pregel(
Args:
config: The config to apply the updates to.
supersteps: A list of supersteps, each including a list of updates to apply sequentially to a graph state.
Each update is a tuple of the form `(values, as_node, task_id)` where `task_id` is optional.
Each update is a tuple of the form `(values, as_node, task_id)` where `task_id` is optional.
Raises:
ValueError: If no checkpointer is set or no updates are provided.
@@ -2167,7 +2148,9 @@ class Pregel(
# we use the task id generated by prepare_next_tasks
node_to_task_ids: dict[str, deque[str]] = defaultdict(deque)
if saved is not None and saved.pending_writes is not None:
# tasks for this checkpoint
# we call prepare_next_tasks to discover the task IDs that
# would have been generated, so we can reuse them and
# properly populate task.result in state history
next_tasks = prepare_next_tasks(
checkpoint,
saved.pending_writes,
@@ -2186,31 +2169,6 @@ class Pregel(
for t in next_tasks.values():
node_to_task_ids[t.name].append(t.id)
# apply null writes
if null_writes := [
w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID
]:
apply_writes(
checkpoint,
channels,
[PregelTaskWrites((), INPUT, null_writes, [])],
checkpointer.get_next_version,
self.trigger_to_nodes,
)
for tid, k, v in saved.pending_writes:
if k in (ERROR, INTERRUPT):
continue
if tid not in next_tasks:
continue
next_tasks[tid].writes.append((k, v))
if tasks := [t for t in next_tasks.values() if t.writes]:
apply_writes(
checkpoint,
channels,
tasks,
checkpointer.get_next_version,
self.trigger_to_nodes,
)
valid_updates: list[tuple[str, dict[str, Any] | None, str | None]] = []
if len(updates) == 1:
values, as_node, task_id = updates[0]
@@ -2401,7 +2359,7 @@ class Pregel(
validate_keys(output_keys, self.channels)
interrupt_before = interrupt_before or self.interrupt_before_nodes
interrupt_after = interrupt_after or self.interrupt_after_nodes
if not isinstance(stream_mode, list):
if isinstance(stream_mode, str):
stream_modes = {stream_mode}
else:
stream_modes = set(stream_mode)
@@ -2467,6 +2425,7 @@ class Pregel(
context: The static context to use for the run.
!!! version-added "Added in version 0.6.0"
stream_mode: The mode to stream output, defaults to `self.stream_mode`.
Options are:
- `"values"`: Emit all values in the state after each step, including interrupts.
@@ -2475,29 +2434,36 @@ class Pregel(
If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are emitted separately.
- `"custom"`: Emit custom data from inside nodes or tasks using `StreamWriter`.
- `"messages"`: Emit LLM messages token-by-token together with metadata for any LLM invocations inside nodes or tasks.
Will be emitted as 2-tuples `(LLM token, metadata)`.
- Will be emitted as 2-tuples `(LLM token, metadata)`.
- `"checkpoints"`: Emit an event when a checkpoint is created, in the same format as returned by `get_state()`.
- `"tasks"`: Emit events when tasks start and finish, including their results and errors.
- `"debug"`: Emit debug events with as much information as possible for each step.
You can pass a list as the `stream_mode` parameter to stream multiple modes at once.
The streamed outputs will be tuples of `(mode, data)`.
See [LangGraph streaming guide](https://langchain-ai.github.io/langgraph/how-tos/streaming/) for more details.
print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes. Does not affect the output of the graph in any way.
See [LangGraph streaming guide](https://docs.langchain.com/oss/python/langgraph/streaming) for more details.
print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes.
Does not affect the output of the graph in any way.
output_keys: The keys to stream, defaults to all non-context channels.
interrupt_before: Nodes to interrupt before, defaults to all nodes in the graph.
interrupt_after: Nodes to interrupt after, defaults to all nodes in the graph.
durability: The durability mode for the graph execution, defaults to `"async"`. Options are:
durability: The durability mode for the graph execution, defaults to `"async"`.
Options are:
- `"sync"`: Changes are persisted synchronously before the next step starts.
- `"async"`: Changes are persisted asynchronously while the next step executes.
- `"exit"`: Changes are persisted only when the graph exits.
subgraphs: Whether to stream events from inside subgraphs, defaults to False.
subgraphs: Whether to stream events from inside subgraphs, defaults to `False`.
If `True`, the events will be emitted as tuples `(namespace, data)`,
or `(namespace, mode, data)` if `stream_mode` is a list,
where `namespace` is a tuple with the path to the node where a subgraph is invoked,
e.g. `("parent_node:<task_id>", "child_node:<task_id>")`.
See [LangGraph streaming guide](https://langchain-ai.github.io/langgraph/how-tos/streaming/) for more details.
See [LangGraph streaming guide](https://docs.langchain.com/oss/python/langgraph/streaming) for more details.
Yields:
The output of each step in the graph. The output shape depends on the `stream_mode`.
@@ -2733,6 +2699,7 @@ class Pregel(
context: The static context to use for the run.
!!! version-added "Added in version 0.6.0"
stream_mode: The mode to stream output, defaults to `self.stream_mode`.
Options are:
- `"values"`: Emit all values in the state after each step, including interrupts.
@@ -2741,28 +2708,36 @@ class Pregel(
If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are emitted separately.
- `"custom"`: Emit custom data from inside nodes or tasks using `StreamWriter`.
- `"messages"`: Emit LLM messages token-by-token together with metadata for any LLM invocations inside nodes or tasks.
Will be emitted as 2-tuples `(LLM token, metadata)`.
- Will be emitted as 2-tuples `(LLM token, metadata)`.
- `"checkpoints"`: Emit an event when a checkpoint is created, in the same format as returned by `get_state()`.
- `"tasks"`: Emit events when tasks start and finish, including their results and errors.
- `"debug"`: Emit debug events with as much information as possible for each step.
You can pass a list as the `stream_mode` parameter to stream multiple modes at once.
The streamed outputs will be tuples of `(mode, data)`.
See [LangGraph streaming guide](https://langchain-ai.github.io/langgraph/how-tos/streaming/) for more details.
print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes. Does not affect the output of the graph in any way.
See [LangGraph streaming guide](https://docs.langchain.com/oss/python/langgraph/streaming) for more details.
print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes.
Does not affect the output of the graph in any way.
output_keys: The keys to stream, defaults to all non-context channels.
interrupt_before: Nodes to interrupt before, defaults to all nodes in the graph.
interrupt_after: Nodes to interrupt after, defaults to all nodes in the graph.
durability: The durability mode for the graph execution, defaults to `"async"`. Options are:
durability: The durability mode for the graph execution, defaults to `"async"`.
Options are:
- `"sync"`: Changes are persisted synchronously before the next step starts.
- `"async"`: Changes are persisted asynchronously while the next step executes.
- `"exit"`: Changes are persisted only when the graph exits.
subgraphs: Whether to stream events from inside subgraphs, defaults to False.
subgraphs: Whether to stream events from inside subgraphs, defaults to `False`.
If `True`, the events will be emitted as tuples `(namespace, data)`,
or `(namespace, mode, data)` if `stream_mode` is a list,
where `namespace` is a tuple with the path to the node where a subgraph is invoked,
e.g. `("parent_node:<task_id>", "child_node:<task_id>")`.
See [LangGraph streaming guide](https://langchain-ai.github.io/langgraph/how-tos/streaming/) for more details.
See [LangGraph streaming guide](https://docs.langchain.com/oss/python/langgraph/streaming) for more details.
Yields:
The output of each step in the graph. The output shape depends on the `stream_mode`.
@@ -3065,11 +3040,16 @@ class Pregel(
context: The static context to use for the run.
!!! version-added "Added in version 0.6.0"
stream_mode: The stream mode for the graph run.
print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes. Does not affect the output of the graph in any way.
print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes.
Does not affect the output of the graph in any way.
output_keys: The output keys to retrieve from the graph run.
interrupt_before: The nodes to interrupt the graph run before.
interrupt_after: The nodes to interrupt the graph run after.
durability: The durability mode for the graph execution, defaults to `"async"`. Options are:
durability: The durability mode for the graph execution, defaults to `"async"`.
Options are:
- `"sync"`: Changes are persisted synchronously before the next step starts.
- `"async"`: Changes are persisted asynchronously while the next step executes.
- `"exit"`: Changes are persisted only when the graph exits.
@@ -3142,29 +3122,33 @@ class Pregel(
durability: Durability | None = None,
**kwargs: Any,
) -> dict[str, Any] | Any:
"""Asynchronously invoke the graph on a single input.
"""Asynchronously run the graph with a single input and config.
Args:
input: The input data for the computation. It can be a dictionary or any other type.
config: The configuration for the computation.
input: The input data for the graph. It can be a dictionary or any other type.
config: The configuration for the graph run.
context: The static context to use for the run.
!!! version-added "Added in version 0.6.0"
stream_mode: The stream mode for the computation.
print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes. Does not affect the output of the graph in any way.
output_keys: The output keys to include in the result.
interrupt_before: The nodes to interrupt before.
interrupt_after: The nodes to interrupt after.
durability: The durability mode for the graph execution, defaults to `"async"`. Options are:
stream_mode: The stream mode for the graph run.
print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes.
Does not affect the output of the graph in any way.
output_keys: The output keys to retrieve from the graph run.
interrupt_before: The nodes to interrupt the graph run before.
interrupt_after: The nodes to interrupt the graph run after.
durability: The durability mode for the graph execution, defaults to `"async"`.
Options are:
- `"sync"`: Changes are persisted synchronously before the next step starts.
- `"async"`: Changes are persisted asynchronously while the next step executes.
- `"exit"`: Changes are persisted only when the graph exits.
**kwargs: Additional keyword arguments.
**kwargs: Additional keyword arguments to pass to the graph run.
Returns:
The result of the computation. If `stream_mode` is `"values"`, it returns the latest value.
If `stream_mode` is `"chunks"`, it returns a list of chunks.
The output of the graph run. If `stream_mode` is `"values"`, it returns the latest output.
If `stream_mode` is not `"values"`, it returns a list of output chunks.
"""
output_keys = output_keys if output_keys is not None else self.output_channels
latest: dict[str, Any] | Any = None
+4 -2
View File
@@ -725,9 +725,10 @@ class RemoteGraph(PregelProtocol):
input = None
else:
command = None
thread_id = sanitized_config.get("configurable", {}).pop("thread_id", None)
for chunk in sync_client.runs.stream(
thread_id=sanitized_config["configurable"].get("thread_id"),
thread_id=thread_id,
assistant_id=self.assistant_id,
input=input,
command=command,
@@ -834,9 +835,10 @@ class RemoteGraph(PregelProtocol):
input = None
else:
command = None
thread_id = sanitized_config.get("configurable", {}).pop("thread_id", None)
async for chunk in client.runs.stream(
thread_id=sanitized_config["configurable"].get("thread_id"),
thread_id=thread_id,
assistant_id=self.assistant_id,
input=input,
command=command,
+1 -1
View File
@@ -111,7 +111,7 @@ class Runtime(Generic[ContextT]):
stream_writer=other.stream_writer
if other.stream_writer is not _no_op_stream_writer
else self.stream_writer,
previous=other.previous or self.previous,
previous=self.previous if other.previous is None else other.previous,
)
def override(
+78 -28
View File
@@ -55,6 +55,7 @@ __all__ = (
"Command",
"Durability",
"interrupt",
"Overwrite",
)
Durability = Literal["sync", "async", "exit"]
@@ -85,11 +86,11 @@ StreamMode = Literal[
- `"messages"`: Emit LLM messages token-by-token together with metadata for any LLM invocations inside nodes or tasks.
- `"checkpoints"`: Emit an event when a checkpoint is created, in the same format as returned by `get_state()`.
- `"tasks"`: Emit events when tasks start and finish, including their results and errors.
- `"debug"`: Emit "checkpoints" and "tasks" events, for debugging purposes.
- `"debug"`: Emit `"checkpoints"` and `"tasks"` events for debugging purposes.
"""
StreamWriter = Callable[[Any], None]
"""Callable that accepts a single argument and writes it to the output stream.
"""`Callable` that accepts a single argument and writes it to the output stream.
Always injected into nodes if requested as a keyword argument, but it's a no-op
when not using `stream_mode="custom"`."""
@@ -115,7 +116,7 @@ class RetryPolicy(NamedTuple):
retry_on: (
type[Exception] | Sequence[type[Exception]] | Callable[[Exception], bool]
) = default_retry_on
"""List of exception classes that should trigger a retry, or a callable that returns True for exceptions that should trigger a retry."""
"""List of exception classes that should trigger a retry, or a callable that returns `True` for exceptions that should trigger a retry."""
KeyFuncT = TypeVar("KeyFuncT", bound=Callable[..., str | bytes])
@@ -283,26 +284,32 @@ class Send:
node (str): The name of the target node to send the message to.
arg (Any): The state or message to send to the target node.
Examples:
>>> from typing import Annotated
>>> import operator
>>> class OverallState(TypedDict):
... subjects: list[str]
... jokes: Annotated[list[str], operator.add]
>>> from langgraph.types import Send
>>> from langgraph.graph import END, START
>>> def continue_to_jokes(state: OverallState):
... return [Send("generate_joke", {"subject": s}) for s in state["subjects"]]
>>> from langgraph.graph import StateGraph
>>> builder = StateGraph(OverallState)
>>> builder.add_node("generate_joke", lambda state: {"jokes": [f"Joke about {state['subject']}"]})
>>> builder.add_conditional_edges(START, continue_to_jokes)
>>> builder.add_edge("generate_joke", END)
>>> graph = builder.compile()
>>>
>>> # Invoking with two subjects results in a generated joke for each
>>> graph.invoke({"subjects": ["cats", "dogs"]})
{'subjects': ['cats', 'dogs'], 'jokes': ['Joke about cats', 'Joke about dogs']}
!!! example
```python
from typing import Annotated
from langgraph.types import Send
from langgraph.graph import END, START
from langgraph.graph import StateGraph
import operator
class OverallState(TypedDict):
subjects: list[str]
jokes: Annotated[list[str], operator.add]
def continue_to_jokes(state: OverallState):
return [Send("generate_joke", {"subject": s}) for s in state["subjects"]]
builder = StateGraph(OverallState)
builder.add_node("generate_joke", lambda state: {"jokes": [f"Joke about {state['subject']}"]})
builder.add_conditional_edges(START, continue_to_jokes)
builder.add_edge("generate_joke", END)
graph = builder.compile()
# Invoking with two subjects results in a generated joke for each
graph.invoke({"subjects": ["cats", "dogs"]})
# {'subjects': ['cats', 'dogs'], 'jokes': ['Joke about cats', 'Joke about dogs']}
```
"""
__slots__ = ("node", "arg")
@@ -312,7 +319,7 @@ class Send:
def __init__(self, /, node: str, arg: Any) -> None:
"""
Initialize a new instance of the Send class.
Initialize a new instance of the `Send` class.
Args:
node: The name of the target node to send the message to.
@@ -342,10 +349,8 @@ N = TypeVar("N", bound=Hashable)
class Command(Generic[N], ToolOutputMixin):
"""One or more commands to update the graph's state and send messages to nodes.
!!! version-added "Added in version 0.2.24"
Args:
graph: graph to send the command to. Supported values are:
graph: Graph to send the command to. Supported values are:
- `None`: the current graph
- `Command.PARENT`: closest parent graph
@@ -415,7 +420,8 @@ def interrupt(value: Any) -> Any:
To use an `interrupt`, you must enable a checkpointer, as the feature relies
on persisting the graph state.
Example:
!!! example
```python
import uuid
from typing import Optional
@@ -516,3 +522,47 @@ def interrupt(value: Any) -> Any:
),
)
)
@dataclass(slots=True)
class Overwrite:
"""Bypass a reducer and write the wrapped value directly to a `BinaryOperatorAggregate` channel.
Receiving multiple `Overwrite` values for the same channel in a single super-step
will raise an `InvalidUpdateError`.
!!! example
```python
from typing import Annotated
import operator
from langgraph.graph import StateGraph
from langgraph.types import Overwrite
class State(TypedDict):
messages: Annotated[list, operator.add]
def node_a(state: TypedDict):
# Normal update: uses the reducer (operator.add)
return {"messages": ["a"]}
def node_b(state: State):
# Overwrite: bypasses the reducer and replaces the entire value
return {"messages": Overwrite(value=["b"])}
builder = StateGraph(State)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.set_entry_point("node_a")
builder.add_edge("node_a", "node_b")
graph = builder.compile()
# Without Overwrite in node_b, messages would be ["START", "a", "b"]
# With Overwrite, messages is just ["b"]
result = graph.invoke({"messages": ["START"]})
assert result == {"messages": ["b"]}
```
"""
value: Any
"""The value to write directly to the channel, bypassing any reducer."""
+2 -2
View File
@@ -31,13 +31,13 @@ ContextT_contra = TypeVar(
)
InputT = TypeVar("InputT", bound=StateLike, default=StateT)
"""Type variable used to represent the input to a state graph.
"""Type variable used to represent the input to a `StateGraph`.
Defaults to `StateT`.
"""
OutputT = TypeVar("OutputT", bound=StateLike, default=StateT)
"""Type variable used to represent the output of a state graph.
"""Type variable used to represent the output of a `StateGraph`.
Defaults to `StateT`.
"""
+37 -13
View File
@@ -4,27 +4,46 @@ build-backend = "hatchling.build"
[project]
name = "langgraph"
version = "1.0.0rc1"
version = "1.0.4"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
requires-python = ">=3.10"
readme = "README.md"
license = "MIT"
license-files = ['LICENSE']
classifiers = [
'Development Status :: 5 - Production/Stable',
'Programming Language :: Python',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
]
dependencies = [
"langchain-core>=0.1",
"langgraph-checkpoint>=2.1.0,<3.0.0",
"langgraph-checkpoint>=2.1.0,<4.0.0",
"langgraph-sdk>=0.2.2,<0.3.0",
"langgraph-prebuilt==0.7.0rc1",
"langgraph-prebuilt>=1.0.2,<1.1.0",
"xxhash>=3.5.0",
"pydantic>=2.7.4",
]
[project.urls]
Repository = "https://www.github.com/langchain-ai/langgraph"
Homepage = "https://docs.langchain.com/oss/python/langgraph/overview"
Documentation = "https://reference.langchain.com/python/langgraph/"
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/langgraph"
Changelog = "https://github.com/langchain-ai/langgraph/releases"
Twitter = "https://x.com/LangChainAI"
Slack = "https://www.langchain.com/join-community"
Reddit = "https://www.reddit.com/r/LangChain/"
[dependency-groups]
dev = [
test = [
"pytest",
"pytest-cov",
"pytest-dotenv",
@@ -32,12 +51,9 @@ dev = [
"syrupy",
"httpx",
"pytest-watcher",
"mypy",
"ruff",
"jupyter",
"pytest-xdist[psutil]",
"pytest-repeat",
"langchain-core==1.0.0a1",
"langchain-core>=1.0.0",
"langgraph-prebuilt",
"langgraph-checkpoint",
"langgraph-checkpoint-sqlite",
@@ -47,14 +63,22 @@ dev = [
"uvloop==0.21.0beta1",
"pyperf",
"py-spy",
"types-requests",
"pycryptodome",
"langgraph-cli[inmem]",
"langgraph-cli; python_version < '3.14'",
"langgraph-cli[inmem]; python_version < '3.14'",
"redis",
]
lint = [
"mypy",
"ruff",
"types-requests",
]
dev = [
{include-group = "test"},
{include-group = "lint"},
"jupyter",
]
[tool.uv]
default-groups = ['dev']
[tool.uv.sources]
langgraph-prebuilt = { path = "../prebuilt", editable = true }
File diff suppressed because one or more lines are too long
@@ -666,18 +666,18 @@
__end__([<p>__end__</p>]):::last
__start__ --> uno;
uno -.-> dos;
uno -.-> subgraph_one;
uno -.-> subgraph\3aone;
dos --> __end__;
subgraph___end__ --> __end__;
subgraph\3a__end__ --> __end__;
subgraph subgraph
subgraph_one(one)
subgraph_two(two)
subgraph_three(three)
subgraph___end__(<p>__end__</p>)
subgraph_one -.-> subgraph_three;
subgraph_one -.-> subgraph_two;
subgraph_three --> subgraph___end__;
subgraph_two --> subgraph___end__;
subgraph\3aone(one)
subgraph\3atwo(two)
subgraph\3athree(three)
subgraph\3a__end__(<p>__end__</p>)
subgraph\3aone -.-> subgraph\3athree;
subgraph\3aone -.-> subgraph\3atwo;
subgraph\3athree --> subgraph\3a__end__;
subgraph\3atwo --> subgraph\3a__end__;
end
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
@@ -705,11 +705,11 @@
__start__([<p>__start__</p>]):::first
side(side)
__end__([<p>__end__</p>]):::last
__start__ --> inner_up;
inner_up --> side;
__start__ --> inner\3aup;
inner\3aup --> side;
side --> __end__;
subgraph inner
inner_up(up)
inner\3aup(up)
end
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
@@ -868,19 +868,19 @@
__end__([<p>__end__</p>]):::last
__start__ -.-> tool_one;
__start__ -.-> tool_three;
__start__ -.-> tool_two___start__;
__start__ -.-> tool_two\3a__start__;
tool_one --> __end__;
tool_three --> __end__;
tool_two___end__ --> __end__;
tool_two\3a__end__ --> __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_fast;
tool_two___start__ -.-> tool_two_tool_two_slow;
tool_two_tool_two_fast --> tool_two___end__;
tool_two_tool_two_slow --> tool_two___end__;
tool_two\3a__start__(<p>__start__</p>)
tool_two\3atool_two_slow(tool_two_slow)
tool_two\3atool_two_fast(tool_two_fast)
tool_two\3a__end__(<p>__end__</p>)
tool_two\3a__start__ -.-> tool_two\3atool_two_fast;
tool_two\3a__start__ -.-> tool_two\3atool_two_slow;
tool_two\3atool_two_fast --> tool_two\3a__end__;
tool_two\3atool_two_slow --> tool_two\3a__end__;
end
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
@@ -891,13 +891,13 @@
# name: test_repeat_condition
'''
graph TD;
Call_Tool -.-> Chart_Generator;
Call_Tool -.-> Researcher;
Chart_Generator -. &nbsp;call_tool&nbsp; .-> Call_Tool;
Chart_Generator -. &nbsp;continue&nbsp; .-> Researcher;
Chart_Generator -. &nbsp;end&nbsp; .-> __end__;
Researcher -. &nbsp;call_tool&nbsp; .-> Call_Tool;
Researcher -. &nbsp;continue&nbsp; .-> Chart_Generator;
Call\20Tool -.-> Chart\20Generator;
Call\20Tool -.-> Researcher;
Chart\20Generator -. &nbsp;call_tool&nbsp; .-> Call\20Tool;
Chart\20Generator -. &nbsp;continue&nbsp; .-> Researcher;
Chart\20Generator -. &nbsp;end&nbsp; .-> __end__;
Researcher -. &nbsp;call_tool&nbsp; .-> Call\20Tool;
Researcher -. &nbsp;continue&nbsp; .-> Chart\20Generator;
Researcher -. &nbsp;end&nbsp; .-> __end__;
__start__ --> Researcher;
Researcher -. &nbsp;redo&nbsp; .-> Researcher;
@@ -939,25 +939,25 @@
__end__([<p>__end__</p>]):::last
__start__ --> gp_one;
gp_one -. &nbsp;1&nbsp; .-> __end__;
gp_one -. &nbsp;0&nbsp; .-> gp_two___start__;
gp_two___end__ --> gp_one;
gp_one -. &nbsp;0&nbsp; .-> gp_two\3a__start__;
gp_two\3a__end__ --> gp_one;
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_one -. &nbsp;1&nbsp; .-> gp_two___end__;
gp_two_p_one -. &nbsp;0&nbsp; .-> gp_two_p_two___start__;
gp_two_p_two___end__ --> gp_two_p_one;
gp_two\3a__start__(<p>__start__</p>)
gp_two\3ap_one(p_one)
gp_two\3a__end__(<p>__end__</p>)
gp_two\3a__start__ --> gp_two\3ap_one;
gp_two\3ap_one -. &nbsp;1&nbsp; .-> gp_two\3a__end__;
gp_two\3ap_one -. &nbsp;0&nbsp; .-> gp_two\3ap_two\3a__start__;
gp_two\3ap_two\3a__end__ --> gp_two\3ap_one;
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_one -. &nbsp;1&nbsp; .-> gp_two_p_two___end__;
gp_two_p_two_c_one -. &nbsp;0&nbsp; .-> gp_two_p_two_c_two;
gp_two_p_two_c_two --> gp_two_p_two_c_one;
gp_two\3ap_two\3a__start__(<p>__start__</p>)
gp_two\3ap_two\3ac_one(c_one)
gp_two\3ap_two\3ac_two(c_two)
gp_two\3ap_two\3a__end__(<p>__end__</p>)
gp_two\3ap_two\3a__start__ --> gp_two\3ap_two\3ac_one;
gp_two\3ap_two\3ac_one -. &nbsp;1&nbsp; .-> gp_two\3ap_two\3a__end__;
gp_two\3ap_two\3ac_one -. &nbsp;0&nbsp; .-> gp_two\3ap_two\3ac_two;
gp_two\3ap_two\3ac_two --> gp_two\3ap_two\3ac_one;
end
end
classDef default fill:#f2f0ff,line-height:1.2
@@ -979,17 +979,17 @@
__end__([<p>__end__</p>]):::last
__start__ --> p_one;
p_one -. &nbsp;1&nbsp; .-> __end__;
p_one -. &nbsp;0&nbsp; .-> p_two___start__;
p_two___end__ --> p_one;
p_one -. &nbsp;0&nbsp; .-> p_two\3a__start__;
p_two\3a__end__ --> p_one;
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_one -. &nbsp;1&nbsp; .-> p_two___end__;
p_two_c_one -. &nbsp;0&nbsp; .-> p_two_c_two;
p_two_c_two --> p_two_c_one;
p_two\3a__start__(<p>__start__</p>)
p_two\3ac_one(c_one)
p_two\3ac_two(c_two)
p_two\3a__end__(<p>__end__</p>)
p_two\3a__start__ --> p_two\3ac_one;
p_two\3ac_one -. &nbsp;1&nbsp; .-> p_two\3a__end__;
p_two\3ac_one -. &nbsp;0&nbsp; .-> p_two\3ac_two;
p_two\3ac_two --> p_two\3ac_one;
end
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
+30
View File
@@ -7,6 +7,7 @@ from langgraph._internal._typing import MISSING
from langgraph.channels.binop import BinaryOperatorAggregate
from langgraph.channels.last_value import LastValue
from langgraph.channels.topic import Topic
from langgraph.channels.untracked_value import UntrackedValue
from langgraph.errors import EmptyChannelError, InvalidUpdateError
pytestmark = pytest.mark.anyio
@@ -87,3 +88,32 @@ def test_binop() -> None:
checkpoint = channel.checkpoint()
channel = BinaryOperatorAggregate(int, operator.add).from_checkpoint(checkpoint)
assert channel.get() == 10
def test_untracked_value() -> None:
channel = UntrackedValue(dict).from_checkpoint(MISSING)
assert channel.ValueType is dict
assert channel.UpdateType is dict
# UntrackedValue should start empty
with pytest.raises(EmptyChannelError):
channel.get()
# Should be able to update with a value
test_data = {"session": "test", "temp": "dir"}
channel.update([test_data])
assert channel.get() == test_data
# Update with new value
new_data = {"session": "updated", "temp": "newdir"}
channel.update([new_data])
assert channel.get() == new_data
# On checkpoint, UntrackedValue should return MISSING
checkpoint = channel.checkpoint()
assert checkpoint is MISSING
# Creating from checkpoint with MISSING should start empty
new_channel = UntrackedValue(dict).from_checkpoint(checkpoint)
with pytest.raises(EmptyChannelError):
new_channel.get()
@@ -21,18 +21,18 @@ def test_interrupt_legacy_ns() -> None:
assert new_interrupt.id == old_interrupt.id
serializer = JsonPlusSerializer()
serializer = JsonPlusSerializer(allowed_json_modules=True)
def test_serialization_roundtrip() -> None:
"""Test that the legacy interrupt (pre v1) can be reserialized as the modern interrupt without id corruption."""
# generated with:
# JsonPlusSerializer().dumps(Interrupt(value="legacy_test", ns=["legacy_test"], resumable=True, when="during"))
# JsonPlusSerializer().dumps_typed(Interrupt(value="legacy_test", ns=["legacy_test"], resumable=True, when="during"))
legacy_interrupt_bytes = b'{"lc": 2, "type": "constructor", "id": ["langgraph", "types", "Interrupt"], "kwargs": {"value": "legacy_test", "resumable": true, "ns": ["legacy_test"], "when": "during"}}'
legacy_interrupt_id = "f1fa625689ec006a5b32b76863e22a6c"
interrupt = serializer.loads(legacy_interrupt_bytes)
interrupt = serializer.loads_typed(("json", legacy_interrupt_bytes))
assert interrupt.id == legacy_interrupt_id
assert interrupt.value == "legacy_test"
@@ -41,10 +41,10 @@ def test_serialization_roundtrip_complex_ns() -> None:
"""Test that the legacy interrupt (pre v1), with a more complex ns can be reserialized as the modern interrupt without id corruption."""
# generated with:
# JsonPlusSerializer().dumps(Interrupt(value="legacy_test", ns=["legacy:test", "with:complex", "name:space"], resumable=True, when="during"))
# JsonPlusSerializer().dumps_typed(Interrupt(value="legacy_test", ns=["legacy:test", "with:complex", "name:space"], resumable=True, when="during"))
legacy_interrupt_bytes = b'{"lc": 2, "type": "constructor", "id": ["langgraph", "types", "Interrupt"], "kwargs": {"value": "legacy_test", "resumable": true, "ns": ["legacy:test", "with:complex", "name:space"], "when": "during"}}'
legacy_interrupt_id = "e69356a9ee3630ee7f4f597f2693000c"
interrupt = serializer.loads(legacy_interrupt_bytes)
interrupt = serializer.loads_typed(("json", legacy_interrupt_bytes))
assert interrupt.id == legacy_interrupt_id
assert interrupt.value == "legacy_test"

Some files were not shown because too many files have changed in this diff Show More