Compare commits

...
Author SHA1 Message Date
William FHandGitHub 8f649abd0a Release PG Checkpointer (#2536) 2024-11-26 01:44:45 +00:00
William FHandGitHub 1febec7c0d Dedup store batch operations (#2534) 2024-11-25 16:31:26 -08:00
Nuno CamposandGitHub a4eb4c6942 Merge pull request #2520 from langchain-ai/nc/22nov/parent-command
lib: Add Command(graph=Command.PARENT, ...)
2024-11-25 15:39:51 -08:00
Nuno Campos 8e1cd0e225 Add test 2024-11-25 14:11:20 -08:00
98935e1ffd fix: Fix race condition in PostgresSaver (#2494)
Signed-off-by: Tyler Ball <tyleraball@gmail.com>
Co-authored-by: Phoenix Logan <plogan@chanzuckerberg.com>
Co-authored-by: Tyler Ball <2481463+tyler-ball@users.noreply.github.com>
2024-11-25 20:19:52 +00:00
William FHandGitHub 328ef609af [CLI] Python path (#2531) 2024-11-25 11:59:49 -08:00
Talha MunirandGitHub 486d5412af docs: Fix grammatical mistake in introduction.ipynb (#2521) 2024-11-23 14:37:55 -05:00
Nuno Campos abc0c8c223 Fix 2024-11-22 16:35:20 -08:00
Nuno Campos 5bbb9dae57 Fix 2024-11-22 16:34:55 -08:00
Nuno Campos fed60e713c lib: Add Command(graph=Command.PARENT, ...)
- This makes the command bubble up out of the current graph and be handled by the calling graph (the immediate parent)
- This could be extended to support eg. ROOT graph, or some other level
2024-11-22 16:28:43 -08:00
Eugene YurtsevandGitHub 4f4e7a6981 docs: more fixes for python version (#2515) 2024-11-22 19:50:31 +00:00
3351d4f6c5 docs: fix typo (#2510)
`python-dotenv` not `python-dot-env`

Signed-off-by: Mingqi <mingqi.hu@intel.com>
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2024-11-22 14:17:59 -05:00
Eugene YurtsevandGitHub b4900341e4 docs: fix broken link (#2514)
We need to check later why CI didn't fail with original PR that broke
the link
2024-11-22 14:07:54 -05:00
Eugene YurtsevandGitHub 65f515e020 docs: add helm chart link (#2512) 2024-11-22 17:17:36 +00:00
Eugene YurtsevandGitHub 0d0665a6e3 docs: Add resource allocation (#2511) 2024-11-22 11:56:16 -05:00
Eugene YurtsevandGitHub 93b8525dc1 docs: fix link checker (#2508)
3rd attempt to fix localhost link
2024-11-21 21:59:43 -05:00
Eugene YurtsevandGitHub aeb6f784e1 docs: fix link checking? (#2506) 2024-11-21 21:21:30 -05:00
Nuno Campos 3eedeac0d4 Not red 2024-11-21 15:31:52 -08:00
Eugene YurtsevandGitHub b09e7b20b0 docs: do not check localhost links (#2505) 2024-11-21 23:28:24 +00:00
Eugene YurtsevandGitHub 26ce731eab docs: update README.md (#2474) 2024-11-21 22:48:12 +00:00
Eugene YurtsevandGitHub 55593446f8 docs: get started with langgraph platform (#2469) 2024-11-21 17:41:03 -05:00
William FHandGitHub 7082e2613e [CLI] Dotenv support (#2501) 2024-11-21 16:28:53 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>William FH
ceeb9636ee build(deps-dev): bump notebook from 7.0.7 to 7.2.2 in /libs/langgraph (#2411)
Bumps [notebook](https://github.com/jupyter/notebook) from 7.0.7 to
7.2.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/jupyter/notebook/releases">notebook's
releases</a>.</em></p>
<blockquote>
<h2>v7.2.2</h2>
<h2>7.2.2</h2>
<p>(<a
href="https://github.com/jupyter/notebook/compare/@jupyter-notebook/application-extension@7.2.1...0426a897ad6b5708d73e6e49ea424076de2906a1">Full
Changelog</a>)</p>
<h3>Maintenance and upkeep improvements</h3>
<ul>
<li>Upgrade JupyterLab dependencies to v4.2.5 <a
href="https://redirect.github.com/jupyter/notebook/pull/7447">#7447</a>
(<a
href="https://github.com/krassowski"><code>@​krassowski</code></a>)</li>
</ul>
<h3>Contributors to this release</h3>
<p>(<a
href="https://github.com/jupyter/notebook/graphs/contributors?from=2024-06-07&amp;to=2024-08-27&amp;type=c">GitHub
contributors page for this release</a>)</p>
<p><a
href="https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Agithub-actions+updated%3A2024-06-07..2024-08-27&amp;type=Issues"><code>@​github-actions</code></a>
| <a
href="https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Akrassowski+updated%3A2024-06-07..2024-08-27&amp;type=Issues"><code>@​krassowski</code></a>
| <a
href="https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3ARRosio+updated%3A2024-06-07..2024-08-27&amp;type=Issues"><code>@​RRosio</code></a></p>
<h2>v7.2.1</h2>
<h2>7.2.1</h2>
<p>(<a
href="https://github.com/jupyter/notebook/compare/@jupyter-notebook/application-extension@7.2.0...e881745c98ea0a0ea585df78f1ca8950a0edeaa2">Full
Changelog</a>)</p>
<h3>Bugs fixed</h3>
<ul>
<li>Remove pseudoelement obstructing the cell collapser <a
href="https://redirect.github.com/jupyter/notebook/pull/7392">#7392</a>
(<a
href="https://github.com/krassowski"><code>@​krassowski</code></a>)</li>
</ul>
<h3>Contributors to this release</h3>
<p>(<a
href="https://github.com/jupyter/notebook/graphs/contributors?from=2024-05-16&amp;to=2024-06-07&amp;type=c">GitHub
contributors page for this release</a>)</p>
<p><a
href="https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Agithub-actions+updated%3A2024-05-16..2024-06-07&amp;type=Issues"><code>@​github-actions</code></a>
| <a
href="https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Ajtpio+updated%3A2024-05-16..2024-06-07&amp;type=Issues"><code>@​jtpio</code></a>
| <a
href="https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Ameeseeksmachine+updated%3A2024-05-16..2024-06-07&amp;type=Issues"><code>@​meeseeksmachine</code></a></p>
<h2>v7.2.0</h2>
<h2>7.2.0</h2>
<p>(<a
href="https://github.com/jupyter/notebook/compare/@jupyter-notebook/application-extension@7.1.2...31bf294e85175bbf39816a90dc8858dedaf73bde">Full
Changelog</a>)</p>
<h3>Enhancements made</h3>
<ul>
<li>Update to JupyterLab 4.2.0 <a
href="https://redirect.github.com/jupyter/notebook/pull/7357">#7357</a>
(<a href="https://github.com/jtpio"><code>@​jtpio</code></a>)</li>
<li>Update to JupyterLab 4.2.0rc0 <a
href="https://redirect.github.com/jupyter/notebook/pull/7333">#7333</a>
(<a href="https://github.com/jtpio"><code>@​jtpio</code></a>)</li>
<li>Add <code>@jupyterlab/theme-dark-high-contrast-extension</code> <a
href="https://redirect.github.com/jupyter/notebook/pull/7331">#7331</a>
(<a href="https://github.com/jtpio"><code>@​jtpio</code></a>)</li>
<li>Update to JupyterLab 4.2.0a2 <a
href="https://redirect.github.com/jupyter/notebook/pull/7307">#7307</a>
(<a href="https://github.com/jtpio"><code>@​jtpio</code></a>)</li>
</ul>
<h3>Bugs fixed</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/jupyter/notebook/blob/@jupyter-notebook/tree@7.2.2/CHANGELOG.md">notebook's
changelog</a>.</em></p>
<blockquote>
<h2>7.2.2</h2>
<p>(<a
href="https://github.com/jupyter/notebook/compare/@jupyter-notebook/application-extension@7.2.1...0426a897ad6b5708d73e6e49ea424076de2906a1">Full
Changelog</a>)</p>
<h3>Maintenance and upkeep improvements</h3>
<ul>
<li>Upgrade JupyterLab dependencies to v4.2.5 <a
href="https://redirect.github.com/jupyter/notebook/pull/7447">#7447</a>
(<a
href="https://github.com/krassowski"><code>@​krassowski</code></a>)</li>
</ul>
<h3>Contributors to this release</h3>
<p>(<a
href="https://github.com/jupyter/notebook/graphs/contributors?from=2024-06-07&amp;to=2024-08-27&amp;type=c">GitHub
contributors page for this release</a>)</p>
<p><a
href="https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Agithub-actions+updated%3A2024-06-07..2024-08-27&amp;type=Issues"><code>@​github-actions</code></a>
| <a
href="https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Akrassowski+updated%3A2024-06-07..2024-08-27&amp;type=Issues"><code>@​krassowski</code></a>
| <a
href="https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3ARRosio+updated%3A2024-06-07..2024-08-27&amp;type=Issues"><code>@​RRosio</code></a></p>
<!-- raw HTML omitted -->
<h2>7.2.1</h2>
<p>(<a
href="https://github.com/jupyter/notebook/compare/@jupyter-notebook/application-extension@7.2.0...e881745c98ea0a0ea585df78f1ca8950a0edeaa2">Full
Changelog</a>)</p>
<h3>Bugs fixed</h3>
<ul>
<li>Remove pseudoelement obstructing the cell collapser <a
href="https://redirect.github.com/jupyter/notebook/pull/7392">#7392</a>
(<a
href="https://github.com/krassowski"><code>@​krassowski</code></a>)</li>
</ul>
<h3>Contributors to this release</h3>
<p>(<a
href="https://github.com/jupyter/notebook/graphs/contributors?from=2024-05-16&amp;to=2024-06-07&amp;type=c">GitHub
contributors page for this release</a>)</p>
<p><a
href="https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Agithub-actions+updated%3A2024-05-16..2024-06-07&amp;type=Issues"><code>@​github-actions</code></a>
| <a
href="https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Ajtpio+updated%3A2024-05-16..2024-06-07&amp;type=Issues"><code>@​jtpio</code></a>
| <a
href="https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Ameeseeksmachine+updated%3A2024-05-16..2024-06-07&amp;type=Issues"><code>@​meeseeksmachine</code></a></p>
<h2>7.2.0</h2>
<p>(<a
href="https://github.com/jupyter/notebook/compare/@jupyter-notebook/application-extension@7.1.2...31bf294e85175bbf39816a90dc8858dedaf73bde">Full
Changelog</a>)</p>
<h3>Enhancements made</h3>
<ul>
<li>Update to JupyterLab 4.2.0 <a
href="https://redirect.github.com/jupyter/notebook/pull/7357">#7357</a>
(<a href="https://github.com/jtpio"><code>@​jtpio</code></a>)</li>
<li>Update to JupyterLab 4.2.0rc0 <a
href="https://redirect.github.com/jupyter/notebook/pull/7333">#7333</a>
(<a href="https://github.com/jtpio"><code>@​jtpio</code></a>)</li>
<li>Add <code>@jupyterlab/theme-dark-high-contrast-extension</code> <a
href="https://redirect.github.com/jupyter/notebook/pull/7331">#7331</a>
(<a href="https://github.com/jtpio"><code>@​jtpio</code></a>)</li>
<li>Update to JupyterLab 4.2.0a2 <a
href="https://redirect.github.com/jupyter/notebook/pull/7307">#7307</a>
(<a href="https://github.com/jtpio"><code>@​jtpio</code></a>)</li>
</ul>
<h3>Bugs fixed</h3>
<ul>
<li>Add the <code>@jupyterlab/notebook-extension:copy-output</code>
plugin <a
href="https://redirect.github.com/jupyter/notebook/pull/7353">#7353</a>
(<a href="https://github.com/jtpio"><code>@​jtpio</code></a>)</li>
<li>Fix CSS for <code>full</code> windowing mode <a
href="https://redirect.github.com/jupyter/notebook/pull/7337">#7337</a>
(<a href="https://github.com/jtpio"><code>@​jtpio</code></a>)</li>
<li>Force notebook windowing mode to <code>defer</code> <a
href="https://redirect.github.com/jupyter/notebook/pull/7335">#7335</a>
(<a href="https://github.com/jtpio"><code>@​jtpio</code></a>)</li>
<li>Fix scrollbar always showing up by default <a
href="https://redirect.github.com/jupyter/notebook/pull/7327">#7327</a>
(<a href="https://github.com/jtpio"><code>@​jtpio</code></a>)</li>
<li>Default to the <code>full</code> windowing mode <a
href="https://redirect.github.com/jupyter/notebook/pull/7321">#7321</a>
(<a href="https://github.com/jtpio"><code>@​jtpio</code></a>)</li>
</ul>
<h3>Maintenance and upkeep improvements</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/jupyter/notebook/commit/d1d232b27c5441c4a040dd3ca491a7cf0fa6c528"><code>d1d232b</code></a>
Publish 7.2.2</li>
<li><a
href="https://github.com/jupyter/notebook/commit/0426a897ad6b5708d73e6e49ea424076de2906a1"><code>0426a89</code></a>
Upgrade JupyterLab dependencies to v4.2.5 (<a
href="https://redirect.github.com/jupyter/notebook/issues/7447">#7447</a>)</li>
<li><a
href="https://github.com/jupyter/notebook/commit/3542421de92c91892d8d8c40ebbd023215c39606"><code>3542421</code></a>
Publish 7.2.1</li>
<li><a
href="https://github.com/jupyter/notebook/commit/e881745c98ea0a0ea585df78f1ca8950a0edeaa2"><code>e881745</code></a>
Backport PR <a
href="https://redirect.github.com/jupyter/notebook/issues/7392">#7392</a>:
Remove pseudoelement obstructing the cell collapser (<a
href="https://redirect.github.com/jupyter/notebook/issues/7393">#7393</a>)</li>
<li><a
href="https://github.com/jupyter/notebook/commit/30587b826a0fe7055a02ea96d43e6305d8b5590b"><code>30587b8</code></a>
Publish 7.2.0</li>
<li><a
href="https://github.com/jupyter/notebook/commit/31bf294e85175bbf39816a90dc8858dedaf73bde"><code>31bf294</code></a>
Add user facing changelog for 7.2 (<a
href="https://redirect.github.com/jupyter/notebook/issues/7372">#7372</a>)</li>
<li><a
href="https://github.com/jupyter/notebook/commit/08fe5c5df12182178280bad5d2fbae02b3486146"><code>08fe5c5</code></a>
Update <code>@jupyterlab/galata</code> (<a
href="https://redirect.github.com/jupyter/notebook/issues/7361">#7361</a>)</li>
<li><a
href="https://github.com/jupyter/notebook/commit/7891117aa9f9cb95c8e301875f9bf74d9496a301"><code>7891117</code></a>
Update config.yml (<a
href="https://redirect.github.com/jupyter/notebook/issues/7363">#7363</a>)</li>
<li><a
href="https://github.com/jupyter/notebook/commit/a1e25b92bf10ef13a760353837114db9b498f242"><code>a1e25b9</code></a>
Publish 7.2.0rc1</li>
<li><a
href="https://github.com/jupyter/notebook/commit/f5d8aea3bdc3eea25213792f9d101738f2a1f627"><code>f5d8aea</code></a>
Default to the <code>full</code> windowing mode (<a
href="https://redirect.github.com/jupyter/notebook/issues/7321">#7321</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/jupyter/notebook/compare/@jupyter-notebook/tree@7.0.7...@jupyter-notebook/tree@7.2.2">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: William FH <13333726+hinthornw@users.noreply.github.com>
2024-11-21 08:00:26 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>William FH
f7788abbb6 build(deps-dev): bump starlette from 0.38.6 to 0.40.0 (#2421)
Bumps [starlette](https://github.com/encode/starlette) from 0.38.6 to
0.40.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/encode/starlette/releases">starlette's
releases</a>.</em></p>
<blockquote>
<h2>Version 0.40.0</h2>
<p>This release fixes a Denial of service (DoS) via
<code>multipart/form-data</code> requests.</p>
<p>You can view the full security advisory:
<a
href="https://github.com/encode/starlette/security/advisories/GHSA-f96h-pmfr-66vw">GHSA-f96h-pmfr-66vw</a></p>
<h2>Fixed</h2>
<ul>
<li>Add <code>max_part_size</code> to <code>MultiPartParser</code> to
limit the size of parts in <code>multipart/form-data</code>
requests <a
href="https://github.com/encode/starlette/commit/fd038f3070c302bff17ef7d173dbb0b007617733">fd038f3</a>.</li>
</ul>
<h2>Version 0.39.2</h2>
<h2>Fixed</h2>
<ul>
<li>Allow use of <code>request.url_for</code> when only &quot;app&quot;
scope is available <a
href="https://redirect.github.com/encode/starlette/pull/2672">#2672</a>.</li>
<li>Fix internal type hints to support
<code>python-multipart==0.0.12</code> <a
href="https://redirect.github.com/encode/starlette/pull/2708">#2708</a>.</li>
</ul>
<hr />
<p><strong>Full Changelog</strong>: <a
href="https://github.com/encode/starlette/compare/0.39.1...0.39.2">https://github.com/encode/starlette/compare/0.39.1...0.39.2</a></p>
<h2>Version 0.39.1</h2>
<h2>Fixed</h2>
<ul>
<li>Avoid regex re-compilation in <code>responses.py</code> and
<code>schemas.py</code> <a
href="https://redirect.github.com/encode/starlette/pull/2700">#2700</a>.</li>
<li>Improve performance of <code>get_route_path</code> by removing
regular expression usage <a
href="https://redirect.github.com/encode/starlette/pull/2701">#2701</a>.</li>
<li>Consider <code>FileResponse.chunk_size</code> when handling multiple
ranges <a
href="https://redirect.github.com/encode/starlette/pull/2703">#2703</a>.</li>
<li>Use <code>token_hex</code> for generating multipart boundary strings
<a
href="https://redirect.github.com/encode/starlette/pull/2702">#2702</a>.</li>
</ul>
<hr />
<p><strong>Full Changelog</strong>: <a
href="https://github.com/encode/starlette/compare/0.39.0...0.39.1">https://github.com/encode/starlette/compare/0.39.0...0.39.1</a></p>
<h2>Version 0.39.0</h2>
<h2>Added</h2>
<ul>
<li>Add support for HTTP Range to <code>FileResponse</code> <a
href="https://redirect.github.com/encode/starlette/pull/2697">#2697</a></li>
</ul>
<hr />
<p><strong>Full Changelog</strong>: <a
href="https://github.com/encode/starlette/compare/0.38.6...0.39.0">https://github.com/encode/starlette/compare/0.38.6...0.39.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/encode/starlette/blob/master/docs/release-notes.md">starlette's
changelog</a>.</em></p>
<blockquote>
<h2>0.40.0 (October 15, 2024)</h2>
<p>This release fixes a Denial of service (DoS) via
<code>multipart/form-data</code> requests.</p>
<p>You can view the full security advisory:
<a
href="https://github.com/encode/starlette/security/advisories/GHSA-f96h-pmfr-66vw">GHSA-f96h-pmfr-66vw</a></p>
<h4>Fixed</h4>
<ul>
<li>Add <code>max_part_size</code> to <code>MultiPartParser</code> to
limit the size of parts in <code>multipart/form-data</code>
requests <a
href="https://github.com/encode/starlette/commit/fd038f3070c302bff17ef7d173dbb0b007617733">fd038f3</a>.</li>
</ul>
<h2>0.39.2 (September 29, 2024)</h2>
<h4>Fixed</h4>
<ul>
<li>Allow use of <code>request.url_for</code> when only &quot;app&quot;
scope is available <a
href="https://redirect.github.com/encode/starlette/pull/2672">#2672</a>.</li>
<li>Fix internal type hints to support
<code>python-multipart==0.0.12</code> <a
href="https://redirect.github.com/encode/starlette/pull/2708">#2708</a>.</li>
</ul>
<h2>0.39.1 (September 25, 2024)</h2>
<h4>Fixed</h4>
<ul>
<li>Avoid regex re-compilation in <code>responses.py</code> and
<code>schemas.py</code> <a
href="https://redirect.github.com/encode/starlette/pull/2700">#2700</a>.</li>
<li>Improve performance of <code>get_route_path</code> by removing
regular expression usage
<a
href="https://redirect.github.com/encode/starlette/pull/2701">#2701</a>.</li>
<li>Consider <code>FileResponse.chunk_size</code> when handling multiple
ranges <a
href="https://redirect.github.com/encode/starlette/pull/2703">#2703</a>.</li>
<li>Use <code>token_hex</code> for generating multipart boundary strings
<a
href="https://redirect.github.com/encode/starlette/pull/2702">#2702</a>.</li>
</ul>
<h2>0.39.0 (September 23, 2024)</h2>
<h4>Added</h4>
<ul>
<li>Add support for <a
href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests">HTTP
Range</a> to
<code>FileResponse</code> <a
href="https://redirect.github.com/encode/starlette/pull/2697">#2697</a>.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/encode/starlette/commit/4ded4b7ac517bd301cee69f5c189b1cb48c069b6"><code>4ded4b7</code></a>
Version 0.40.0 (<a
href="https://redirect.github.com/encode/starlette/issues/2728">#2728</a>)</li>
<li><a
href="https://github.com/encode/starlette/commit/fd038f3070c302bff17ef7d173dbb0b007617733"><code>fd038f3</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/encode/starlette/commit/e11684013fe5ca084f5bd4e54830512a4dff9618"><code>e116840</code></a>
Bump the python-packages group with 6 updates (<a
href="https://redirect.github.com/encode/starlette/issues/2713">#2713</a>)</li>
<li><a
href="https://github.com/encode/starlette/commit/0b50b9c4abd992a39d6e32148cc6f577ac3b1c44"><code>0b50b9c</code></a>
Version 0.39.2 (<a
href="https://redirect.github.com/encode/starlette/issues/2710">#2710</a>)</li>
<li><a
href="https://github.com/encode/starlette/commit/fe46d99d92da17efe1827f96ad29d748aac870d2"><code>fe46d99</code></a>
Support <code>request.url_for</code> when only &quot;app&quot; scope is
avaialable (<a
href="https://redirect.github.com/encode/starlette/issues/2672">#2672</a>)</li>
<li><a
href="https://github.com/encode/starlette/commit/1a6018e08a994c78f5c169b8535408259af0f249"><code>1a6018e</code></a>
Support python-multipart 0.0.12 (<a
href="https://redirect.github.com/encode/starlette/issues/2708">#2708</a>)</li>
<li><a
href="https://github.com/encode/starlette/commit/fa7b382a66cd99e3dc18f3baa44dae5ec68be76b"><code>fa7b382</code></a>
Version 0.39.1 (<a
href="https://redirect.github.com/encode/starlette/issues/2706">#2706</a>)</li>
<li><a
href="https://github.com/encode/starlette/commit/075efd0c5c9f5e49a4416f3b4a24e24efab135f8"><code>075efd0</code></a>
generate boundary with token_hex (<a
href="https://redirect.github.com/encode/starlette/issues/2702">#2702</a>)</li>
<li><a
href="https://github.com/encode/starlette/commit/b8139f9fe3b1acb34ddbe38dc6472a60b621540e"><code>b8139f9</code></a>
Consider <code>FileResponse.chunk_size</code> when handling multiple
ranges (<a
href="https://redirect.github.com/encode/starlette/issues/2703">#2703</a>)</li>
<li><a
href="https://github.com/encode/starlette/commit/4fbf766b3eac4146b86175682cec88d266fd8470"><code>4fbf766</code></a>
test: add tests in <code>test_requests</code> (<a
href="https://redirect.github.com/encode/starlette/issues/2677">#2677</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/encode/starlette/compare/0.38.6...0.40.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=starlette&package-manager=pip&previous-version=0.38.6&new-version=0.40.0)](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>
Co-authored-by: William FH <13333726+hinthornw@users.noreply.github.com>
2024-11-21 07:58:30 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9bd430142a build(deps-dev): bump aiohttp from 3.10.6 to 3.10.11 (#2454)
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.10.6 to
3.10.11.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/aio-libs/aiohttp/releases">aiohttp's
releases</a>.</em></p>
<blockquote>
<h2>3.10.11</h2>
<h2>Bug fixes</h2>
<ul>
<li>
<p>Authentication provided by a redirect now takes precedence over
provided <code>auth</code> when making requests with the client -- by
:user:<code>PLPeeters</code>.</p>
<p><em>Related issues and pull requests on GitHub:</em>
<a
href="https://redirect.github.com/aio-libs/aiohttp/issues/9436">#9436</a>.</p>
</li>
<li>
<p>Fixed :py:meth:<code>WebSocketResponse.close()
&lt;aiohttp.web.WebSocketResponse.close&gt;</code> to discard non-close
messages within its timeout window after sending close -- by
:user:<code>lenard-mosys</code>.</p>
<p><em>Related issues and pull requests on GitHub:</em>
<a
href="https://redirect.github.com/aio-libs/aiohttp/issues/9506">#9506</a>.</p>
</li>
<li>
<p>Fixed a deadlock that could occur while attempting to get a new
connection slot after a timeout -- by :user:<code>bdraco</code>.</p>
<p>The connector was not cancellation-safe.</p>
<p><em>Related issues and pull requests on GitHub:</em>
<a
href="https://redirect.github.com/aio-libs/aiohttp/issues/9670">#9670</a>,
<a
href="https://redirect.github.com/aio-libs/aiohttp/issues/9671">#9671</a>.</p>
</li>
<li>
<p>Fixed the WebSocket flow control calculation undercounting with
multi-byte data -- by :user:<code>bdraco</code>.</p>
<p><em>Related issues and pull requests on GitHub:</em>
<a
href="https://redirect.github.com/aio-libs/aiohttp/issues/9686">#9686</a>.</p>
</li>
<li>
<p>Fixed incorrect parsing of chunk extensions with the pure Python
parser -- by :user:<code>bdraco</code>.</p>
<p><em>Related issues and pull requests on GitHub:</em>
<a
href="https://redirect.github.com/aio-libs/aiohttp/issues/9851">#9851</a>.</p>
</li>
<li>
<p>Fixed system routes polluting the middleware cache -- by
:user:<code>bdraco</code>.</p>
<p><em>Related issues and pull requests on GitHub:</em></p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst">aiohttp's
changelog</a>.</em></p>
<blockquote>
<h1>3.10.11 (2024-11-13)</h1>
<h2>Bug fixes</h2>
<ul>
<li>
<p>Authentication provided by a redirect now takes precedence over
provided <code>auth</code> when making requests with the client -- by
:user:<code>PLPeeters</code>.</p>
<p><em>Related issues and pull requests on GitHub:</em>
:issue:<code>9436</code>.</p>
</li>
<li>
<p>Fixed :py:meth:<code>WebSocketResponse.close()
&lt;aiohttp.web.WebSocketResponse.close&gt;</code> to discard non-close
messages within its timeout window after sending close -- by
:user:<code>lenard-mosys</code>.</p>
<p><em>Related issues and pull requests on GitHub:</em>
:issue:<code>9506</code>.</p>
</li>
<li>
<p>Fixed a deadlock that could occur while attempting to get a new
connection slot after a timeout -- by :user:<code>bdraco</code>.</p>
<p>The connector was not cancellation-safe.</p>
<p><em>Related issues and pull requests on GitHub:</em>
:issue:<code>9670</code>, :issue:<code>9671</code>.</p>
</li>
<li>
<p>Fixed the WebSocket flow control calculation undercounting with
multi-byte data -- by :user:<code>bdraco</code>.</p>
<p><em>Related issues and pull requests on GitHub:</em>
:issue:<code>9686</code>.</p>
</li>
<li>
<p>Fixed incorrect parsing of chunk extensions with the pure Python
parser -- by :user:<code>bdraco</code>.</p>
<p><em>Related issues and pull requests on GitHub:</em>
:issue:<code>9851</code>.</p>
</li>
<li>
<p>Fixed system routes polluting the middleware cache -- by
:user:<code>bdraco</code>.</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/aio-libs/aiohttp/commit/3e09325e4839117df13fbac301f360edf8d3a0ee"><code>3e09325</code></a>
Remove 3.10.11rc0 from 3.10 changelog (<a
href="https://redirect.github.com/aio-libs/aiohttp/issues/9858">#9858</a>)</li>
<li><a
href="https://github.com/aio-libs/aiohttp/commit/beb7b740533b81d75706e6615f07d92fcbf1c325"><code>beb7b74</code></a>
Release 3.10.11 (<a
href="https://redirect.github.com/aio-libs/aiohttp/issues/9857">#9857</a>)</li>
<li><a
href="https://github.com/aio-libs/aiohttp/commit/259edc369075de63e6f3a4eaade058c62af0df71"><code>259edc3</code></a>
[PR <a
href="https://redirect.github.com/aio-libs/aiohttp/issues/9851">#9851</a>/541d86d
backport][3.10] Fix incorrect parsing of chunk extensions w...</li>
<li><a
href="https://github.com/aio-libs/aiohttp/commit/bc15db61615079d1b6327ba42c682f758fa96936"><code>bc15db6</code></a>
[PR <a
href="https://redirect.github.com/aio-libs/aiohttp/issues/9852">#9852</a>/249855a
backport][3.10] Fix system routes polluting the middleware ...</li>
<li><a
href="https://github.com/aio-libs/aiohttp/commit/158bf304bdd8047eec192540fa5bf7fe3862bffd"><code>158bf30</code></a>
Release 3.10.11rc0 (<a
href="https://redirect.github.com/aio-libs/aiohttp/issues/9848">#9848</a>)</li>
<li><a
href="https://github.com/aio-libs/aiohttp/commit/e5917cd3480b01e7527b6524f9bec954325e1d5f"><code>e5917cd</code></a>
[PR <a
href="https://redirect.github.com/aio-libs/aiohttp/issues/9844">#9844</a>/fabf3884
backport][3.10] Fix compressed get request benchmark paylo...</li>
<li><a
href="https://github.com/aio-libs/aiohttp/commit/68a1f42af90a5beae28c8617e0dfc15c3bd5153c"><code>68a1f42</code></a>
[PR <a
href="https://redirect.github.com/aio-libs/aiohttp/issues/9840">#9840</a>/cc5fa316
backport][3.10] Add benchmark for sending compressed paylo...</li>
<li><a
href="https://github.com/aio-libs/aiohttp/commit/4f4b90fef082fbb37395c394d68ee0ab3fcbc7e6"><code>4f4b90f</code></a>
[PR <a
href="https://redirect.github.com/aio-libs/aiohttp/issues/9835">#9835</a>/32ccfc9a
backport][3.10] Adjust client payload benchmarks to better...</li>
<li><a
href="https://github.com/aio-libs/aiohttp/commit/f3dd0f9fece79dc3cd9d00e2ffddd49c36598361"><code>f3dd0f9</code></a>
[PR <a
href="https://redirect.github.com/aio-libs/aiohttp/issues/9832">#9832</a>/006f4070
backport][3.10] Increase allowed import time for Python 3....</li>
<li><a
href="https://github.com/aio-libs/aiohttp/commit/f2aab2e40336848d6a53ea03dc6d072a38c5e7f9"><code>f2aab2e</code></a>
[PR <a
href="https://redirect.github.com/aio-libs/aiohttp/issues/9827">#9827</a>/14fcfd4c
backport][3.10] Adjust client GET read benchmarks to inclu...</li>
<li>Additional commits viewable in <a
href="https://github.com/aio-libs/aiohttp/compare/v3.10.6...v3.10.11">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=aiohttp&package-manager=pip&previous-version=3.10.6&new-version=3.10.11)](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>
2024-11-21 07:56:56 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
72dac006f4 build(deps): bump cross-spawn from 7.0.3 to 7.0.6 in /libs/cli/js-examples (#2456)
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from
7.0.3 to 7.0.6.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md">cross-spawn's
changelog</a>.</em></p>
<blockquote>
<h3><a
href="https://github.com/moxystudio/node-cross-spawn/compare/v7.0.5...v7.0.6">7.0.6</a>
(2024-11-18)</h3>
<h3>Bug Fixes</h3>
<ul>
<li>update cross-spawn version to 7.0.5 in package-lock.json (<a
href="https://github.com/moxystudio/node-cross-spawn/commit/f700743918d901eff92960e15a8dd68f87bd4176">f700743</a>)</li>
</ul>
<h3><a
href="https://github.com/moxystudio/node-cross-spawn/compare/v7.0.4...v7.0.5">7.0.5</a>
(2024-11-07)</h3>
<h3>Bug Fixes</h3>
<ul>
<li>fix escaping bug introduced by backtracking (<a
href="https://github.com/moxystudio/node-cross-spawn/commit/640d391fde65388548601d95abedccc12943374f">640d391</a>)</li>
</ul>
<h3><a
href="https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.4">7.0.4</a>
(2024-11-07)</h3>
<h3>Bug Fixes</h3>
<ul>
<li>disable regexp backtracking (<a
href="https://redirect.github.com/moxystudio/node-cross-spawn/issues/160">#160</a>)
(<a
href="https://github.com/moxystudio/node-cross-spawn/commit/5ff3a07d9add449021d806e45c4168203aa833ff">5ff3a07</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/moxystudio/node-cross-spawn/commit/77cd97f3ca7b62c904a63a698fc4a79bf41977d0"><code>77cd97f</code></a>
chore(release): 7.0.6</li>
<li><a
href="https://github.com/moxystudio/node-cross-spawn/commit/6717de49ff1e5de49622488dcb9c33fb25370c85"><code>6717de4</code></a>
chore: upgrade standard-version</li>
<li><a
href="https://github.com/moxystudio/node-cross-spawn/commit/f700743918d901eff92960e15a8dd68f87bd4176"><code>f700743</code></a>
fix: update cross-spawn version to 7.0.5 in package-lock.json</li>
<li><a
href="https://github.com/moxystudio/node-cross-spawn/commit/9a7e3b2165917367f74b8365faad9873b30d7263"><code>9a7e3b2</code></a>
chore: fix build status badge</li>
<li><a
href="https://github.com/moxystudio/node-cross-spawn/commit/085268352dcbcad8064c64c5efb25268b4023184"><code>0852683</code></a>
chore(release): 7.0.5</li>
<li><a
href="https://github.com/moxystudio/node-cross-spawn/commit/640d391fde65388548601d95abedccc12943374f"><code>640d391</code></a>
fix: fix escaping bug introduced by backtracking</li>
<li><a
href="https://github.com/moxystudio/node-cross-spawn/commit/bff0c87c8b627c4e6d04ec2449e733048bebb464"><code>bff0c87</code></a>
chore: remove codecov</li>
<li><a
href="https://github.com/moxystudio/node-cross-spawn/commit/a7c6abc6fee79641d45b452fe6217deaa1bd0973"><code>a7c6abc</code></a>
chore: replace travis with github workflows</li>
<li><a
href="https://github.com/moxystudio/node-cross-spawn/commit/9b9246e0969e86656d7ccd527716bc3c18842a19"><code>9b9246e</code></a>
chore(release): 7.0.4</li>
<li><a
href="https://github.com/moxystudio/node-cross-spawn/commit/5ff3a07d9add449021d806e45c4168203aa833ff"><code>5ff3a07</code></a>
fix: disable regexp backtracking (<a
href="https://redirect.github.com/moxystudio/node-cross-spawn/issues/160">#160</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cross-spawn&package-manager=npm_and_yarn&previous-version=7.0.3&new-version=7.0.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)
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>
2024-11-21 07:56:33 -08:00
William FHandGitHub 54d848913f [CLI] Update Inmem Version (#2500) 2024-11-21 15:49:59 +00:00
Nuno CamposandGitHub 7021e81150 Merge pull request #2496 from langchain-ai/vb/fix-error-message
langgraph: fix error message on invalid update
2024-11-20 18:46:50 -08:00
vbarda b977045679 langgraph: fix error message on invalid update 2024-11-20 21:24:50 -05:00
William FHandGitHub a933776436 [CLI] Validate node version (#2489) 2024-11-20 17:24:34 -08:00
Nuno Campos 588373c2d5 0.2.53 2024-11-20 17:13:42 -08:00
Nuno CamposandGitHub 267962bece Merge pull request #2491 from langchain-ai/nc/20nov/stream-putnowait-loop
lib: For subgraphs / stream modes call stream.put as a callback in the original event loop
2024-11-20 17:11:56 -08:00
Nuno CamposandGitHub 4ae29b6e2a Merge pull request #2492 from langchain-ai/wfh/accept_313
[CLI] Accept 3.13 in build
2024-11-20 17:04:15 -08:00
William Fu-Hinthorn 3c0de26914 Accept 3.13 in build 2024-11-20 16:26:17 -08:00
Nuno Campos a570662773 Lint 2024-11-20 15:43:46 -08:00
Nuno Campos 9766068896 lib: For subgraphs / stream modes call stream.put as a callback in the original event loop
- This is asynchronous, so we shouldn't use for regular writes to the output stream (ie those from PregelLoop)
- For writes from subgraphs / nodes this is fine to use, as we make no guarantees about when those show up anyway
2024-11-20 15:39:30 -08:00
Vadym BardaandGitHub 7e8eef88ca docs: small fix for tutorial (#2487) 2024-11-20 14:36:42 -05:00
Eugene YurtsevandGitHub e3e63c70c9 docs: how-to guide language changes (#2462) 2024-11-19 14:54:08 -05:00
Brace SproulandGitHub 312f0982bc Merge pull request #2476 from langchain-ai/release
(sdk-js): Release 0.0.26
2024-11-19 11:38:11 -08:00
bracesproul 153245145e (sdk-js): Release 0.0.26 2024-11-19 11:32:22 -08:00
Brace SproulandGitHub c95abd88a1 Merge pull request #2117 from langchain-ai/brace/default-assign-api-key
fix(sdk-js): Pass api key in headers by default if in env
2024-11-19 11:26:01 -08:00
Brace SproulandGitHub a2b357bed5 Merge branch 'main' into brace/default-assign-api-key 2024-11-19 11:16:55 -08:00
Brace SproulandGitHub 7090d7e9a8 Merge pull request #2471 from langchain-ai/brace/drop-trailing-slash
fix(sdk-js): remove trailing slash from url
2024-11-19 09:31:59 -08:00
bracesproul b3fa43e4a6 fix(sdk-js): remove trailing slash from url 2024-11-19 09:23:47 -08:00
Vadym BardaandGitHub b1779cf348 docs: update autogen docs (#2470) 2024-11-19 11:57:26 -05:00
Harrison ChaseandGitHub 26d18d3ca5 add how to guides for autogen integration (#2466) 2024-11-19 08:44:17 -08:00
12052d7d26 CLI docs (#2464)
Co-authored-by: Harrison Chase <hw.chase.17@gmail.com>
2024-11-19 10:57:07 -05:00
Vadym BardaandGitHub e3a30a9b69 docs: fix prompt (#2467) 2024-11-19 09:42:20 -05:00
William FHandGitHub ff1370a9a5 Release CLI (#2465) 2024-11-19 08:41:18 +00:00
William FHandGitHub 679a7365da Add default ns in put_writes (#2404) 2024-11-18 22:55:12 -08:00
b2522ffe19 CLI Dev command (#2463)
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2024-11-19 05:12:03 +00:00
Eugene YurtsevandGitHub 4212a795a0 docs[minor]: Fix layout issues in available templates (#2452) 2024-11-18 22:53:19 -05:00
Eugene YurtsevandGitHub 517d67aa32 docs: Update to use LANGSMITH_API_KEY throughout (#2461) 2024-11-18 22:51:36 -05:00
Brace SproulandGitHub feaf14765a Merge pull request #2458 from langchain-ai/brace/expose-command-interface
fix(sdk-js): Expose Command interface
2024-11-18 18:54:19 -08:00
Vadym BardaandGitHub cc6063c729 docs: simplify multi-agent tutorials (#2443) 2024-11-19 02:31:12 +00:00
013397042e docs: grammar (#2449)
Co-authored-by: Ian Sullivan <ian@frame.ai>
2024-11-18 21:01:37 -05:00
Nuno Campos 9a775d9c9f 0.2.52 2024-11-18 17:17:40 -08:00
Nuno Campos 2c945ceb68 Copy configurable in ensure_config 2024-11-18 17:17:20 -08:00
Erick FriisandGitHub 39eabd0fb8 Merge pull request #2459 from langchain-ai/erick/docs-self-hosted-plan-links
docs: self-hosted plan links
2024-11-18 16:42:02 -08:00
Erick Friis e5cc2e2044 docs: self-hosted plan links 2024-11-18 16:35:19 -08:00
bracesproul f00c0515e7 add jsdoc 2024-11-18 16:32:32 -08:00
bracesproul d87c0d4d53 fix(sdk-js): Expose Command interface 2024-11-18 16:25:51 -08:00
Nuno Campos fb40a974c8 0.2.51 2024-11-18 16:03:04 -08:00
Nuno Campos d63bfc6879 Add missing property 2024-11-18 16:02:54 -08:00
Nuno CamposandGitHub 97dd30711a Merge pull request #2437 from langchain-ai/nc/16nov/speed-up-find-subgraph
lib: find_subgraph doesn't need to look in both func and afunc
2024-11-18 15:59:58 -08:00
Nuno Campos 7866bd2718 lib: find_subgraph doesn't need to look in both func and afunc
- if they both exist they're expected to share the same implementation, so looking in both is redundant
2024-11-16 16:57:32 -08:00
bracesproul 433c382280 cr 2024-10-15 11:37:20 -07:00
bracesproul 7352ab14a2 cr 2024-10-15 11:36:37 -07:00
bracesproul 85a76912d3 fix(sdk-js): Pass api key in headers by default if in env 2024-10-15 11:33:40 -07:00
87 changed files with 4984 additions and 2055 deletions
+1 -2
View File
@@ -22,8 +22,7 @@ def test(
# check docker available
capabilities = langgraph_cli.docker.check_capabilities(runner)
# open config
with open(config) as f:
config_json = langgraph_cli.config.validate_config(json.load(f))
config_json = langgraph_cli.config.validate_config_file(config)
set("Running...")
args = [
+2
View File
@@ -88,6 +88,7 @@ jobs:
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
--check-links-ignore "https://x.com/.*" \
--check-links-ignore "https://github\.com/.*" \
--check-links-ignore "http://localhost:8123/.*" \
--check-links-ignore "/.*\.(ipynb|html)$" \
--check-links-ignore "https://python\.langchain\.com/.*" \
--check-links-ignore "https://openai\.com/.*" \
@@ -104,6 +105,7 @@ jobs:
echo "Running link check on HTML files matching changed notebook files..."
poetry run pytest -v \
--check-links-ignore "https://(api|web|docs)\.smith\.langchain\.com/.*" \
--check-links-ignore "http://localhost:8123/.*" \
--check-links-ignore "https://x.com/.*" \
--check-links-ignore "https://github\.com/.*" \
--check-links-ignore "/.*\.(ipynb|html)$" \
+1 -1
View File
@@ -238,7 +238,7 @@ final_state["messages"][-1].content
* [How-to Guides](https://langchain-ai.github.io/langgraph/how-tos/): Accomplish specific things within LangGraph, from streaming, to adding memory & persistence, to common design patterns (branching, subgraphs, etc.), these are the place to go if you want to copy and run a specific code snippet.
* [Conceptual Guides](https://langchain-ai.github.io/langgraph/concepts/high_level/): In-depth explanations of the key concepts and principles behind LangGraph, such as nodes, edges, state and more.
* [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Review important classes and methods, simple examples of how to use the graph and checkpointing APIs, higher-level prebuilt components and more.
* [Cloud (beta)](https://langchain-ai.github.io/langgraph/cloud/): With one click, deploy LangGraph applications to LangGraph Cloud.
* [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform): LangGraph Platform is a commercial solution for deploying agentic applications in production, built on the open-source LangGraph framework.
## Contributing
+2 -1
View File
@@ -36,10 +36,11 @@ NOTEBOOKS_NO_EXECUTION = [
"docs/docs/tutorials/rag/langgraph_self_rag_local.ipynb",
# this loads a massive dataset from gcp
"docs/docs/tutorials/usaco/usaco.ipynb",
# TODO: figure out why autogen notebook is not runnable (they are just hanging. possible due to code execution?)
"docs/docs/how-tos/autogen-integration.ipynb",
# TODO: need to update these notebooks to make sure they are runnable in CI
"docs/docs/tutorials/storm/storm.ipynb", # issues only when running with VCR
"docs/docs/tutorials/lats/lats.ipynb", # issues only when running with VCR
"docs/docs/tutorials/multi_agent/hierarchical_agent_teams.ipynb", # taking a very long time to run
"docs/docs/tutorials/rag/langgraph_crag.ipynb", # flakiness from tavily
"docs/docs/tutorials/rag/langgraph_adaptive_rag.ipynb", # Cannot create a consistent method resolution error from VCR
"docs/docs/how-tos/map-reduce.ipynb" # flakiness from structured output, only when running with VCR
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6 -6
View File
@@ -21,7 +21,7 @@ Install the proper packages:
Ensure you have an API key, which you can create from the [LangSmith UI](https://smith.langchain.com) (Settings > API Keys). This is required to authenticate that you have LangGraph Cloud access. After you have saved the key to a safe place, place the following line in your `.env` file:
```python
LANGCHAIN_API_KEY = *********
LANGSMITH_API_KEY = *********
```
## Start the API server
@@ -54,7 +54,7 @@ You can either initialize by passing authentication or by setting an environment
from langgraph_sdk import get_client
# only pass the url argument to get_client() if you changed the default port when calling langgraph up
client = get_client(url=<DEPLOYMENT_URL>,api_key=<LANGCHAIN_API_KEY>)
client = get_client(url=<DEPLOYMENT_URL>,api_key=<LANGSMITH_API_KEY>)
# Using the graph deployed with the name "agent"
assistant_id = "agent"
thread = await client.threads.create()
@@ -66,7 +66,7 @@ You can either initialize by passing authentication or by setting an environment
import { Client } from "@langchain/langgraph-sdk";
// only set the apiUrl if you changed the default port when calling langgraph up
const client = new Client({ apiUrl: <DEPLOYMENT_URL>, apiKey: <LANGCHAIN_API_KEY> });
const client = new Client({ apiUrl: <DEPLOYMENT_URL>, apiKey: <LANGSMITH_API_KEY> });
// Using the graph deployed with the name "agent"
const assistantId = "agent";
const thread = await client.threads.create();
@@ -78,13 +78,13 @@ You can either initialize by passing authentication or by setting an environment
curl --request POST \
--url <DEPLOYMENT_URL>/threads \
--header 'Content-Type: application/json'
--header 'x-api-key: <LANGCHAIN_API_KEY>'
--header 'x-api-key: <LANGSMITH_API_KEY>'
```
#### Initialize with environment variables
If you have a `LANGCHAIN_API_KEY` set in your environment, you do not need to explicitly pass authentication to the client
If you have a `LANGSMITH_API_KEY` set in your environment, you do not need to explicitly pass authentication to the client
=== "Python"
@@ -154,7 +154,7 @@ Now we can invoke our graph to ensure it is working. Make sure to change the inp
}
```
=== "CURL"
=== "CURL"
```bash
curl --request POST \
+3 -3
View File
@@ -8,9 +8,9 @@ If you want to learn how to build an agent like this from scratch, take a look a
This tutorial will use:
- Anthropic for the LLM - sign up and get an API key [here](https://console.anthropic.com/)
- Tavily for the search engine - sign up and get an API key [here](https://app.tavily.com/)
- LangSmith for hosting - sign up and get an API key [here](https://smith.langchain.com/)
- Anthropic for the LLM - sign up and get an API key [here](https://console.anthropic.com/).
- Tavily for the search engine - sign up and get an API key [here](https://app.tavily.com/).
- LangSmith for hosting - sign up and get an API key [here](https://smith.langchain.com/).
## Create and configure your app
+37 -9
View File
@@ -53,7 +53,7 @@ Example:
}
```
Example:
Example with environment variables:
```json
{
@@ -78,6 +78,37 @@ The base command for the LangGraph CLI is `langgraph`.
langgraph [OPTIONS] COMMAND [ARGS]
```
### `dev`
Run LangGraph API server in development mode with hot reloading and debugging capabilities. This lightweight server requires no Docker installation and is suitable for development and testing. State is persisted to a local directory.
**Installation**
This command requires the "inmem" extra to be installed:
```bash
pip install -U "langgraph-cli[inmem]"
```
**Usage**
```
langgraph dev [OPTIONS]
```
**Options**
| Option | Default | Description |
|----------------------------|------------------|--------------------------------------------------------------------------------------------|
| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables |
| `--host TEXT` | `127.0.0.1` | Host to bind the server to |
| `--port INTEGER` | `2024` | Port to bind the server to |
| `--no-reload` | | Disable auto-reload |
| `--n-jobs-per-worker INTEGER` | | Number of jobs per worker. Default is 10 |
| `--no-browser` | | Disable automatic browser opening |
| `--debug-port INTEGER` | | Port for debugger to listen on |
| `--help` | | Display command documentation |
### `build`
Build LangGraph Cloud API server Docker image.
@@ -100,7 +131,7 @@ langgraph build [OPTIONS]
### `up`
Start langgraph API server. For local testing, requires a LangSmith API key with access to LangGraph Cloud closed beta. Requires a license key for production use.
Start LangGraph API server. For local testing, requires a LangSmith API key with access to LangGraph Cloud closed beta. Requires a license key for production use.
**Usage**
@@ -120,8 +151,8 @@ langgraph up [OPTIONS]
| `--verbose` | | Show more output from the server logs. |
| `-c, --config FILE` | `langgraph.json` | Path to configuration file declaring dependencies, graphs and environment variables. |
| `-d, --docker-compose FILE` | | Path to docker-compose.yml file with additional services to launch. |
| `-p, --port INTEGER` | `8123` | Port to expose. Example: `langgraph test --port 8000` |
| `--pull / --no-pull` | `pull` | Pull latest images. Use --no-pull for running the server with locally-built images. Example: `langgraph up --no-pull` |
| `-p, --port INTEGER` | `8123` | Port to expose. Example: `langgraph up --port 8000` |
| `--pull / --no-pull` | `pull` | Pull latest images. Use `--no-pull` for running the server with locally-built images. Example: `langgraph up --no-pull` |
| `--recreate / --no-recreate` | `no-recreate` | Recreate containers even if their configuration and image haven't changed |
| `--help` | | Display command documentation. |
@@ -148,9 +179,9 @@ Example:
langgraph dockerfile -c langgraph.json Dockerfile
```
Would generate something like the following:
This generates a Dockerfile that looks similar to:
```text
```dockerfile
FROM langchain/langgraph-api:3.11
ADD ./pipconf.txt /pipconfig.txt
@@ -170,6 +201,3 @@ RUN set -ex && \
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph", "storm": "/deps/__outer_graphs/src/storm.py:graph"}'
```
You can then customize, build images, push, and deploy from this file.
+2 -2
View File
@@ -2,13 +2,13 @@
LLMs are extremely powerful, particularly when connected to other systems such as a retriever or APIs. This is why many LLM applications use a control flow of steps before and / or after LLM calls. As an example [RAG](https://github.com/langchain-ai/rag-from-scratch) performs retrieval of relevant documents to a question, and passes those documents to an LLM in order to ground the response. Often a control flow of steps before and / or after an LLM is called a "chain." Chains are a popular paradigm for programming with LLMs and offer a high degree of reliability; the same set of steps runs with each chain invocation.
However, we often want LLM systems that can pick their own control flow! This is one definition of an [agent](https://blog.langchain.dev/what-is-an-agent/): an agent is a system that uses an LLM to decide the control flow of an application. Unlike a chain, an agent given an LLM some degree of control over the sequence of steps in the application. Examples of using an LLM to decide the control of an application:
However, we often want LLM systems that can pick their own control flow! This is one definition of an [agent](https://blog.langchain.dev/what-is-an-agent/): an agent is a system that uses an LLM to decide the control flow of an application. Unlike a chain, an agent gives an LLM some degree of control over the sequence of steps in the application. Examples of using an LLM to decide the control of an application:
- Using an LLM to route between two potential paths
- Using an LLM to decide which of many tools to call
- Using an LLM to decide whether the generated answer is sufficient or more work is need
There are many different types of [agent architectures](https://blog.langchain.dev/what-is-a-cognitive-architecture/) to consider, which given an LLM varying levels of control. On one extreme, a router allows an LLM to select a single step from a specified set of options and, on the other extreme, a fully autonomous long-running agent may have complete freedom to select any sequence of steps that it wants for a given problem.
There are many different types of [agent architectures](https://blog.langchain.dev/what-is-a-cognitive-architecture/) to consider, which give an LLM varying levels of control. On one extreme, a router allows an LLM to select a single step from a specified set of options and, on the other extreme, a fully autonomous long-running agent may have complete freedom to select any sequence of steps that it wants for a given problem.
![Agent Types](img/agent_types.png)
+1 -1
View File
@@ -30,7 +30,7 @@ The conceptual guide does not cover step-by-step instructions or specific implem
- [Streaming](streaming.md): Streaming is crucial for enhancing the responsiveness of applications built on LLMs. By displaying output progressively, even before a complete response is ready, streaming significantly improves user experience (UX), particularly when dealing with the latency of LLMs.
- [FAQ](faq.md): Frequently asked questions about LangGraph.
## LangGraph Platform
## LangGraph Platform
LangGraph Platform is a commercial solution for deploying agentic applications in production, built on the open-source LangGraph framework.
+20 -1
View File
@@ -28,9 +28,28 @@ The CLI provides the following core functionality:
The `langgraph build` command builds a Docker image for the [LangGraph API server](./langgraph_server.md) that can be directly deployed.
### `dev`
!!! note "New in version 0.1.55"
The `langgraph dev` command was introduced in langgraph-cli version 0.1.55.
The `langgraph dev` command starts a lightweight development server that requires no Docker installation. This server is ideal for rapid development and testing, with features like:
- Hot reloading: Changes to your code are automatically detected and reloaded
- Debugger support: Attach your IDE's debugger for line-by-line debugging
- In-memory state with local persistence: Server state is stored in memory for speed but persisted locally between restarts
To use this command, you need to install the CLI with the "inmem" extra:
```bash
pip install -U "langgraph-cli[inmem]"
```
**Note**: This command is intended for local development and testing only. It is not recommended for production use. Since it does not use Docker, we recommend using virtual environments to manage your project's dependencies.
### `up`
The `langgraph up` command starts an instance of the [LangGraph API server](./langgraph_server.md) locally. This requires docker to be installed and running locally. It also requires a LangSmith API key for local development or a license key for production use.
The `langgraph up` command starts an instance of the [LangGraph API server](./langgraph_server.md) locally in a docker container. This requires thedocker server to be running locally. It also requires a LangSmith API key for local development or a license key for production use.
The server includes all API endpoints for your graph's runs, threads, assistants, etc. as well as the other services required to run your agent, including a managed database for checkpointing and storage.
+8
View File
@@ -14,6 +14,13 @@ A **deployment** is an instance of a LangGraph API. A single deployment can have
See the [how-to guide](../cloud/deployment/cloud.md#create-new-deployment) for creating a new deployment.
## Resource Allocation
| **Deployment Type** | **CPU** | **Memory** | **Scaling** |
|---------------------|---------|------------|---------------------|
| Development | 1 CPU | 1 GB | Up to 1 container |
| Production | 1 CPU | 2 GB | Up to 10 containers |
## Revision
A revision is an iteration of a [deployment](#deployment). When a new deployment is created, an initial revision is automatically created. To deploy new code changes or update environment variable configurations for a deployment, a new revision must be created. When a revision is created, a new container image is built automatically.
@@ -33,6 +40,7 @@ A high-level diagram of a Cloud SaaS deployment.
![diagram](img/langgraph_cloud_architecture.png)
## Related
- [Deployment Options](./deployment_options.md)
+10
View File
@@ -35,6 +35,16 @@ While in Beta, LangGraph Studio is available for free to all [LangSmith](https:/
If you have deployed your LangGraph application on LangGraph Platform (Cloud), you can access the studio as part of that
### Development server
LangGraph CLI also contains a command for running an in-memory development server that can be used to connect a local LangGraph app with the studio.
See [instructions here](../cloud/reference/cli.md#dev) for more information.
The way this works is that it runs inside your local environment.
It will spin up an in-memory, development server to deploy the graph.
You can then connect to the studio via the Cloud hosted version of LangGraph Platform.
To be clear, the web studio will connect to your locally running server - your agent is still running locally and never leaves your device.
## Studio FAQs
### Why is my project failing to start?
+5 -1
View File
@@ -7,7 +7,7 @@
## Versions
There are two versions of the self hosted deployment: [Self-Hosted Enterprise](./deployment_options.md#self-hosted-enterprise) and [Self-Hosted Lite](./deployment_options.md#self-hosted-lite).
There are two versions of the self-hosted deployment: [Self-Hosted Enterprise](./deployment_options.md#self-hosted-enterprise) and [Self-Hosted Lite](./deployment_options.md#self-hosted-lite).
### Self-Hosted Lite
@@ -34,6 +34,10 @@ To use the Self-Hosted Enterprise version, you must acquire a license key that y
For step-by-step instructions, see [How to set up a self-hosted deployment of LangGraph](../how-tos/deploy-self-hosted.md).
## Helm Chart
If you would like to deploy LangGraph Cloud on Kubernetes, you can use this [Helm chart](https://github.com/langchain-ai/helm/blob/main/charts/langgraph-cloud/README.md).
## Related
- [How to set up a self-hosted deployment of LangGraph](../how-tos/deploy-self-hosted.md).
+8 -16
View File
@@ -6,22 +6,14 @@
Templates are open source reference applications designed to help you get started quickly when building with LangGraph. They provide working examples of common agentic workflows that can be customized to your needs.
Templates can be accessed via [LangGraph Studio](langgraph_studio.md), or cloned directly from Github. You can download LangGraph Studio and see available templates [here](https://studio.langchain.com/).
Templates can be accessed via [LangGraph Studio (macOS only)](langgraph_studio.md), or cloned directly from Github. You can download LangGraph Studio and see available templates [here](https://studio.langchain.com/).
## Available templates
- **New LangGraph Project**: A simple, minimal chatbot with memory.
- [Python](https://github.com/langchain-ai/new-langgraph-project)
- [JS/TS](https://github.com/langchain-ai/new-langgraphjs-project)
- **ReAct Agent**: A simple agent that can be flexibly extended to many tools.
- [Python](https://github.com/langchain-ai/react-agent)
- [JS/TS](https://github.com/langchain-ai/react-agent-js)
- **Memory Agent**: A ReAct-style agent with an additional tool to store memories for use across conversational threads.
- [Python](https://github.com/langchain-ai/memory-agent)
- [JS/TS](https://github.com/langchain-ai/memory-agent-js)
- **Retrieval Agent**: An agent that includes a retrieval-based question-answering system.
- [Python](https://github.com/langchain-ai/retrieval-agent-template)
- [JS/TS](https://github.com/langchain-ai/retrieval-agent-template-js)
- **Data-enrichment Agent**: An agent that performs web searches and organizes its findings into a structured format.
- [Python](https://github.com/langchain-ai/data-enrichment)
- [JS/TS](https://github.com/langchain-ai/data-enrichment-js)
| Template | Description | Python | JS/TS |
|---------------------------|------------------------------------------------------------------------------------------|------------------------------------------------------------------|---------------------------------------------------------------------|
| **New LangGraph Project** | A simple, minimal chatbot with memory. | [Repo](https://github.com/langchain-ai/new-langgraph-project) | [Repo](https://github.com/langchain-ai/new-langgraphjs-project) |
| **ReAct Agent** | A simple agent that can be flexibly extended to many tools. | [Repo](https://github.com/langchain-ai/react-agent) | [Repo](https://github.com/langchain-ai/react-agent-js) |
| **Memory Agent** | A ReAct-style agent with an additional tool to store memories for use across threads. | [Repo](https://github.com/langchain-ai/memory-agent) | [Repo](https://github.com/langchain-ai/memory-agent-js) |
| **Retrieval Agent** | An agent that includes a retrieval-based question-answering system. | [Repo](https://github.com/langchain-ai/retrieval-agent-template) | [Repo](https://github.com/langchain-ai/retrieval-agent-template-js) |
| **Data-Enrichment Agent** | An agent that performs web searches and organizes its findings into a structured format. | [Repo](https://github.com/langchain-ai/data-enrichment) | [Repo](https://github.com/langchain-ai/data-enrichment-js) |
File diff suppressed because one or more lines are too long
@@ -0,0 +1,171 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "8381b6e0-29a6-48c5-b451-5d2549351249",
"metadata": {},
"source": [
"# How to use LangGraph Platform to deploy CrewAI, AutoGen, and other frameworks\n",
"\n",
"[LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) provides infrastructure for deploying agents. This integrates seamlessly with LangGraph, but can also work with other frameworks. The way to make this work is to wrap the agent in a single LangGraph node, and have that be the entire graph.\n",
"\n",
"Doing so will allow you to deploy to LangGraph Platform, and allows you to get a lot of the [benefits](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/). You get horizontally scalable infrastructure, a task queue to handle bursty operations, a persistence layer to power short term memory, and long term memory support.\n",
"\n",
"In this guide we show how to do this with an AutoGen agent, but this method should work for agents defined in other frameworks like CrewAI, LlamaIndex, and others as well."
]
},
{
"cell_type": "markdown",
"id": "1113cb16-b538-448c-924c-85731ce96ebd",
"metadata": {},
"source": [
"## Setup"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "f05993fa-9d03-4f45-bc13-0a8d87260d86",
"metadata": {
"scrolled": true
},
"outputs": [],
"source": [
"%pip install autogen langgraph"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f4e0ca12-1714-4776-a30a-9527e519799b",
"metadata": {},
"outputs": [],
"source": [
"import getpass\n",
"import os\n",
"\n",
"\n",
"def _set_env(var: str):\n",
" if not os.environ.get(var):\n",
" os.environ[var] = getpass.getpass(f\"{var}: \")\n",
"\n",
"\n",
"_set_env(\"OPENAI_API_KEY\")"
]
},
{
"cell_type": "markdown",
"id": "1926bbc3-6b06-41e0-9604-860a2bbf8fa3",
"metadata": {},
"source": [
"## Define autogen agent\n",
"\n",
"Here we define our AutoGen agent. From https://github.com/microsoft/autogen/blob/0.2/notebook/agentchat_web_info.ipynb"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d4a14dc7-d565-4207-8788-525f85b9fb27",
"metadata": {},
"outputs": [],
"source": [
"import autogen\n",
"import os\n",
"\n",
"config_list = [{\"model\": \"gpt-4o\", \"api_key\": os.environ[\"OPENAI_API_KEY\"]}]\n",
"\n",
"llm_config = {\n",
" \"timeout\": 600,\n",
" \"cache_seed\": 42,\n",
" \"config_list\": config_list,\n",
" \"temperature\": 0,\n",
"}\n",
"\n",
"autogen_agent = autogen.AssistantAgent(\n",
" name=\"assistant\",\n",
" llm_config=llm_config,\n",
")\n",
"\n",
"user_proxy = autogen.UserProxyAgent(\n",
" name=\"user_proxy\",\n",
" human_input_mode=\"NEVER\",\n",
" max_consecutive_auto_reply=10,\n",
" is_termination_msg=lambda x: x.get(\"content\", \"\").rstrip().endswith(\"TERMINATE\"),\n",
" code_execution_config={\n",
" \"work_dir\": \"web\",\n",
" \"use_docker\": False,\n",
" }, # Please set use_docker=True if docker is available to run the generated code. Using docker is safer than running the generated code directly.\n",
" llm_config=llm_config,\n",
" system_message=\"Reply TERMINATE if the task has been solved at full satisfaction. Otherwise, reply CONTINUE, or the reason why the task is not solved yet.\",\n",
")"
]
},
{
"cell_type": "markdown",
"id": "b1170836-f23e-4e4c-ab83-ce791cd7fbd2",
"metadata": {},
"source": [
"## Wrap in LangGraph\n",
"\n",
"We now wrap the AutoGen agent in a single LangGraph node, and make that the entire graph.\n",
"The main thing this involves is defining an Input and Output schema for the node, which you would need to do if deploying this manually, so it's no extra work"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "7b417c16-ff4e-4d5c-a9a9-0aaeeef6ede5",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import StateGraph, MessagesState\n",
"\n",
"\n",
"def call_autogen_agent(state: MessagesState):\n",
" last_message = state[\"messages\"][-1]\n",
" response = user_proxy.initiate_chat(autogen_agent, message=last_message.content)\n",
" # get the final response from the agent\n",
" content = response.chat_history[-1][\"content\"]\n",
" return {\"messages\": {\"role\": \"assistant\", \"content\": content}}\n",
"\n",
"\n",
"graph = StateGraph(MessagesState)\n",
"graph.add_node(call_autogen_agent)\n",
"graph.set_entry_point(\"call_autogen_agent\")\n",
"graph = graph.compile()"
]
},
{
"cell_type": "markdown",
"id": "f6a18377-ac29-478f-a76a-b213f1a3c85d",
"metadata": {},
"source": [
"## Deploy with LangGraph Platform\n",
"\n",
"You can now deploy this as you normally would with LangGraph Platform. See [these instructions](https://langchain-ai.github.io/langgraph/concepts/deployment_options/) for more details."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
+1 -1
View File
@@ -345,7 +345,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
"version": "3.11.1"
}
},
"nbformat": 4,
+7 -3
View File
@@ -17,14 +17,18 @@ You will need to do the following:
2. Build a docker image with the [LangGraph Server](../concepts/langgraph_server.md) using the [LangGraph CLI](../concepts/langgraph_cli.md).
3. Deploy a web server that will run the docker image and pass in the necessary environment variables.
## Helm Chart
If you would like to deploy LangGraph Cloud on Kubernetes, you can use this [Helm chart](https://github.com/langchain-ai/helm/blob/main/charts/langgraph-cloud/README.md).
## Environment Variables
You will eventually need to pass in the following environment variables to the LangGraph Deploy server:
- `REDIS_URI`: Connection details to a Redis instance. Redis will be used as a pub-sub broker to enable streaming real time output from background runs.
- `DATABASE_URI`: Postgres connection details. Postgres will be used to store assistants, threads, runs, persist thread state and long term memory, and to manage the state of the background task queue with 'exactly once' semantics.
- `LANGSMITH_API_KEY`: (If using [Self-Hosted Lite]) LangSmith API key. This will be used to authenticate ONCE at server start up.
- `LANGGRAPH_CLOUD_LICENSE_KEY`: (If using Self-Hosted Enterprise) LangGraph Platform license key. This will be used to authenticate ONCE at server start up.
- `LANGSMITH_API_KEY`: (If using [Self-Hosted Lite](../concepts/deployment_options.md#self-hosted-lite)) LangSmith API key. This will be used to authenticate ONCE at server start up.
- `LANGGRAPH_CLOUD_LICENSE_KEY`: (If using [Self-Hosted Enterprise](../concepts/deployment_options.md#self-hosted-enterprise)) LangGraph Platform license key. This will be used to authenticate ONCE at server start up.
## Build the Docker Image
@@ -70,7 +74,7 @@ If you want to run this quickly without setting up a separate Redis and Postgres
* You need to replace `my-image` with the name of the image you built in the previous step (from `langgraph build`).
and you should provide appropriate values for `REDIS_URI`, `DATABASE_URI`, and `LANGSMITH_API_KEY`.
* If your application requires additional environment variables, you can pass them in a similar way.
* If using Self-Hosted Enterprise, you must provide `LANGGRAPH_CLOUD_LICENSE_KEY` as an additional environment variable.
* If using [Self-Hosted Enterprise](../concepts/deployment_options.md#self-hosted-enterprise), you must provide `LANGGRAPH_CLOUD_LICENSE_KEY` as an additional environment variable.
### Using Docker Compose
+6 -3
View File
@@ -103,6 +103,7 @@ These how-to guides show common patterns for tool calling with LangGraph:
- [How to force function calling agent to structure output](react-agent-structured-output.ipynb)
- [How to pass custom LangSmith run ID for graph runs](run-id-langsmith.ipynb)
- [How to return state before hitting recursion limit](return-when-recursion-limit-hits.ipynb)
- [How to integrate LangGraph with AutoGen, CrewAI, and other frameworks](autogen-integration.ipynb)
### Prebuilt ReAct Agent
@@ -141,6 +142,7 @@ Learn how to set up your app for deployment to LangGraph Platform:
- [How to customize Dockerfile](../cloud/deployment/custom_docker.md)
- [How to test locally](../cloud/deployment/test_locally.md)
- [How to rebuild graph at runtime](../cloud/deployment/graph_rebuild.md)
- [How to use LangGraph Platform to deploy CrewAI, AutoGen, and other frameworks](autogen-langgraph-platform.ipynb)
### Deployment
@@ -149,6 +151,7 @@ LangGraph applications can be deployed using LangGraph Cloud, which provides a r
- [How to deploy to LangGraph cloud](../cloud/deployment/cloud.md)
- [How to deploy to a self-hosted environment](./deploy-self-hosted.md)
- [How to interact with the deployment using RemoteGraph](./use-remote-graph.md)
### Assistants
[Assistants](../concepts/assistants.md) is a configured instance of a template.
@@ -163,7 +166,7 @@ LangGraph applications can be deployed using LangGraph Cloud, which provides a r
### Runs
LangGraph Cloud supports multiple types of runs besides streaming runs.
LangGraph Platform supports multiple types of runs besides streaming runs.
- [How to run an agent in the background](../cloud/how-tos/background_run.md)
- [How to run multiple agents in the same thread](../cloud/how-tos/same-thread.md)
@@ -183,7 +186,7 @@ Streaming the results of your LLM application is vital for ensuring a good user
### Human-in-the-loop
When creating complex graphs, leaving every decision up to the LLM can be dangerous, especially when the decisions involve invoking certain tools or accessing specific documents. To remedy this, LangGraph allows you to insert human-in-the-loop behavior to ensure your graph does not have undesired outcomes. Read more about the different ways you can add human-in-the-loop capabilities to your LangGraph Cloud projects in these how-to guides:
When designing complex graphs, relying entirely on the LLM for decision-making can be risky, particularly when it involves tools that interact with files, APIs, or databases. These interactions may lead to unintended data access or modifications, depending on the use case. To mitigate these risks, LangGraph allows you to integrate human-in-the-loop behavior, ensuring your LLM applications operate as intended without undesirable outcomes.
- [How to add a breakpoint](../cloud/how-tos/human_in_the_loop_breakpoint.md)
- [How to wait for user input](../cloud/how-tos/human_in_the_loop_user_input.md)
@@ -193,7 +196,7 @@ When creating complex graphs, leaving every decision up to the LLM can be danger
### Double-texting
Graph execution can take a while, and sometimes users may change their mind about the input they wanted to send before their original input has finished running. For example, a user might notice a typo in their original request and will edit the prompt and resend it. Deciding what to do in these cases is important for ensuring a smooth user experience and preventing your graphs from behaving in unexpected ways. The following how-to guides provide information on the various options LangGraph Cloud gives you for dealing with double-texting:
Graph execution can take a while, and sometimes users may change their mind about the input they wanted to send before their original input has finished running. For example, a user might notice a typo in their original request and will edit the prompt and resend it. Deciding what to do in these cases is important for ensuring a smooth user experience and preventing your graphs from behaving in unexpected ways.
- [How to use the interrupt option](../cloud/how-tos/interrupt_concurrent.md)
- [How to use the rollback option](../cloud/how-tos/rollback_concurrent.md)
+86
View File
@@ -0,0 +1,86 @@
# How to connect a local agent to LangGraph Studio
This guide shows you how to connect your local agent to [LangGraph Studio](../concepts/langgraph_studio.md) for visualization, interaction, and debugging.
## Connection Options
There are two ways to connect your local agent to LangGraph Studio:
- [LangGraph Desktop](../concepts/langgraph_studio.md#desktop-app): Application, Mac only, requires Docker
- [Development Server](../concepts/langgraph_studio.md#dev-server): Python package, all platforms, no Docker
In this guide we will cover how to use the development server as that is generally an easier and better experience.
## Setup your application
First, you will need to setup your application in the proper format.
This means defining a `langgraph.json` file which contains paths to your agent(s).
See [this guide](../concepts/application_structure.md) for information on how to do so.
## Install langgraph-cli
You will need to install [`langgraph-cli`](../cloud/reference/cli.md#langgraph-cli) (version `0.1.55` or higher).
You will need to make sure to install the `inmem` extras.
```shell
pip install "langgraph-cli[inmem]==0.1.55"
```
## Run the development server
1. Navigate to your project directory (where `langgraph.json` is located)
2. Start the server:
```bash
langgraph dev
```
This will look for the `langgraph.json` file in your current directory.
In there, it will find the paths to the graph(s), and start those up.
It will then automatically connect to the cloud-hosted studio.
## Use the studio
After connecting to the studio, a browser window should automatically pop up.
This will use the cloud hosted studio UI to connect to your local development server.
Your graph is still running locally, the UI is connecting to visualizing the agent and threads that are defined locally.
The graph will always use the most up-to-date code, so you will be able to change the underlying code and have it automatically reflected in the studio.
This is useful for debugging workflows.
You can run your graph in the UI until it messes up, go in and change your code, and then rerun from the node that failed.
# (Optional) Attach a debugger
For step-by-step debugging with breakpoints and variable inspection:
```bash
# Install debugpy package
pip install debugpy
# Start server with debugging enabled
langgraph dev --debug-port 5678
```
Then attach your preferred debugger:
=== "VS Code"
Add this configuration to `launch.json`:
```json
{
"name": "Attach to LangGraph",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "0.0.0.0",
"port": 5678
}
}
```
Specify the port number you chose in the previous step.
=== "PyCharm"
1. Go to Run → Edit Configurations
2. Click + and select "Python Debug Server"
3. Set IDE host name: `localhost`
4. Set port: `5678` (or the port number you chose in the previous step)
5. Click "OK" and start debugging
+10 -12
View File
@@ -6,25 +6,23 @@ title: Tutorials
# Tutorials
Welcome to the LangGraph Tutorials! These notebooks introduce LangGraph through building various language agents and applications.
New to LangGraph or LLM app development? Read this material to get up and running building your first applications.
## Quick Start
## Get Started 🚀 {#quick-start}
Learn the basics of LangGraph through a comprehensive quick start in which you will build an agent from scratch.
- [LangGraph Quickstart](introduction.ipynb): Build a chatbot that can use tools and keep track of conversation history. Add human-in-the-loop capabilities and explore how time-travel works.
- [LangGraph Server Quickstart](langgraph-platform/local-server.md): Launch a LangGraph server locally and interact with it using the REST API and LangGraph Studio Web UI.
- [LangGraph Cloud QuickStart](../cloud/quick_start.md): Deploy a LangGraph app using LangGraph Cloud.
- [Quick Start](introduction.ipynb): In this tutorial, you will build a support chatbot using LangGraph.
- [LangGraph Cloud Quick Start](../cloud/quick_start.md): In this tutorial, you will build and deploy an agent to LangGraph Cloud.
## Use cases 🛠️
## Use cases
Learn from example implementations of graphs designed for specific scenarios and that implement common design patterns.
Explore practical implementations tailored for specific scenarios:
### Chatbots
- [Customer Support](customer-support/customer-support.ipynb): Build a customer support chatbot to manage flights, hotel reservations, car rentals, and other tasks
- [Prompt Generation from User Requirements](chatbots/information-gather-prompting.ipynb): Build an information gathering chatbot
- [Code Assistant](code_assistant/langgraph_code_assistant.ipynb): Build a code analysis and generation assistant
- [Customer Support](customer-support/customer-support.ipynb): Build a multi-functional support bot for flights, hotels, and car rentals.
- [Prompt Generation from User Requirements](chatbots/information-gather-prompting.ipynb): Build an information gathering chatbot.
- [Code Assistant](code_assistant/langgraph_code_assistant.ipynb): Build a code analysis and generation assistant.
### RAG
+47 -32
View File
@@ -5,17 +5,17 @@
"id": "4a1aae78-88a6-4133-b905-7e46c8e3772f",
"metadata": {},
"source": [
"# LangGraph Quick Start\n",
"# 🚀 LangGraph Quick Start\n",
"\n",
"In this comprehensive quick start, we will build a support chatbot in LangGraph that can:\n",
"In this tutorial, we will build a support chatbot in LangGraph that can:\n",
"\n",
"- Answer common questions by searching the web\n",
"- Maintain conversation state across calls\n",
"- Route complex queries to a human for review\n",
"- Use custom state to control its behavior\n",
"- Rewind and explore alternative conversation paths\n",
"✅ **Answer common questions** by searching the web \n",
"✅ **Maintain conversation state** across calls \n",
"✅ **Route complex queries** to a human for review \n",
"✅ **Use custom state** to control its behavior \n",
"✅ **Rewind and explore** alternative conversation paths \n",
"\n",
"We'll start with a basic chatbot and progressively add more sophisticated capabilities, introducing key LangGraph concepts along the way.\n",
"We'll start with a **basic chatbot** and progressively add more sophisticated capabilities, introducing key LangGraph concepts along the way. Lets dive in! 🌟\n",
"\n",
"## Setup\n",
"\n",
@@ -38,7 +38,7 @@
"id": "a6d1e870-1bc0-4d44-86c0-96681ccf6113",
"metadata": {},
"source": [
"Next, set your API keys:"
"In this tutorial, we'll be "
]
},
{
@@ -120,27 +120,24 @@
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "31c755cd-8994-4867-bdff-96a55d7beae7",
"id": "c08c41da-0855-49d3-9a3d-b7eb94413367",
"metadata": {},
"source": [
"<div class=\"admonition tip\">\n",
" <p class=\"admonition-title\">Note</p>\n",
" <p>\n",
" The first thing you do when you define a graph is define the <code>State</code> of the graph. The <code>State</code> consists of the schema of the graph as well as <a href=\"https://langchain-ai.github.io/langgraph/concepts/low_level/#reducers\">reducer functions</a> which specify how to apply updates to the state. In our example <code>State</code> is a <code>TypedDict</code> with a single key: <code>messages</code>. The <code>messages</code> key is annotated with the <a href=\"https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages\"><code>add_messages</code></a> reducer function, which tells LangGraph to append new messages to the existing list, rather than overwriting it. State keys without an annotation will be overwritten by each update, storing the most recent value. Check out <a href=\"https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages\">this conceptual guide</a> to learn more about state, reducers and other low-level concepts.\n",
" </p>\n",
"</div>"
]
},
{
"cell_type": "markdown",
"id": "4137feed-746e-4c72-a34a-f7a699ad5dcf",
"metadata": {},
"source": [
"So now our graph knows two things:\n",
"Our graph can now handle two key tasks:\n",
"\n",
"1. Each `node` can receive the current `State` as input and output an update to the state.\n",
"2. Updates to `messages` will be appended to the existing list rather than overwriting it, thanks to the prebuilt [`add_messages`](https://langchain-ai.github.io/langgraph/reference/graphs/?h=add+messages#add_messages) function used with the `Annotated` syntax.\n",
"\n",
"------\n",
"\n",
"!!! tip \"Concept\"\n",
"\n",
" When defining a graph, the first step is to define its `State`. The `State` includes the graph's schema and [reducer functions](https://langchain-ai.github.io/langgraph/concepts/low_level/#reducers) that handle state updates. In our example, `State` is a `TypedDict` with one key: `messages`. The [`add_messages`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages) reducer function is used to append new messages to the list instead of overwriting it. Keys without a reducer annotation will overwrite previous values. Learn more about state, reducers, and related concepts in [this guide](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.message.add_messages).\n",
"\n",
"---------\n",
"\n",
"1. Every `node` we define will receive the current `State` as input and return a value that updates that state.\n",
"2. `messages` will be _appended_ to the current list, rather than directly overwritten. This is communicated via the prebuilt [`add_messages`](https://langchain-ai.github.io/langgraph/reference/graphs/?h=add+messages#add_messages) function in the `Annotated` syntax.\n",
"\n",
"Next, add a \"`chatbot`\" node. Nodes represent units of work. They are typically regular python functions."
]
@@ -365,7 +362,7 @@
"id": "f22c5d4a-3134-413c-81fe-dd9752fbeb66",
"metadata": {},
"source": [
"## Part 2: Enhancing the Chatbot with Tools\n",
"## Part 2: 🛠️ Enhancing the Chatbot with Tools\n",
"\n",
"To handle queries our chatbot can't answer \"from memory\", we'll integrate a web search tool. Our bot can use this tool to find relevant information and provide better responses.\n",
"\n",
@@ -2046,7 +2043,7 @@
"\n",
"So far, we've relied on a simple state (it's just a list of messages!). You can go far with this simple state, but if you want to define complex behavior without relying on the message list, you can add additional fields to the state. In this section, we will extend our chat bot with a new node to illustrate this.\n",
"\n",
"In the examples above, we involved a human deterministically: the graph __always__ interrupted whenever an tool was invoked. Suppose we wanted our chat bot to have the choice of relying on a human.\n",
"In the examples above, we involved a human deterministically: the graph __always__ interrupted whenever a tool was invoked. Suppose we wanted our chat bot to have the choice of relying on a human.\n",
"\n",
"One way to do this is to create a passthrough \"human\" node, before which the graph will always stop. We will only execute this node if the LLM invokes a \"human\" tool. For our convenience, we will include an \"ask_human\" flag in our graph state that we will flip if the LLM calls this tool.\n",
"\n",
@@ -3136,11 +3133,29 @@
"id": "e584d57f-5aad-4507-815f-0b2e4b64b791",
"metadata": {},
"source": [
"## Conclusion\n",
"## Next Steps\n",
"\n",
"Congrats! You've completed the intro tutorial and built a chat bot in LangGraph that supports tool calling, persistent memory, human-in-the-loop interactivity, and even time-travel!\n",
"Take your journey further by exploring deployment and advanced features:\n",
"\n",
"The [LangGraph documentation](https://langchain-ai.github.io/langgraph/) is a great resource for diving deeper into the library's capabilities."
"### Server Quickstart\n",
"\n",
"- **[LangGraph Server Quickstart](../langgraph-platform/local-server)**: Launch a LangGraph server locally and interact with it using the REST API and LangGraph Studio Web UI.\n",
"\n",
"### LangGraph Cloud\n",
"\n",
"- **[LangGraph Cloud QuickStart](../../cloud/quick_start)**: Deploy your LangGraph app using LangGraph Cloud.\n",
"\n",
"### LangGraph Framework\n",
"\n",
"- **[LangGraph Concepts](../../concepts)**: Learn the foundational concepts of LangGraph. \n",
"- **[LangGraph How-to Guides](../../how-tos)**: Guides for common tasks with LangGraph.\n",
"\n",
"### LangGraph Platform\n",
"\n",
"Expand your knowledge with these resources:\n",
"\n",
"- **[LangGraph Platform Concepts](../../concepts#langgraph-platform)**: Understand the foundational concepts of the LangGraph Platform. \n",
"- **[LangGraph Platform How-to Guides](../../how-tos#langgraph-platform)**: Guides for common tasks with LangGraph Platform. "
]
}
],
@@ -3160,7 +3175,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
"version": "3.11.4"
}
},
"nbformat": 4,
@@ -0,0 +1,253 @@
# Quick Start: Launch Local LangGraph Server
This is a quick start guide to help you get a LangGraph app up and running locally.
!!! info "Requirements"
- Python >= 3.11
- [LangGraph CLI](https://langchain-ai.github.io/langgraph/cloud/reference/cli/): Requires langchain-cli[inmem] >= 0.1.58
## Install the LangGraph CLI
```bash
pip install "langgraph-cli[inmem]==0.1.58" python-dotenv
```
## 🌱 Create a LangGraph App
Create a new app from the `react-agent` template. This template is a simple agent that can be flexibly extended to many tools.
=== "Python Server"
```shell
langgraph new path/to/your/app --template react-agent-python
```
=== "Node Server"
```shell
langgraph new path/to/your/app --template react-agent-js
```
!!! tip "Additional Templates"
If you use `langgraph new` without specifying a template, you will be presented with an interactive menu that will allow you to choose from a list of available templates.
## Install Dependencies
In the root of your new LangGraph app, install the dependencies:
```shell
pip install .
```
## Create a `.env` file
You will find a `.env.example` in the root of your new LangGraph app. Create
a `.env` file in the root of your new LangGraph app and copy the contents of the `.env.example` file into it, filling in the necessary API keys:
```bash
LANGSMITH_API_KEY=lsv2...
TAVILY_API_KEY=tvly-...
ANTHROPIC_API_KEY=sk-
OPENAI_API_KEY=sk-...
```
<details><summary>Get API Keys</summary>
<ul>
<li> <b>LANGSMITH_API_KEY</b>: Go to the <a href="https://smith.langchain.com/settings">LangSmith Settings page</a>. Then clck <b>Create API Key</b>.
</li>
<li>
<b>ANTHROPIC_API_KEY</b>: Get an API key from <a href="https://console.anthropic.com/">Anthropic</a>.
</li>
<li>
<b>OPENAI_API_KEY</b>: Get an API key from <a href="https://openai.com/">OpenAI</a>.
</li>
<li>
<b>TAVILY_API_KEY</b>: Get an API key on the <a href="https://app.tavily.com/">Tavily website</a>.
</li>
</ul>
</details>
## 🚀 Launch LangGraph Server
```shell
langgraph dev
```
This will start up the LangGraph API server locally. If this runs successfully, you should see something like:
> Ready!
>
> - API: [http://localhost:8123](http://localhost:8123/)
>
> - Docs: http://localhost:8123/docs
>
> - LangGraph Studio Web UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:8123
!!! note "In-Memory Mode"
The `langgraph dev` command starts LangGraph Server in an in-memory mode. This mode is suitable for development and testing purposes. For production use, you should deploy LangGraph Server with access to a persistent storage backend.
If you want to test your application with a persistent storage backend, you can use the `langgraph up` command instead of `langgraph dev`. You will
need to have `docker` installed on your machine to use this command.
## LangGraph Studio Web UI
Test your graph in the LangGraph Studio Web UI by visiting the URL provided in the output of the `langgraph up` command.
> - LangGraph Studio Web UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:8123
!!! warning "Safari Compatibility"
Currently, LangGraph Studio Web does not support Safari when running a server locally.
## Test the API
=== "Python SDK (Async)"
**Install the LangGraph Python SDK**
```shell
pip install langgraph-sdk
```
**Send a message to the assistant (threadless run)**
```python
from langgraph_sdk import get_client
client = get_client(url="http://localhost:8123")
async for chunk in client.runs.stream(
None, # Threadless run
"agent", # Name of assistant. Defined in langgraph.json.
input={
"messages": [{
"role": "human",
"content": "What is LangGraph?",
}],
},
stream_mode="updates",
):
print(f"Receiving new event of type: {chunk.event}...")
print(chunk.data)
print("\n\n")
```
=== "Python SDK (Sync)"
**Install the LangGraph Python SDK**
```shell
pip install langgraph-sdk
```
**Send a message to the assistant (threadless run)**
```python
from langgraph_sdk import get_sync_client
client = get_sync_client(url="http://localhost:8123")
for chunk in client.runs.stream(
None, # Threadless run
"agent", # Name of assistant. Defined in langgraph.json.
input={
"messages": [{
"role": "human",
"content": "What is LangGraph?",
}],
},
stream_mode="updates",
):
print(f"Receiving new event of type: {chunk.event}...")
print(chunk.data)
print("\n\n")
```
=== "Javascript SDK"
**Install the LangGraph JS SDK**
```shell
npm install @langchain/langgraph-sdk
```
**Send a message to the assistant (threadless run)**
```js
const { Client } = await import("@langchain/langgraph-sdk");
// only set the apiUrl if you changed the default port when calling langgraph up
const client = new Client({ apiUrl: "http://localhost:8123"});
const streamResponse = client.runs.stream(
null, // Threadless run
"agent", // Assistant ID
{
input: {
"messages": [
{ "role": "user", "content": "What is LangGraph?"}
]
},
streamMode: "messages",
}
);
for await (const chunk of streamResponse) {
console.log(`Receiving new event of type: ${chunk.event}...`);
console.log(JSON.stringify(chunk.data));
console.log("\n\n");
}
```
=== "Rest API"
```bash
curl -s --request POST \
--url "http://localhost:8123/runs/stream" \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"input\": {
\"messages\": [
{
\"role\": \"human\",
\"content\": \"What is LangGraph?\"
}
]
},
\"stream_mode\": \"updates\"
}"
```
!!! tip "Auth"
If you're connecting to a remote server, you will need to provide a LangSmith
API Key for authorization. Please see the API Reference for the clients
for more information.
## Next Steps
Now that you have a LangGraph app running locally, take your journey further by exploring deployment and advanced features:
### 🌐 Deploy to LangGraph Cloud
- **[LangGraph Cloud QuickStart](../../cloud/quick_start.md)**: Deploy your LangGraph app using LangGraph Cloud.
### 📚 Learn More about LangGraph Platform
Expand your knowledge with these resources:
- **[LangGraph Platform Concepts](../../concepts/index.md#langgraph-platform)**: Understand the foundational concepts of the LangGraph Platform.
- **[LangGraph Platform How-to Guides](../../how-tos/index.md#langgraph-platform)**: Discover step-by-step guides to build and deploy applications.
### 🛠️ Developer References
Access detailed documentation for development and API usage:
- **[LangGraph Server API Reference](../../cloud/reference/api/api_ref.html)**: Explore the LangGraph Server API documentation.
- **[Python SDK Reference](../../cloud/reference/sdk/python_sdk_ref.md)**: Explore the Python SDK API Reference.
- **[JS/TS SDK Reference](../../cloud/reference/sdk/js_ts_sdk_ref.md)**: Explore the Python SDK API Reference.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -934,7 +934,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.9"
"version": "3.11.4"
}
},
"nbformat": 4,
@@ -112,7 +112,7 @@
"metadata": {},
"outputs": [],
"source": [
"_set_env(\"LANGCHAIN_API_KEY\")\n",
"_set_env(\"LANGSMITH_API_KEY\")\n",
"os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
"os.environ[\"LANGCHAIN_PROJECT\"] = \"local-llama32-rag\""
]
+2 -1
View File
@@ -94,6 +94,7 @@ nav:
- Quick Start:
- Quick Start: tutorials#quick-start
- tutorials/introduction.ipynb
- tutorials/langgraph-platform/local-server.md
- cloud/quick_start.md
- Chatbots:
- Chatbots: tutorials#chatbots
@@ -438,4 +439,4 @@ validation:
# and those anchors are not available in the actual doc
anchors: info
# this is needed to handle headers with anchors for nav
not_found: info
not_found: info
@@ -1,6 +1,6 @@
import threading
from contextlib import contextmanager
from typing import Any, Iterator, Optional, Sequence, Union
from typing import Any, Iterator, Optional, Sequence
from langchain_core.runnables import RunnableConfig
from psycopg import Capabilities, Connection, Cursor, Pipeline
@@ -17,21 +17,11 @@ from langgraph.checkpoint.base import (
CheckpointTuple,
get_checkpoint_id,
)
from langgraph.checkpoint.postgres import _internal
from langgraph.checkpoint.postgres.base import BasePostgresSaver
from langgraph.checkpoint.serde.base import SerializerProtocol
Conn = Union[Connection[DictRow], ConnectionPool[Connection[DictRow]]]
@contextmanager
def _get_connection(conn: Conn) -> Iterator[Connection[DictRow]]:
if isinstance(conn, Connection):
yield conn
elif isinstance(conn, ConnectionPool):
with conn.connection() as conn:
yield conn
else:
raise TypeError(f"Invalid connection type: {type(conn)}")
Conn = _internal.Conn # For backward compatibility
class PostgresSaver(BasePostgresSaver):
@@ -39,7 +29,7 @@ class PostgresSaver(BasePostgresSaver):
def __init__(
self,
conn: Conn,
conn: _internal.Conn,
pipe: Optional[Pipeline] = None,
serde: Optional[SerializerProtocol] = None,
) -> None:
@@ -73,9 +63,9 @@ class PostgresSaver(BasePostgresSaver):
) as conn:
if pipeline:
with conn.pipeline() as pipe:
yield PostgresSaver(conn, pipe)
yield cls(conn, pipe)
else:
yield PostgresSaver(conn)
yield cls(conn)
def setup(self) -> None:
"""Set up the checkpoint database asynchronously.
@@ -373,7 +363,7 @@ class PostgresSaver(BasePostgresSaver):
Will be applied regardless of whether the PostgresSaver instance was initialized with a pipeline.
If pipeline mode is not supported, will fall back to using transaction context manager.
"""
with _get_connection(self.conn) as conn:
with _internal.get_connection(self.conn) as conn:
if self.pipe:
# a connection in pipeline mode can be used concurrently
# in multiple threads/coroutines, but only one cursor can be
@@ -403,4 +393,4 @@ class PostgresSaver(BasePostgresSaver):
yield cur
__all__ = ["PostgresSaver", "Conn"]
__all__ = ["PostgresSaver", "BasePostgresSaver", "Conn"]
@@ -0,0 +1,23 @@
"""Shared async utility functions for the Postgres checkpoint & storage classes."""
from contextlib import asynccontextmanager
from typing import AsyncIterator, Union
from psycopg import AsyncConnection
from psycopg.rows import DictRow
from psycopg_pool import AsyncConnectionPool
Conn = Union[AsyncConnection[DictRow], AsyncConnectionPool[AsyncConnection[DictRow]]]
@asynccontextmanager
async def get_connection(
conn: Conn,
) -> AsyncIterator[AsyncConnection[DictRow]]:
if isinstance(conn, AsyncConnection):
yield conn
elif isinstance(conn, AsyncConnectionPool):
async with conn.connection() as conn:
yield conn
else:
raise TypeError(f"Invalid connection type: {type(conn)}")
@@ -0,0 +1,21 @@
"""Shared utility functions for the Postgres checkpoint & storage classes."""
from contextlib import contextmanager
from typing import Iterator, Union
from psycopg import Connection
from psycopg.rows import DictRow
from psycopg_pool import ConnectionPool
Conn = Union[Connection[DictRow], ConnectionPool[Connection[DictRow]]]
@contextmanager
def get_connection(conn: Conn) -> Iterator[Connection[DictRow]]:
if isinstance(conn, Connection):
yield conn
elif isinstance(conn, ConnectionPool):
with conn.connection() as conn:
yield conn
else:
raise TypeError(f"Invalid connection type: {type(conn)}")
@@ -1,6 +1,6 @@
import asyncio
from contextlib import asynccontextmanager
from typing import Any, AsyncIterator, Iterator, Optional, Sequence, Union
from typing import Any, AsyncIterator, Iterator, Optional, Sequence
from langchain_core.runnables import RunnableConfig
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
@@ -17,23 +17,11 @@ from langgraph.checkpoint.base import (
CheckpointTuple,
get_checkpoint_id,
)
from langgraph.checkpoint.postgres import _ainternal
from langgraph.checkpoint.postgres.base import BasePostgresSaver
from langgraph.checkpoint.serde.base import SerializerProtocol
Conn = Union[AsyncConnection[DictRow], AsyncConnectionPool[AsyncConnection[DictRow]]]
@asynccontextmanager
async def _get_connection(
conn: Conn,
) -> AsyncIterator[AsyncConnection[DictRow]]:
if isinstance(conn, AsyncConnection):
yield conn
elif isinstance(conn, AsyncConnectionPool):
async with conn.connection() as conn:
yield conn
else:
raise TypeError(f"Invalid connection type: {type(conn)}")
Conn = _ainternal.Conn # For backward compatibility
class AsyncPostgresSaver(BasePostgresSaver):
@@ -41,7 +29,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
def __init__(
self,
conn: Conn,
conn: _ainternal.Conn,
pipe: Optional[AsyncPipeline] = None,
serde: Optional[SerializerProtocol] = None,
) -> None:
@@ -80,9 +68,9 @@ class AsyncPostgresSaver(BasePostgresSaver):
) as conn:
if pipeline:
async with conn.pipeline() as pipe:
yield AsyncPostgresSaver(conn=conn, pipe=pipe, serde=serde)
yield cls(conn=conn, pipe=pipe, serde=serde)
else:
yield AsyncPostgresSaver(conn=conn, serde=serde)
yield cls(conn=conn, serde=serde)
async def setup(self) -> None:
"""Set up the checkpoint database asynchronously.
@@ -157,15 +145,17 @@ class AsyncPostgresSaver(BasePostgresSaver):
value["pending_sends"],
),
self._load_metadata(value["metadata"]),
{
"configurable": {
"thread_id": value["thread_id"],
"checkpoint_ns": value["checkpoint_ns"],
"checkpoint_id": value["parent_checkpoint_id"],
(
{
"configurable": {
"thread_id": value["thread_id"],
"checkpoint_ns": value["checkpoint_ns"],
"checkpoint_id": value["parent_checkpoint_id"],
}
}
}
if value["parent_checkpoint_id"]
else None,
if value["parent_checkpoint_id"]
else None
),
await asyncio.to_thread(self._load_writes, value["pending_writes"]),
)
@@ -216,15 +206,17 @@ class AsyncPostgresSaver(BasePostgresSaver):
value["pending_sends"],
),
self._load_metadata(value["metadata"]),
{
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": checkpoint_ns,
"checkpoint_id": value["parent_checkpoint_id"],
(
{
"configurable": {
"thread_id": thread_id,
"checkpoint_ns": checkpoint_ns,
"checkpoint_id": value["parent_checkpoint_id"],
}
}
}
if value["parent_checkpoint_id"]
else None,
if value["parent_checkpoint_id"]
else None
),
await asyncio.to_thread(self._load_writes, value["pending_writes"]),
)
@@ -331,7 +323,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
Will be applied regardless of whether the AsyncPostgresSaver instance was initialized with a pipeline.
If pipeline mode is not supported, will fall back to using transaction context manager.
"""
async with _get_connection(self.conn) as conn:
async with _ainternal.get_connection(self.conn) as conn:
if self.pipe:
# a connection in pipeline mode can be used concurrently
# in multiple threads/coroutines, but only one cursor can be
@@ -467,3 +459,6 @@ class AsyncPostgresSaver(BasePostgresSaver):
return asyncio.run_coroutine_threadsafe(
self.aput_writes(config, writes, task_id), self.loop
).result()
__all__ = ["AsyncPostgresSaver", "Conn"]
@@ -13,14 +13,17 @@ from typing import (
)
import orjson
from psycopg import AsyncConnection, AsyncCursor
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
from psycopg.errors import UndefinedTable
from psycopg.rows import dict_row
from psycopg.rows import DictRow, dict_row
from psycopg_pool import AsyncConnectionPool
from langgraph.checkpoint.postgres import _ainternal
from langgraph.store.base import GetOp, ListNamespacesOp, Op, PutOp, Result, SearchOp
from langgraph.store.base.batch import AsyncBatchedBaseStore
from langgraph.store.postgres.base import (
BasePostgresStore,
PoolConfig,
Row,
_decode_ns_bytes,
_group_ops,
@@ -30,81 +33,88 @@ from langgraph.store.postgres.base import (
logger = logging.getLogger(__name__)
class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[AsyncConnection]):
__slots__ = ("_deserializer",)
class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Conn]):
__slots__ = ("_deserializer", "pipe", "lock", "supports_pipeline")
def __init__(
self,
conn: AsyncConnection[Any],
conn: _ainternal.Conn,
*,
pipe: Optional[AsyncPipeline] = None,
deserializer: Optional[
Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]
] = None,
) -> None:
if isinstance(conn, AsyncConnectionPool) and pipe is not None:
raise ValueError(
"Pipeline should be used only with a single AsyncConnection, not AsyncConnectionPool."
)
super().__init__()
self._deserializer = deserializer
self.conn = conn
self.pipe = pipe
self.lock = asyncio.Lock()
self.loop = asyncio.get_running_loop()
self.supports_pipeline = Capabilities().has_pipeline()
async def abatch(self, ops: Iterable[Op]) -> list[Result]:
grouped_ops, num_ops = _group_ops(ops)
results: list[Result] = [None] * num_ops
async with self.conn.pipeline():
tasks = []
if GetOp in grouped_ops:
tasks.append(
self._batch_get_ops(
cast(Sequence[tuple[int, GetOp]], grouped_ops[GetOp]), results
)
)
if PutOp in grouped_ops:
tasks.append(
self._batch_put_ops(
cast(Sequence[tuple[int, PutOp]], grouped_ops[PutOp])
)
)
if SearchOp in grouped_ops:
tasks.append(
self._batch_search_ops(
cast(Sequence[tuple[int, SearchOp]], grouped_ops[SearchOp]),
results,
)
)
if ListNamespacesOp in grouped_ops:
tasks.append(
self._batch_list_namespaces_ops(
cast(
Sequence[tuple[int, ListNamespacesOp]],
grouped_ops[ListNamespacesOp],
),
results,
)
)
await asyncio.gather(*tasks)
async with _ainternal.get_connection(self.conn) as conn:
if self.pipe:
async with self.pipe:
await self._execute_batch(grouped_ops, results, conn)
else:
await self._execute_batch(grouped_ops, results, conn)
return results
def batch(self, ops: Iterable[Op]) -> list[Result]:
return asyncio.run_coroutine_threadsafe(self.abatch(ops), self.loop).result()
async def _execute_batch(
self,
grouped_ops: dict,
results: list[Result],
conn: AsyncConnection[DictRow],
) -> None:
async with self._cursor(conn, pipeline=True) as cur:
if GetOp in grouped_ops:
await self._batch_get_ops(
cast(Sequence[tuple[int, GetOp]], grouped_ops[GetOp]),
results,
cur,
)
if SearchOp in grouped_ops:
await self._batch_search_ops(
cast(Sequence[tuple[int, SearchOp]], grouped_ops[SearchOp]),
results,
cur,
)
if ListNamespacesOp in grouped_ops:
await self._batch_list_namespaces_ops(
cast(
Sequence[tuple[int, ListNamespacesOp]],
grouped_ops[ListNamespacesOp],
),
results,
cur,
)
if PutOp in grouped_ops:
await self._batch_put_ops(
cast(Sequence[tuple[int, PutOp]], grouped_ops[PutOp]),
cur,
)
async def _batch_get_ops(
self,
get_ops: Sequence[tuple[int, GetOp]],
results: list[Result],
cur: AsyncCursor[DictRow],
) -> None:
cursors = []
for query, params, namespace, items in self._get_batch_GET_ops_queries(get_ops):
cur = self.conn.cursor(binary=True)
await cur.execute(query, params)
cursors.append((cur, namespace, items))
for cur, namespace, items in cursors:
rows = cast(list[Row], await cur.fetchall())
key_to_row = {row["key"]: row for row in rows}
for idx, key in items:
@@ -119,26 +129,21 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[AsyncConnectio
async def _batch_put_ops(
self,
put_ops: Sequence[tuple[int, PutOp]],
cur: AsyncCursor[DictRow],
) -> None:
queries = self._get_batch_PUT_queries(put_ops)
for query, params in queries:
cur = self.conn.cursor(binary=True)
await cur.execute(query, params)
async def _batch_search_ops(
self,
search_ops: Sequence[tuple[int, SearchOp]],
results: list[Result],
cur: AsyncCursor[DictRow],
) -> None:
queries = self._get_batch_search_queries(search_ops)
cursors: list[tuple[AsyncCursor[Any], int]] = []
for (query, params), (idx, _) in zip(queries, search_ops):
cur = self.conn.cursor(binary=True)
await cur.execute(query, params)
cursors.append((cur, idx))
for cur, idx in cursors:
rows = cast(list[Row], await cur.fetchall())
items = [
_row_to_item(
@@ -152,37 +157,103 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[AsyncConnectio
self,
list_ops: Sequence[tuple[int, ListNamespacesOp]],
results: list[Result],
cur: AsyncCursor[DictRow],
) -> None:
queries = self._get_batch_list_namespaces_queries(list_ops)
cursors: list[tuple[AsyncCursor[Any], int]] = []
for (query, params), (idx, _) in zip(queries, list_ops):
cur = self.conn.cursor(binary=True)
await cur.execute(query, params)
cursors.append((cur, idx))
for cur, idx in cursors:
rows = cast(list[dict], await cur.fetchall())
namespaces = [_decode_ns_bytes(row["truncated_prefix"]) for row in rows]
results[idx] = namespaces
@asynccontextmanager
async def _cursor(
self, conn: AsyncConnection[DictRow], *, pipeline: bool = False
) -> AsyncIterator[AsyncCursor[Any]]:
"""Create a database cursor as a context manager.
Args:
conn: The database connection to use
pipeline: whether to use pipeline for the DB operations inside the context manager.
Will be applied regardless of whether the PostgresStore instance was initialized with a pipeline.
If pipeline mode is not supported, will fall back to using transaction context manager.
"""
if self.pipe:
# a connection in pipeline mode can be used concurrently
# in multiple threads/coroutines, but only one cursor can be
# used at a time
async with conn.cursor(binary=True) as cur:
try:
yield cur
finally:
if pipeline:
await self.pipe.sync()
elif pipeline:
# a connection not in pipeline mode can only be used by one
# thread/coroutine at a time, so we acquire a lock
if self.supports_pipeline:
async with self.lock, conn.pipeline(), conn.cursor(binary=True) as cur:
yield cur
else:
async with self.lock, conn.transaction(), conn.cursor(
binary=True
) as cur:
yield cur
else:
async with conn.cursor(binary=True) as cur:
yield cur
def batch(self, ops: Iterable[Op]) -> list[Result]:
return asyncio.run_coroutine_threadsafe(self.abatch(ops), self.loop).result()
@classmethod
@asynccontextmanager
async def from_conn_string(
cls,
conn_string: str,
*,
pipeline: bool = False,
pool_config: Optional[PoolConfig] = None,
) -> AsyncIterator["AsyncPostgresStore"]:
"""Create a new AsyncPostgresStore instance from a connection string.
Args:
conn_string (str): The Postgres connection info string.
pipeline (bool): Whether to use AsyncPipeline (only for single connections)
pool_config (Optional[PoolConfig]): Configuration for the connection pool.
If provided, will create a connection pool and use it instead of a single connection.
This overrides the `pipeline` argument.
Returns:
AsyncPostgresStore: A new AsyncPostgresStore instance.
"""
async with await AsyncConnection.connect(
conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
) as conn:
yield cls(conn=conn)
if pool_config is not None:
pc = pool_config.copy()
async with cast(
AsyncConnectionPool[AsyncConnection[DictRow]],
AsyncConnectionPool(
conn_string,
min_size=pc.pop("min_size", 1),
max_size=pc.pop("max_size", None),
kwargs={
"autocommit": True,
"prepare_threshold": 0,
"row_factory": dict_row,
**(pc.pop("kwargs", None) or {}),
},
**cast(dict, pc),
),
) as pool:
yield cls(conn=pool)
else:
async with await AsyncConnection.connect(
conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
) as conn:
if pipeline:
async with conn.pipeline() as pipe:
yield cls(conn=conn, pipe=pipe)
else:
yield cls(conn=conn)
async def setup(self) -> None:
"""Set up the store database asynchronously.
@@ -191,28 +262,33 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[AsyncConnectio
already exist and runs database migrations. It MUST be called directly by the user
the first time the store is used.
"""
async with self.conn.cursor() as cur:
try:
await cur.execute(
"SELECT v FROM store_migrations ORDER BY v DESC LIMIT 1"
)
row = cast(dict, await cur.fetchone())
if row is None:
version = -1
else:
version = row["v"]
except UndefinedTable:
version = -1
# Create store_migrations table if it doesn't exist
await cur.execute(
"""
CREATE TABLE IF NOT EXISTS store_migrations (
v INTEGER PRIMARY KEY
async with _ainternal.get_connection(self.conn) as conn:
async with conn.cursor() as cur:
try:
await cur.execute(
"SELECT v FROM store_migrations ORDER BY v DESC LIMIT 1"
)
"""
)
for v, migration in enumerate(
self.MIGRATIONS[version + 1 :], start=version + 1
):
await cur.execute(migration)
await cur.execute("INSERT INTO store_migrations (v) VALUES (%s)", (v,))
row = cast(dict, await cur.fetchone())
if row is None:
version = -1
else:
version = row["v"]
except UndefinedTable:
version = -1
# Create store_migrations table if it doesn't exist
await cur.execute(
"""
CREATE TABLE IF NOT EXISTS store_migrations (
v INTEGER PRIMARY KEY
)
"""
)
for v, migration in enumerate(
self.MIGRATIONS[version + 1 :], start=version + 1
):
await cur.execute(migration)
await cur.execute(
"INSERT INTO store_migrations (v) VALUES (%s)", (v,)
)
if self.pipe:
await self.pipe.sync()
@@ -1,6 +1,7 @@
import asyncio
import json
import logging
import threading
from collections import defaultdict
from contextlib import contextmanager
from datetime import datetime
@@ -18,12 +19,15 @@ from typing import (
)
import orjson
from psycopg import BaseConnection, Connection, Cursor
from psycopg import Capabilities, Connection, Cursor, Pipeline
from psycopg.errors import UndefinedTable
from psycopg.rows import dict_row
from psycopg.rows import DictRow, dict_row
from psycopg.types.json import Jsonb
from psycopg_pool import ConnectionPool
from typing_extensions import TypedDict
from langgraph.checkpoint.postgres import _ainternal as _ainternal
from langgraph.checkpoint.postgres import _internal as _pg_internal
from langgraph.store.base import (
BaseStore,
GetOp,
@@ -56,7 +60,32 @@ CREATE INDEX IF NOT EXISTS store_prefix_idx ON store USING btree (prefix text_pa
""",
]
C = TypeVar("C", bound=BaseConnection)
C = TypeVar("C", bound=Union[_pg_internal.Conn, _ainternal.Conn])
class PoolConfig(TypedDict, total=False):
"""Connection pool settings for PostgreSQL connections.
Controls connection lifecycle and resource utilization:
- Small pools (1-5) suit low-concurrency workloads
- Larger pools handle concurrent requests but consume more resources
- Setting max_size prevents resource exhaustion under load
"""
min_size: int
"""Minimum number of connections maintained in the pool. Defaults to 1."""
max_size: Optional[int]
"""Maximum number of connections allowed in the pool. None means unlimited."""
kwargs: dict
"""Additional connection arguments passed to each connection in the pool.
Default kwargs set automatically:
- autocommit: True
- prepare_threshold: 0
- row_factory: dict_row
"""
class BasePostgresStore(Generic[C]):
@@ -88,9 +117,14 @@ class BasePostgresStore(Generic[C]):
self,
put_ops: Sequence[tuple[int, PutOp]],
) -> list[tuple[str, Sequence]]:
# Last-write wins
dedupped_ops: dict[tuple[tuple[str, ...], str], PutOp] = {}
for _, op in put_ops:
dedupped_ops[(op.namespace, op.key)] = op
inserts: list[PutOp] = []
deletes: list[PutOp] = []
for _, op in put_ops:
for op in dedupped_ops.values():
if op.value is None:
deletes.append(op)
else:
@@ -219,13 +253,14 @@ class BasePostgresStore(Generic[C]):
return queries
class PostgresStore(BaseStore, BasePostgresStore[Connection]):
__slots__ = ("_deserializer",)
class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
__slots__ = ("_deserializer", "pipe", "lock", "supports_pipeline")
def __init__(
self,
conn: Connection[Any],
conn: _pg_internal.Conn,
*,
pipe: Optional[Pipeline] = None,
deserializer: Optional[
Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]
] = None,
@@ -233,26 +268,110 @@ class PostgresStore(BaseStore, BasePostgresStore[Connection]):
super().__init__()
self._deserializer = deserializer
self.conn = conn
self.pipe = pipe
self.supports_pipeline = Capabilities().has_pipeline()
self.lock = threading.Lock()
@classmethod
@contextmanager
def from_conn_string(
cls,
conn_string: str,
*,
pipeline: bool = False,
pool_config: Optional[PoolConfig] = None,
) -> Iterator["PostgresStore"]:
"""Create a new PostgresStore instance from a connection string.
Args:
conn_string (str): The Postgres connection info string.
pipeline (bool): whether to use Pipeline (only for single connections)
pool_config (Optional[PoolArgs]): Configuration for the connection pool.
If provided, will create a connection pool and use it instead of a single connection.
This overrides the `pipeline` argument.
Returns:
PostgresStore: A new PostgresStore instance.
"""
if pool_config is not None:
pc = pool_config.copy()
with cast(
ConnectionPool[Connection[DictRow]],
ConnectionPool(
conn_string,
min_size=pc.pop("min_size", 1),
max_size=pc.pop("max_size", None),
kwargs={
"autocommit": True,
"prepare_threshold": 0,
"row_factory": dict_row,
**(pc.pop("kwargs", None) or {}),
},
**cast(dict, pc),
),
) as pool:
yield cls(conn=pool)
else:
with Connection.connect(
conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
) as conn:
if pipeline:
with conn.pipeline() as pipe:
yield cls(conn, pipe=pipe)
else:
yield cls(conn)
@contextmanager
def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor[DictRow]]:
"""Create a database cursor as a context manager.
Args:
pipeline (bool): whether to use pipeline for the DB operations inside the context manager.
Will be applied regardless of whether the PostgresStore instance was initialized with a pipeline.
If pipeline mode is not supported, will fall back to using transaction context manager.
"""
with _pg_internal.get_connection(self.conn) as conn:
if self.pipe:
# a connection in pipeline mode can be used concurrently
# in multiple threads/coroutines, but only one cursor can be
# used at a time
try:
with conn.cursor(binary=True, row_factory=dict_row) as cur:
yield cur
finally:
if pipeline:
self.pipe.sync()
elif pipeline:
# a connection not in pipeline mode can only be used by one
# thread/coroutine at a time, so we acquire a lock
if self.supports_pipeline:
with self.lock, conn.pipeline(), conn.cursor(
binary=True, row_factory=dict_row
) as cur:
yield cur
else:
with self.lock, conn.transaction(), conn.cursor(
binary=True, row_factory=dict_row
) as cur:
yield cur
else:
with conn.cursor(binary=True, row_factory=dict_row) as cur:
yield cur
def batch(self, ops: Iterable[Op]) -> list[Result]:
grouped_ops, num_ops = _group_ops(ops)
results: list[Result] = [None] * num_ops
with self.conn.pipeline():
with self._cursor(pipeline=True) as cur:
if GetOp in grouped_ops:
self._batch_get_ops(
cast(Sequence[tuple[int, GetOp]], grouped_ops[GetOp]), results
)
if PutOp in grouped_ops:
self._batch_put_ops(
cast(Sequence[tuple[int, PutOp]], grouped_ops[PutOp])
cast(Sequence[tuple[int, GetOp]], grouped_ops[GetOp]), results, cur
)
if SearchOp in grouped_ops:
self._batch_search_ops(
cast(Sequence[tuple[int, SearchOp]], grouped_ops[SearchOp]),
results,
cur,
)
if ListNamespacesOp in grouped_ops:
@@ -262,25 +381,23 @@ class PostgresStore(BaseStore, BasePostgresStore[Connection]):
grouped_ops[ListNamespacesOp],
),
results,
cur,
)
if PutOp in grouped_ops:
self._batch_put_ops(
cast(Sequence[tuple[int, PutOp]], grouped_ops[PutOp]), cur
)
return results
async def abatch(self, ops: Iterable[Op]) -> list[Result]:
return await asyncio.get_running_loop().run_in_executor(None, self.batch, ops)
def _batch_get_ops(
self,
get_ops: Sequence[tuple[int, GetOp]],
results: list[Result],
cur: Cursor[DictRow],
) -> None:
cursors = []
for query, params, namespace, items in self._get_batch_GET_ops_queries(get_ops):
cur = self.conn.cursor(binary=True)
cur.execute(query, params)
cursors.append((cur, namespace, items))
for cur, namespace, items in cursors:
rows = cast(list[Row], cur.fetchall())
key_to_row = {row["key"]: row for row in rows}
for idx, key in items:
@@ -295,70 +412,44 @@ class PostgresStore(BaseStore, BasePostgresStore[Connection]):
def _batch_put_ops(
self,
put_ops: Sequence[tuple[int, PutOp]],
cur: Cursor[DictRow],
) -> None:
queries = self._get_batch_PUT_queries(put_ops)
for query, params in queries:
cur = self.conn.cursor(binary=True)
cur.execute(query, params)
def _batch_search_ops(
self,
search_ops: Sequence[tuple[int, SearchOp]],
results: list[Result],
cur: Cursor[DictRow],
) -> None:
queries = self._get_batch_search_queries(search_ops)
cursors: list[tuple[Cursor[Any], int]] = []
for (query, params), (idx, _) in zip(queries, search_ops):
cur = self.conn.cursor(binary=True)
for (query, params), (idx, _) in zip(
self._get_batch_search_queries(search_ops), search_ops
):
cur.execute(query, params)
cursors.append((cur, idx))
for cur, idx in cursors:
rows = cast(list[Row], cur.fetchall())
items = [
results[idx] = [
_row_to_item(
_decode_ns_bytes(row["prefix"]), row, loader=self._deserializer
)
for row in rows
]
results[idx] = items
def _batch_list_namespaces_ops(
self,
list_ops: Sequence[tuple[int, ListNamespacesOp]],
results: list[Result],
cur: Cursor[DictRow],
) -> None:
queries = self._get_batch_list_namespaces_queries(list_ops)
cursors: list[tuple[Cursor[Any], int]] = []
for (query, params), (idx, _) in zip(queries, list_ops):
cur = self.conn.cursor(binary=True)
for (query, params), (idx, _) in zip(
self._get_batch_list_namespaces_queries(list_ops), list_ops
):
cur.execute(query, params)
cursors.append((cur, idx))
results[idx] = [_decode_ns_bytes(row["truncated_prefix"]) for row in cur]
for cur, idx in cursors:
rows = cast(list[dict], cur.fetchall())
namespaces = [_decode_ns_bytes(row["truncated_prefix"]) for row in rows]
results[idx] = namespaces
@classmethod
@contextmanager
def from_conn_string(
cls,
conn_string: str,
) -> Iterator["PostgresStore"]:
"""Create a new BasePostgresStore instance from a connection string.
Args:
conn_string (str): The Postgres connection info string.
Returns:
BasePostgresStore: A new BasePostgresStore instance.
"""
with Connection.connect(
conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
) as conn:
yield cls(conn=conn)
async def abatch(self, ops: Iterable[Op]) -> list[Result]:
return await asyncio.get_running_loop().run_in_executor(None, self.batch, ops)
def setup(self) -> None:
"""Set up the store database.
@@ -367,7 +458,7 @@ class PostgresStore(BaseStore, BasePostgresStore[Connection]):
already exist and runs database migrations. It MUST be called directly by the user
the first time the store is used.
"""
with self.conn.cursor(binary=True) as cur:
with self._cursor() as cur:
try:
cur.execute("SELECT v FROM store_migrations ORDER BY v DESC LIMIT 1")
row = cast(dict, cur.fetchone())
@@ -376,9 +467,7 @@ class PostgresStore(BaseStore, BasePostgresStore[Connection]):
else:
version = row["v"]
except UndefinedTable:
self.conn.rollback()
version = -1
# Create store_migrations table if it doesn't exist
cur.execute(
"""
CREATE TABLE IF NOT EXISTS store_migrations (
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-postgres"
version = "2.0.3"
version = "2.0.4"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
+5 -1
View File
@@ -24,6 +24,10 @@ async def clear_test_db(conn: AsyncConnection[DictRow]) -> None:
await conn.execute("DELETE FROM checkpoint_blobs")
await conn.execute("DELETE FROM checkpoint_writes")
await conn.execute("DELETE FROM checkpoint_migrations")
await conn.execute("DELETE FROM store_migrations")
except UndefinedTable:
pass
try:
await conn.execute("DELETE FROM store_migrations")
await conn.execute("DELETE FROM store")
except UndefinedTable:
pass
@@ -1,114 +1,76 @@
# type: ignore
import sys
import uuid
from datetime import datetime
from typing import Any
from unittest.mock import AsyncMock, MagicMock
from typing import AsyncIterator
import pytest
from conftest import DEFAULT_URI # type: ignore
from psycopg import AsyncConnection
from langgraph.store.base import GetOp, Item, ListNamespacesOp, PutOp, SearchOp
from langgraph.store.postgres import AsyncPostgresStore
class MockAsyncCursor:
def __init__(self, fetch_result: Any) -> None:
self.fetch_result = fetch_result
self.execute = AsyncMock()
self.fetchall = AsyncMock(return_value=self.fetch_result)
@pytest.fixture(scope="function", params=["default", "pipe", "pool"])
async def store(request) -> AsyncIterator[AsyncPostgresStore]:
if sys.version_info < (3, 10):
pytest.skip("Async Postgres tests require Python 3.10+")
database = f"test_{uuid.uuid4().hex[:16]}"
uri_parts = DEFAULT_URI.split("/")
uri_base = "/".join(uri_parts[:-1])
query_params = ""
if "?" in uri_parts[-1]:
db_name, query_params = uri_parts[-1].split("?", 1)
query_params = "?" + query_params
class MockAsyncConnection:
def __init__(self) -> None:
self.cursor = MagicMock()
self.pipeline = MagicMock(
return_value=AsyncMock(__aenter__=AsyncMock(), __aexit__=AsyncMock())
)
conn_string = f"{uri_base}/{database}{query_params}"
admin_conn_string = DEFAULT_URI
async with await AsyncConnection.connect(
admin_conn_string, autocommit=True
) as conn:
await conn.execute(f"CREATE DATABASE {database}")
try:
async with AsyncPostgresStore.from_conn_string(conn_string) as store:
await store.setup()
@pytest.fixture
def mock_connection() -> MockAsyncConnection:
return MockAsyncConnection()
@pytest.fixture
async def store(mock_connection: MockAsyncConnection) -> AsyncPostgresStore:
return AsyncPostgresStore(mock_connection)
if request.param == "pipe":
async with AsyncPostgresStore.from_conn_string(
conn_string, pipeline=True
) as store:
yield store
elif request.param == "pool":
async with AsyncPostgresStore.from_conn_string(
conn_string, pool_config={"min_size": 1, "max_size": 10}
) as store:
yield store
else: # default
async with AsyncPostgresStore.from_conn_string(conn_string) as store:
yield store
finally:
async with await AsyncConnection.connect(
admin_conn_string, autocommit=True
) as conn:
await conn.execute(f"DROP DATABASE {database}")
async def test_abatch_order(store: AsyncPostgresStore) -> None:
mock_connection = store.conn
mock_get_cursor = MockAsyncCursor(
[
{
"key": "key1",
"value": '{"data": "value1"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.foo",
},
{
"key": "key2",
"value": '{"data": "value2"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.bar",
},
]
)
mock_search_cursor = MockAsyncCursor(
[
{
"key": "key1",
"value": '{"data": "value1"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.foo",
},
]
)
mock_list_namespaces_cursor = MockAsyncCursor(
[
{"truncated_prefix": b"\x01test"},
]
)
failures = []
def cursor_side_effect(binary: bool = False) -> Any:
cursor = MagicMock()
async def execute_side_effect(query: str, *params: Any) -> None:
# My super sophisticated database.
if "SELECT prefix, key," in query:
cursor.fetchall = mock_search_cursor.fetchall
elif "SELECT DISTINCT ON (truncated_prefix)" in query:
cursor.fetchall = mock_list_namespaces_cursor.fetchall
elif "WHERE prefix = %s AND key" in query:
cursor.fetchall = mock_get_cursor.fetchall
elif "INSERT INTO " in query:
pass
else:
e = ValueError(f"Unmatched query: {query}")
failures.append(e)
raise e
cursor.execute = AsyncMock(side_effect=execute_side_effect)
return cursor
mock_connection.cursor.side_effect = cursor_side_effect # type: ignore
# Setup test data
await store.aput(("test", "foo"), "key1", {"data": "value1"})
await store.aput(("test", "bar"), "key2", {"data": "value2"})
ops = [
GetOp(namespace=("test",), key="key1"),
PutOp(namespace=("test",), key="key2", value={"data": "value2"}),
GetOp(namespace=("test", "foo"), key="key1"),
PutOp(namespace=("test", "bar"), key="key2", value={"data": "value2"}),
SearchOp(
namespace_prefix=("test",), filter={"data": "value1"}, limit=10, offset=0
),
ListNamespacesOp(match_conditions=None, max_depth=None, limit=10, offset=0),
GetOp(namespace=("test",), key="key3"),
]
results = await store.abatch(ops)
assert not failures
assert len(results) == 5
assert isinstance(results[0], Item)
assert isinstance(results[0].value, dict)
@@ -118,27 +80,29 @@ async def test_abatch_order(store: AsyncPostgresStore) -> None:
assert isinstance(results[2], list)
assert len(results[2]) == 1
assert isinstance(results[3], list)
assert results[3] == [("test",)]
assert ("test", "foo") in results[3] and ("test", "bar") in results[3]
assert results[4] is None
ops_reordered = [
SearchOp(namespace_prefix=("test",), filter=None, limit=5, offset=0),
GetOp(namespace=("test",), key="key2"),
GetOp(namespace=("test", "bar"), key="key2"),
ListNamespacesOp(match_conditions=None, max_depth=None, limit=5, offset=0),
PutOp(namespace=("test",), key="key3", value={"data": "value3"}),
GetOp(namespace=("test",), key="key1"),
GetOp(namespace=("test", "foo"), key="key1"),
]
results_reordered = await store.abatch(ops_reordered)
assert not failures
assert len(results_reordered) == 5
assert isinstance(results_reordered[0], list)
assert len(results_reordered[0]) == 1
assert len(results_reordered[0]) == 2
assert isinstance(results_reordered[1], Item)
assert results_reordered[1].value == {"data": "value2"}
assert results_reordered[1].key == "key2"
assert isinstance(results_reordered[2], list)
assert results_reordered[2] == [("test",)]
assert ("test", "foo") in results_reordered[2] and (
"test",
"bar",
) in results_reordered[2]
assert results_reordered[3] is None
assert isinstance(results_reordered[4], Item)
assert results_reordered[4].value == {"data": "value1"}
@@ -146,26 +110,9 @@ async def test_abatch_order(store: AsyncPostgresStore) -> None:
async def test_batch_get_ops(store: AsyncPostgresStore) -> None:
mock_connection = store.conn
mock_cursor = MockAsyncCursor(
[
{
"key": "key1",
"value": '{"data": "value1"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.foo",
},
{
"key": "key2",
"value": '{"data": "value2"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.bar",
},
]
)
mock_connection.cursor.return_value = mock_cursor
# Setup test data
await store.aput(("test",), "key1", {"data": "value1"})
await store.aput(("test",), "key2", {"data": "value2"})
ops = [
GetOp(namespace=("test",), key="key1"),
@@ -184,10 +131,6 @@ async def test_batch_get_ops(store: AsyncPostgresStore) -> None:
async def test_batch_put_ops(store: AsyncPostgresStore) -> None:
mock_connection = store.conn
mock_cursor = MockAsyncCursor([])
mock_connection.cursor.return_value = mock_cursor
ops = [
PutOp(namespace=("test",), key="key1", value={"data": "value1"}),
PutOp(namespace=("test",), key="key2", value={"data": "value2"}),
@@ -198,30 +141,16 @@ async def test_batch_put_ops(store: AsyncPostgresStore) -> None:
assert len(results) == 3
assert all(result is None for result in results)
assert mock_cursor.execute.call_count == 2
# Verify the puts worked
items = await store.asearch(["test"], limit=10)
assert len(items) == 2 # key3 had None value so wasn't stored
async def test_batch_search_ops(store: AsyncPostgresStore) -> None:
mock_connection = store.conn
mock_cursor = MockAsyncCursor(
[
{
"key": "key1",
"value": '{"data": "value1"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.foo",
},
{
"key": "key2",
"value": '{"data": "value2"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.bar",
},
]
)
mock_connection.cursor.return_value = mock_cursor
# Setup test data
await store.aput(("test", "foo"), "key1", {"data": "value1"})
await store.aput(("test", "bar"), "key2", {"data": "value2"})
ops = [
SearchOp(
@@ -233,29 +162,23 @@ async def test_batch_search_ops(store: AsyncPostgresStore) -> None:
results = await store.abatch(ops)
assert len(results) == 2
assert len(results[0]) == 2
assert len(results[1]) == 2
assert len(results[0]) == 1 # Filtered results
assert len(results[1]) == 2 # All results
async def test_batch_list_namespaces_ops(store: AsyncPostgresStore) -> None:
mock_connection = store.conn
mock_cursor = MockAsyncCursor(
[
{"truncated_prefix": b"\x01test.namespace1"},
{"truncated_prefix": b"\x01test.namespace2"},
]
)
mock_connection.cursor.return_value = mock_cursor
# Setup test data
await store.aput(("test", "namespace1"), "key1", {"data": "value1"})
await store.aput(("test", "namespace2"), "key2", {"data": "value2"})
ops = [ListNamespacesOp(match_conditions=None, max_depth=None, limit=10, offset=0)]
results = await store.abatch(ops)
assert len(results) == 1
assert results[0] == [("test", "namespace1"), ("test", "namespace2")]
# The following use the actual DB connection
assert len(results[0]) == 2
assert ("test", "namespace1") in results[0]
assert ("test", "namespace2") in results[0]
class TestAsyncPostgresStore:
+188 -313
View File
@@ -1,174 +1,118 @@
# type: ignore
import uuid
from datetime import datetime
from typing import Any
from unittest.mock import MagicMock
from uuid import uuid4
import pytest
from conftest import DEFAULT_URI # type: ignore
from psycopg import Connection
from langgraph.store.base import GetOp, Item, ListNamespacesOp, PutOp, SearchOp
from langgraph.store.base import (
GetOp,
Item,
ListNamespacesOp,
MatchCondition,
PutOp,
SearchOp,
)
from langgraph.store.postgres import PostgresStore
class MockCursor:
def __init__(self, fetch_result: Any) -> None:
self.fetch_result = fetch_result
self.execute = MagicMock()
self.fetchall = MagicMock(return_value=self.fetch_result)
@pytest.fixture(scope="function", params=["default", "pipe", "pool"])
def store(request) -> PostgresStore:
database = f"test_{uuid4().hex[:16]}"
uri_parts = DEFAULT_URI.split("/")
uri_base = "/".join(uri_parts[:-1])
query_params = ""
if "?" in uri_parts[-1]:
db_name, query_params = uri_parts[-1].split("?", 1)
query_params = "?" + query_params
conn_string = f"{uri_base}/{database}{query_params}"
admin_conn_string = DEFAULT_URI
class MockConnection:
def __init__(self) -> None:
self.cursor = MagicMock()
self.pipeline = MagicMock()
with Connection.connect(admin_conn_string, autocommit=True) as conn:
conn.execute(f"CREATE DATABASE {database}")
try:
with PostgresStore.from_conn_string(conn_string) as store:
store.setup()
@pytest.fixture
def mock_connection() -> MockConnection:
return MockConnection()
@pytest.fixture
def store(mock_connection: MockConnection) -> PostgresStore:
return PostgresStore(mock_connection)
if request.param == "pipe":
with PostgresStore.from_conn_string(conn_string, pipeline=True) as store:
yield store
elif request.param == "pool":
with PostgresStore.from_conn_string(
conn_string, pool_config={"min_size": 1, "max_size": 10}
) as store:
yield store
else: # default
with PostgresStore.from_conn_string(conn_string) as store:
yield store
finally:
with Connection.connect(admin_conn_string, autocommit=True) as conn:
conn.execute(f"DROP DATABASE {database}")
def test_batch_order(store: PostgresStore) -> None:
mock_connection = store.conn
mock_get_cursor = MockCursor(
[
{
"key": "key1",
"value": '{"data": "value1"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.foo",
},
{
"key": "key2",
"value": '{"data": "value2"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.bar",
},
]
)
mock_search_cursor = MockCursor(
[
{
"key": "key1",
"value": '{"data": "value1"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.foo",
},
]
)
mock_list_namespaces_cursor = MockCursor(
[
{"truncated_prefix": b"\x01test"},
]
)
failures = []
def cursor_side_effect(binary: bool = False) -> Any:
cursor = MagicMock()
def execute_side_effect(query: str, *params: Any) -> None:
# My super sophisticated database.
if "SELECT prefix, key, value" in query:
cursor.fetchall = mock_search_cursor.fetchall
elif "SELECT DISTINCT ON (truncated_prefix)" in query:
cursor.fetchall = mock_list_namespaces_cursor.fetchall
elif "WHERE prefix = %s AND key" in query:
cursor.fetchall = mock_get_cursor.fetchall
elif "INSERT INTO " in query:
pass
else:
e = ValueError(f"Unmatched query: {query}")
failures.append(e)
raise e
cursor.execute = MagicMock(side_effect=execute_side_effect)
return cursor
mock_connection.cursor.side_effect = cursor_side_effect
# Setup test data
store.put(("test", "foo"), "key1", {"data": "value1"})
store.put(("test", "bar"), "key2", {"data": "value2"})
ops = [
GetOp(namespace=("test",), key="key1"),
PutOp(namespace=("test",), key="key2", value={"data": "value2"}),
GetOp(namespace=("test", "foo"), key="key1"),
PutOp(namespace=("test", "bar"), key="key2", value={"data": "value2"}),
SearchOp(
namespace_prefix=("test",), filter={"data": "value1"}, limit=10, offset=0
),
ListNamespacesOp(match_conditions=None, max_depth=None, limit=10, offset=0),
GetOp(namespace=("test",), key="key3"),
]
results = store.batch(ops)
assert not failures
assert len(results) == 5
assert isinstance(results[0], Item)
assert isinstance(results[0].value, dict)
assert results[0].value == {"data": "value1"}
assert results[0].key == "key1"
assert results[1] is None
assert results[1] is None # Put operation returns None
assert isinstance(results[2], list)
assert len(results[2]) == 1
assert isinstance(results[3], list)
assert results[3] == [("test",)]
assert results[4] is None
assert len(results[3]) > 0 # Should contain at least our test namespaces
assert results[4] is None # Non-existent key returns None
# Test reordered operations
ops_reordered = [
SearchOp(namespace_prefix=("test",), filter=None, limit=5, offset=0),
GetOp(namespace=("test",), key="key2"),
GetOp(namespace=("test", "bar"), key="key2"),
ListNamespacesOp(match_conditions=None, max_depth=None, limit=5, offset=0),
PutOp(namespace=("test",), key="key3", value={"data": "value3"}),
GetOp(namespace=("test",), key="key1"),
GetOp(namespace=("test", "foo"), key="key1"),
]
results_reordered = store.batch(ops_reordered)
assert not failures
assert len(results_reordered) == 5
assert isinstance(results_reordered[0], list)
assert len(results_reordered[0]) == 1
assert len(results_reordered[0]) >= 2 # Should find at least our two test items
assert isinstance(results_reordered[1], Item)
assert results_reordered[1].value == {"data": "value2"}
assert results_reordered[1].key == "key2"
assert isinstance(results_reordered[2], list)
assert results_reordered[2] == [("test",)]
assert results_reordered[3] is None
assert len(results_reordered[2]) > 0
assert results_reordered[3] is None # Put operation returns None
assert isinstance(results_reordered[4], Item)
assert results_reordered[4].value == {"data": "value1"}
assert results_reordered[4].key == "key1"
def test_batch_get_ops(store: PostgresStore) -> None:
mock_connection = store.conn
mock_cursor = MockCursor(
[
{
"key": "key1",
"value": '{"data": "value1"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.foo",
},
{
"key": "key2",
"value": '{"data": "value2"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.bar",
},
]
)
mock_connection.cursor.return_value = mock_cursor
# Setup test data
store.put(("test",), "key1", {"data": "value1"})
store.put(("test",), "key2", {"data": "value2"})
ops = [
GetOp(namespace=("test",), key="key1"),
GetOp(namespace=("test",), key="key2"),
GetOp(namespace=("test",), key="key3"),
GetOp(namespace=("test",), key="key3"), # Non-existent key
]
results = store.batch(ops)
@@ -182,75 +126,90 @@ def test_batch_get_ops(store: PostgresStore) -> None:
def test_batch_put_ops(store: PostgresStore) -> None:
mock_connection = store.conn
mock_cursor = MockCursor([])
mock_connection.cursor.return_value = mock_cursor
ops = [
PutOp(namespace=("test",), key="key1", value={"data": "value1"}),
PutOp(namespace=("test",), key="key2", value={"data": "value2"}),
PutOp(namespace=("test",), key="key3", value=None),
PutOp(namespace=("test",), key="key3", value=None), # Delete operation
]
results = store.batch(ops)
assert len(results) == 3
assert all(result is None for result in results)
assert mock_cursor.execute.call_count == 2
# Verify the puts worked
item1 = store.get(("test",), "key1")
item2 = store.get(("test",), "key2")
item3 = store.get(("test",), "key3")
assert item1 and item1.value == {"data": "value1"}
assert item2 and item2.value == {"data": "value2"}
assert item3 is None
def test_batch_search_ops(store: PostgresStore) -> None:
mock_connection = store.conn
mock_cursor = MockCursor(
[
{
"key": "key1",
"value": '{"data": "value1"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.foo",
},
{
"key": "key2",
"value": '{"data": "value2"}',
"created_at": datetime.now(),
"updated_at": datetime.now(),
"prefix": "test.bar",
},
]
)
mock_connection.cursor.return_value = mock_cursor
# Setup test data
test_data = [
(("test", "foo"), "key1", {"data": "value1", "tag": "a"}),
(("test", "bar"), "key2", {"data": "value2", "tag": "a"}),
(("test", "baz"), "key3", {"data": "value3", "tag": "b"}),
]
for namespace, key, value in test_data:
store.put(namespace, key, value)
ops = [
SearchOp(
namespace_prefix=("test",), filter={"data": "value1"}, limit=10, offset=0
),
SearchOp(namespace_prefix=("test",), filter=None, limit=5, offset=0),
SearchOp(namespace_prefix=("test",), filter={"tag": "a"}, limit=10, offset=0),
SearchOp(namespace_prefix=("test",), filter=None, limit=2, offset=0),
SearchOp(namespace_prefix=("test", "foo"), filter=None, limit=10, offset=0),
]
results = store.batch(ops)
assert len(results) == 3
assert len(results) == 2
# First search should find items with tag "a"
assert len(results[0]) == 2
assert all(item.value["tag"] == "a" for item in results[0])
# Second search should return first 2 items
assert len(results[1]) == 2
# Third search should only find items in test/foo namespace
assert len(results[2]) == 1
assert results[2][0].namespace == ("test", "foo")
def test_batch_list_namespaces_ops(store: PostgresStore) -> None:
mock_connection = store.conn
mock_cursor = MockCursor(
[
{"truncated_prefix": b"\x01test.namespace1"},
{"truncated_prefix": b"\x01test.namespace2"},
]
)
mock_connection.cursor.return_value = mock_cursor
# Setup test data with various namespaces
test_data = [
(("test", "documents", "public"), "doc1", {"content": "public doc"}),
(("test", "documents", "private"), "doc2", {"content": "private doc"}),
(("test", "images", "public"), "img1", {"content": "public image"}),
(("prod", "documents", "public"), "doc3", {"content": "prod doc"}),
]
for namespace, key, value in test_data:
store.put(namespace, key, value)
ops = [ListNamespacesOp(match_conditions=None, max_depth=None, limit=10, offset=0)]
ops = [
ListNamespacesOp(match_conditions=None, max_depth=None, limit=10, offset=0),
ListNamespacesOp(match_conditions=None, max_depth=2, limit=10, offset=0),
ListNamespacesOp(
match_conditions=[MatchCondition("suffix", "public")],
max_depth=None,
limit=10,
offset=0,
),
]
results = store.batch(ops)
assert len(results) == 3
assert len(results) == 1
assert results[0] == [("test", "namespace1"), ("test", "namespace2")]
# First operation should list all namespaces
assert len(results[0]) == len(test_data)
# Second operation should only return namespaces up to depth 2
assert all(len(ns) <= 2 for ns in results[1])
# Third operation should only return namespaces ending with "public"
assert all(ns[-1] == "public" for ns in results[2])
class TestPostgresStore:
@@ -273,195 +232,111 @@ class TestPostgresStore:
assert item.key == item_id
assert item.value == item_value
updated_value = {
"title": "Updated Test Document",
"content": "Hello, LangGraph!",
}
# Test update
updated_value = {"title": "Updated Document", "content": "Hello, Updated!"}
store.put(namespace, item_id, updated_value)
updated_item = store.get(namespace, item_id)
assert updated_item.value == updated_value
assert updated_item.updated_at > item.updated_at
# Test get from non-existent namespace
different_namespace = ("test", "other_documents")
item_in_different_namespace = store.get(different_namespace, item_id)
assert item_in_different_namespace is None
new_item_id = "doc2"
new_item_value = {"title": "Another Document", "content": "Greetings!"}
store.put(namespace, new_item_id, new_item_value)
search_results = store.search(["test"], limit=10)
items = search_results
assert len(items) == 2
assert any(item.key == item_id for item in items)
assert any(item.key == new_item_id for item in items)
namespaces = store.list_namespaces(prefix=["test"])
assert ("test", "documents") in namespaces
# Test delete
store.delete(namespace, item_id)
store.delete(namespace, new_item_id)
deleted_item = store.get(namespace, item_id)
assert deleted_item is None
deleted_item = store.get(namespace, new_item_id)
assert deleted_item is None
empty_search_results = store.search(["test"], limit=10)
assert len(empty_search_results) == 0
def test_list_namespaces(self) -> None:
with PostgresStore.from_conn_string(DEFAULT_URI) as store:
test_pref = str(uuid.uuid4())
# Create test data with various namespaces
test_namespaces = [
(test_pref, "test", "documents", "public", test_pref),
(test_pref, "test", "documents", "private", test_pref),
(test_pref, "test", "images", "public", test_pref),
(test_pref, "test", "images", "private", test_pref),
(test_pref, "prod", "documents", "public", test_pref),
(
test_pref,
"prod",
"documents",
"some",
"nesting",
"public",
test_pref,
),
(test_pref, "prod", "documents", "private", test_pref),
("test", "documents", "public"),
("test", "documents", "private"),
("test", "images", "public"),
("test", "images", "private"),
("prod", "documents", "public"),
("prod", "documents", "private"),
]
# Insert test data
for namespace in test_namespaces:
store.put(namespace, "dummy", {"content": "dummy"})
prefix_result = store.list_namespaces(prefix=[test_pref, "test"])
assert len(prefix_result) == 4
assert all([ns[1] == "test" for ns in prefix_result])
# Test listing with various filters
all_namespaces = store.list_namespaces()
assert len(all_namespaces) == len(test_namespaces)
specific_prefix_result = store.list_namespaces(
prefix=[test_pref, "test", "documents"]
)
assert len(specific_prefix_result) == 2
assert all(
[ns[1:3] == ("test", "documents") for ns in specific_prefix_result]
)
# Test prefix filtering
test_prefix_namespaces = store.list_namespaces(prefix=["test"])
assert len(test_prefix_namespaces) == 4
assert all(ns[0] == "test" for ns in test_prefix_namespaces)
suffix_result = store.list_namespaces(suffix=["public", test_pref])
assert len(suffix_result) == 4
assert all(ns[-2] == "public" for ns in suffix_result)
# Test suffix filtering
public_namespaces = store.list_namespaces(suffix=["public"])
assert len(public_namespaces) == 3
assert all(ns[-1] == "public" for ns in public_namespaces)
prefix_suffix_result = store.list_namespaces(
prefix=[test_pref, "test"], suffix=["public", test_pref]
)
assert len(prefix_suffix_result) == 2
assert all(
ns[1] == "test" and ns[-2] == "public" for ns in prefix_suffix_result
)
# Test max depth
depth_2_namespaces = store.list_namespaces(max_depth=2)
assert all(len(ns) <= 2 for ns in depth_2_namespaces)
wildcard_prefix_result = store.list_namespaces(
prefix=[test_pref, "*", "documents"]
)
assert len(wildcard_prefix_result) == 5
assert all(ns[2] == "documents" for ns in wildcard_prefix_result)
wildcard_suffix_result = store.list_namespaces(
suffix=["*", "public", test_pref]
)
assert len(wildcard_suffix_result) == 4
assert all(ns[-2] == "public" for ns in wildcard_suffix_result)
wildcard_single = store.list_namespaces(
suffix=["some", "*", "public", test_pref]
)
assert len(wildcard_single) == 1
assert wildcard_single[0] == (
test_pref,
"prod",
"documents",
"some",
"nesting",
"public",
test_pref,
)
max_depth_result = store.list_namespaces(max_depth=3)
assert all([len(ns) <= 3 for ns in max_depth_result])
max_depth_result = store.list_namespaces(
max_depth=4, prefix=[test_pref, "*", "documents"]
)
assert (
len(set(tuple(res) for res in max_depth_result))
== len(max_depth_result)
== 5
)
limit_result = store.list_namespaces(prefix=[test_pref], limit=3)
assert len(limit_result) == 3
offset_result = store.list_namespaces(prefix=[test_pref], offset=3)
assert len(offset_result) == len(test_namespaces) - 3
empty_prefix_result = store.list_namespaces(prefix=[test_pref])
assert len(empty_prefix_result) == len(test_namespaces)
assert set(tuple(ns) for ns in empty_prefix_result) == set(
tuple(ns) for ns in test_namespaces
)
# Test pagination
paginated_namespaces = store.list_namespaces(limit=3)
assert len(paginated_namespaces) == 3
# Cleanup
for namespace in test_namespaces:
store.delete(namespace, "dummy")
def test_search(self):
def test_search(self) -> None:
with PostgresStore.from_conn_string(DEFAULT_URI) as store:
test_namespaces = [
("test_search", "documents", "user1"),
("test_search", "documents", "user2"),
("test_search", "reports", "department1"),
("test_search", "reports", "department2"),
]
test_items = [
{"title": "Doc 1", "author": "John Doe", "tags": ["important"]},
{"title": "Doc 2", "author": "Jane Smith", "tags": ["draft"]},
{"title": "Report A", "author": "John Doe", "tags": ["final"]},
{"title": "Report B", "author": "Alice Johnson", "tags": ["draft"]},
# Create test data
test_data = [
(
("test", "docs"),
"doc1",
{"title": "First Doc", "author": "Alice", "tags": ["important"]},
),
(
("test", "docs"),
"doc2",
{"title": "Second Doc", "author": "Bob", "tags": ["draft"]},
),
(
("test", "images"),
"img1",
{"title": "Image 1", "author": "Alice", "tags": ["final"]},
),
]
for namespace, item in zip(test_namespaces, test_items):
store.put(namespace, f"item_{namespace[-1]}", item)
for namespace, key, value in test_data:
store.put(namespace, key, value)
docs_result = store.search(["test_search", "documents"])
assert len(docs_result) == 2
assert all(
[item.namespace[1] == "documents" for item in docs_result]
), docs_result
# Test basic search
all_items = store.search(["test"])
assert len(all_items) == 3
reports_result = store.search(["test_search", "reports"])
assert len(reports_result) == 2
assert all(item.namespace[1] == "reports" for item in reports_result)
# Test namespace filtering
docs_items = store.search(["test", "docs"])
assert len(docs_items) == 2
assert all(item.namespace == ("test", "docs") for item in docs_items)
limited_result = store.search(["test_search"], limit=2)
assert len(limited_result) == 2
offset_result = store.search(["test_search"])
assert len(offset_result) == 4
# Test value filtering
alice_items = store.search(["test"], filter={"author": "Alice"})
assert len(alice_items) == 2
assert all(item.value["author"] == "Alice" for item in alice_items)
offset_result = store.search(["test_search"], offset=2)
assert len(offset_result) == 2
assert all(item not in limited_result for item in offset_result)
# Test pagination
paginated_items = store.search(["test"], limit=2)
assert len(paginated_items) == 2
john_doe_result = store.search(
["test_search"], filter={"author": "John Doe"}
)
assert len(john_doe_result) == 2
assert all(item.value["author"] == "John Doe" for item in john_doe_result)
offset_items = store.search(["test"], offset=2)
assert len(offset_items) == 1
draft_result = store.search(["test_search"], filter={"tags": ["draft"]})
assert len(draft_result) == 2
assert all("draft" in item.value["tags"] for item in draft_result)
page1 = store.search(["test_search"], limit=2, offset=0)
page2 = store.search(["test_search"], limit=2, offset=2)
all_items = page1 + page2
assert len(all_items) == 4
assert len(set(item.key for item in all_items)) == 4
for namespace in test_namespaces:
store.delete(namespace, f"item_{namespace[-1]}")
# Cleanup
for namespace, key, _ in test_data:
store.delete(namespace, key)
@@ -372,7 +372,7 @@ class MemorySaver(
RunnableConfig: The updated config containing the saved writes' timestamp.
"""
thread_id = config["configurable"]["thread_id"]
checkpoint_ns = config["configurable"]["checkpoint_ns"]
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
checkpoint_id = config["configurable"]["checkpoint_id"]
outer_key = (thread_id, checkpoint_ns, checkpoint_id)
outer_writes_ = self.writes.get(outer_key)
+77 -1
View File
@@ -6,6 +6,9 @@ from langgraph.store.base import (
BaseStore,
GetOp,
Item,
ListNamespacesOp,
MatchCondition,
NameSpacePath,
Op,
PutOp,
SearchOp,
@@ -68,6 +71,74 @@ class AsyncBatchedBaseStore(BaseStore):
self._aqueue[fut] = PutOp(namespace, key, None)
return await fut
async def alist_namespaces(
self,
*,
prefix: Optional[NameSpacePath] = None,
suffix: Optional[NameSpacePath] = None,
max_depth: Optional[int] = None,
limit: int = 100,
offset: int = 0,
) -> list[tuple[str, ...]]:
fut = self._loop.create_future()
match_conditions = []
if prefix:
match_conditions.append(MatchCondition(match_type="prefix", path=prefix))
if suffix:
match_conditions.append(MatchCondition(match_type="suffix", path=suffix))
op = ListNamespacesOp(
match_conditions=tuple(match_conditions),
max_depth=max_depth,
limit=limit,
offset=offset,
)
self._aqueue[fut] = op
return await fut
def _dedupe_ops(values: list[Op]) -> tuple[Optional[list[int]], list[Op]]:
"""Dedupe operations while preserving order for results.
Args:
values: List of operations to dedupe
Returns:
Tuple of (listen indices, deduped operations)
where listen indices map deduped operation results back to original positions
"""
if len(values) <= 1:
return None, list(values)
dedupped: list[Op] = []
listen: list[int] = []
puts: dict[tuple[tuple[str, ...], str], int] = {}
for op in values:
if isinstance(op, (GetOp, SearchOp, ListNamespacesOp)):
try:
listen.append(dedupped.index(op))
except ValueError:
listen.append(len(dedupped))
dedupped.append(op)
elif isinstance(op, PutOp):
putkey = (op.namespace, op.key)
if putkey in puts:
# Overwrite previous put
ix = puts[putkey]
dedupped[ix] = op
listen.append(ix)
else:
puts[putkey] = len(dedupped)
listen.append(len(dedupped))
dedupped.append(op)
else: # Any new ops will be treated regularly
listen.append(len(dedupped))
dedupped.append(op)
return listen, dedupped
async def _run(
aqueue: dict[asyncio.Future, Op], store: weakref.ReferenceType[BaseStore]
@@ -81,7 +152,12 @@ async def _run(
taken = aqueue.copy()
# action each operation
try:
results = await s.abatch(taken.values())
values = list(taken.values())
listen, dedupped = _dedupe_ops(values)
results = await s.abatch(dedupped)
if listen is not None:
results = [results[ix] for ix in listen]
# set the results of each operation
for fut, result in zip(taken, results):
fut.set_result(result)
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint"
version = "2.0.5"
version = "2.0.6"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
license = "MIT"
+77 -11
View File
@@ -10,6 +10,18 @@ from langgraph.store.base.batch import AsyncBatchedBaseStore
from langgraph.store.memory import InMemoryStore
class MockAsyncBatchedStore(AsyncBatchedBaseStore):
def __init__(self) -> None:
super().__init__()
self._store = InMemoryStore()
def batch(self, ops: Iterable[Op]) -> list[Result]:
return self._store.batch(ops)
async def abatch(self, ops: Iterable[Op]) -> list[Result]:
return self._store.batch(ops)
async def test_async_batch_store(mocker: MockerFixture) -> None:
abatch = mocker.stub()
@@ -313,17 +325,6 @@ async def test_cannot_put_empty_namespace() -> None:
store.delete(("langgraph", "foo"), "bar")
assert store.get(("langgraph", "foo"), "bar") is None
class MockAsyncBatchedStore(AsyncBatchedBaseStore):
def __init__(self) -> None:
super().__init__()
self._store = InMemoryStore()
def batch(self, ops: Iterable[Op]) -> list[Result]:
return self._store.batch(ops)
async def abatch(self, ops: Iterable[Op]) -> list[Result]:
return self._store.batch(ops)
async_store = MockAsyncBatchedStore()
doc = {"foo": "bar"}
@@ -354,3 +355,68 @@ async def test_cannot_put_empty_namespace() -> None:
assert (await async_store.asearch(("valid", "namespace")))[0].value == doc
await async_store.adelete(("valid", "namespace"), "key")
assert (await async_store.aget(("valid", "namespace"), "key")) is None
async def test_async_batch_store_deduplication(mocker: MockerFixture) -> None:
abatch = mocker.spy(InMemoryStore, "batch")
store = MockAsyncBatchedStore()
same_doc = {"value": "same"}
diff_doc = {"value": "different"}
await asyncio.gather(
store.aput(namespace=("test",), key="same", value=same_doc),
store.aput(namespace=("test",), key="different", value=diff_doc),
)
abatch.reset_mock()
results = await asyncio.gather(
store.aget(namespace=("test",), key="same"),
store.aget(namespace=("test",), key="same"),
store.aget(namespace=("test",), key="different"),
)
assert len(results) == 3
assert results[0] == results[1]
assert results[0] != results[2]
assert results[0].value == same_doc # type: ignore
assert results[2].value == diff_doc # type: ignore
assert len(abatch.call_args_list) == 1
ops = list(abatch.call_args_list[0].args[1])
assert len(ops) == 2
assert GetOp(("test",), "same") in ops
assert GetOp(("test",), "different") in ops
abatch.reset_mock()
doc1 = {"value": 1}
doc2 = {"value": 2}
results = await asyncio.gather(
store.aput(namespace=("test",), key="key", value=doc1),
store.aput(namespace=("test",), key="key", value=doc2),
)
assert len(abatch.call_args_list) == 1
ops = list(abatch.call_args_list[0].args[1])
assert len(ops) == 1
assert ops[0] == PutOp(("test",), "key", doc2)
assert len(results) == 2
assert all(result is None for result in results)
result = await store.aget(namespace=("test",), key="key")
assert result is not None
assert result.value == doc2
abatch.reset_mock()
results = await asyncio.gather(
store.asearch(("test",), filter={"value": 2}),
store.asearch(("test",), filter={"value": 2}),
)
assert len(abatch.call_args_list) == 1
ops = list(abatch.call_args_list[0].args[1])
assert len(ops) == 1
assert len(results) == 2
assert results[0] == results[1]
assert len(results[0]) == 1
assert results[0][0].value == doc2
abatch.reset_mock()
+103 -8
View File
@@ -1,10 +1,105 @@
# langchain-cli
# LangGraph CLI
This package implements the official CLI for LangGraph API.
The official command-line interface for LangGraph, providing tools to create, develop, and deploy LangGraph applications.
## How to Test CLI Changes Locally
These instructions are for CLI development and testing. Use the CLI examples to test CLI changes locally.
1. Make changes to the CLI code.
1. Navigate to the `libs/cli/examples`: `cd libs/cli/examples`
1. Install CLI examples dependencies: `poetry install`
1. Run/test CLI command (e.g. `langgraph build`).
## Installation
Install via pip:
```bash
pip install langgraph-cli
```
For development mode with hot reloading:
```bash
pip install "langgraph-cli[inmem]"
```
## Commands
### `langgraph new` 🌱
Create a new LangGraph project from a template
```bash
langgraph new [PATH] --template TEMPLATE_NAME
```
### `langgraph dev` 🏃‍♀️
Run LangGraph API server in development mode with hot reloading
```bash
langgraph dev [OPTIONS]
--host TEXT Host to bind to (default: 127.0.0.1)
--port INTEGER Port to bind to (default: 2024)
--no-reload Disable auto-reload
--debug-port INTEGER Enable remote debugging
--no-browser Skip opening browser window
-c, --config FILE Config file path (default: langgraph.json)
```
### `langgraph up` 🚀
Launch LangGraph API server in Docker
```bash
langgraph up [OPTIONS]
-p, --port INTEGER Port to expose (default: 8123)
--wait Wait for services to start
--watch Restart on file changes
--verbose Show detailed logs
-c, --config FILE Config file path
-d, --docker-compose Additional services file
```
### `langgraph build`
Build a Docker image for your LangGraph application
```bash
langgraph build -t IMAGE_TAG [OPTIONS]
--platform TEXT Target platforms (e.g., linux/amd64,linux/arm64)
--pull / --no-pull Use latest/local base image
-c, --config FILE Config file path
```
### `langgraph dockerfile`
Generate a Dockerfile for custom deployments
```bash
langgraph dockerfile SAVE_PATH [OPTIONS]
-c, --config FILE Config file path
```
## Configuration
The CLI uses a `langgraph.json` configuration file with these key settings:
```json
{
"dependencies": ["langchain_openai", "./your_package"], // Required: Package dependencies
"graphs": {
"my_graph": "./your_package/file.py:graph" // Required: Graph definitions
},
"env": "./.env", // Optional: Environment variables
"python_version": "3.11", // Optional: Python version (3.11/3.12)
"pip_config_file": "./pip.conf", // Optional: pip configuration
"dockerfile_lines": [] // Optional: Additional Dockerfile commands
}
```
See the [full documentation](https://langchain-ai.github.io/langgraph/docs/cloud/reference/cli.html) for detailed configuration options.
## Development
To develop the CLI itself:
1. Clone the repository
2. Navigate to the CLI directory: `cd libs/cli`
3. Install development dependencies: `poetry install`
4. Make your changes to the CLI code
5. Test your changes:
```bash
# Run CLI commands directly
poetry run langgraph --help
# Or use the examples
cd examples
poetry install
poetry run langgraph dev # or other commands
```
## License
This project is licensed under the terms specified in the repository's LICENSE file.
+3 -3
View File
@@ -1299,9 +1299,9 @@ create-jest@^29.7.0:
prompts "^2.0.1"
cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
+122 -22
View File
@@ -1,4 +1,4 @@
import json
import os
import pathlib
import shutil
import sys
@@ -45,7 +45,7 @@ OPT_CONFIG = click.option(
- "graphs": mapping from graph ID to path where the compiled graph is defined, i.e. ./your_package/your_file.py:variable, where
"variable" is an instance of langgraph.graph.graph.CompiledGraph
- "env": (optional) path to .env file or a mapping from environment variable to its value
- "python_version": (optional) 3.11 or 3.12. Defaults to 3.11
- "python_version": (optional) 3.11, 3.12, or 3.13. Defaults to 3.11
- "pip_config_file": (optional) path to pip config file
- "dockerfile_lines": (optional) array of additional lines to add to Dockerfile following the import from parent image
@@ -190,7 +190,6 @@ def up(
click.secho(
"""For local dev, requires env var LANGSMITH_API_KEY with access to LangGraph Cloud closed beta.
For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KEY.""",
fg="red",
)
with Runner() as runner, Progress(message="Pulling...") as set:
capabilities = langgraph_cli.docker.check_capabilities(runner)
@@ -285,9 +284,11 @@ def _build(
subp_exec(
"docker",
"pull",
f"{base_image}:{config_json['node_version']}"
if config_json.get("node_version")
else f"{base_image}:{config_json['python_version']}",
(
f"{base_image}:{config_json['node_version']}"
if config_json.get("node_version")
else f"{base_image}:{config_json['python_version']}"
),
verbose=True,
)
)
@@ -352,8 +353,7 @@ def build(
with Runner() as runner, Progress(message="Pulling...") as set:
if shutil.which("docker") is None:
raise click.UsageError("Docker not installed") from None
with open(config) as f:
config_json = langgraph_cli.config.validate_config(json.load(f))
config_json = langgraph_cli.config.validate_config_file(config)
_build(
runner, set, config, config_json, base_image, pull, tag, docker_build_args
)
@@ -433,8 +433,7 @@ tests
def dockerfile(save_path: str, config: pathlib.Path, add_docker_compose: bool) -> None:
save_path = pathlib.Path(save_path).absolute()
secho(f"🔍 Validating configuration at path: {config}", fg="yellow")
with open(config, encoding="utf-8") as f:
config_json = langgraph_cli.config.validate_config(json.load(f))
config_json = langgraph_cli.config.validate_config_file(config)
secho("✅ Configuration validated!", fg="green")
secho(f"📝 Generating Dockerfile at {save_path}", fg="yellow")
@@ -443,9 +442,11 @@ def dockerfile(save_path: str, config: pathlib.Path, add_docker_compose: bool) -
langgraph_cli.config.config_to_docker(
config,
config_json,
"langchain/langgraphjs-api"
if config_json.get("node_version")
else "langchain/langgraph-api",
(
"langchain/langgraphjs-api"
if config_json.get("node_version")
else "langchain/langgraph-api"
),
)
)
secho("✅ Created: Dockerfile", fg="green")
@@ -523,6 +524,102 @@ def new(path: Optional[str], template: Optional[str]) -> None:
return create_new(path, template)
@click.option(
"--host",
default="127.0.0.1",
help="Network interface to bind the development server to. Default 127.0.0.1 is recommended for security. Only use 0.0.0.0 in trusted networks",
)
@click.option(
"--port",
default=2024,
type=int,
help="Port number to bind the development server to. Example: langgraph dev --port 8000",
)
@click.option(
"--no-reload",
is_flag=True,
help="Disable automatic reloading when code changes are detected",
)
@click.option(
"--config",
type=click.Path(exists=True),
default="langgraph.json",
help="Path to configuration file declaring dependencies, graphs and environment variables",
)
@click.option(
"--n-jobs-per-worker",
default=None,
type=int,
help="Maximum number of concurrent jobs each worker process can handle. Default: 10",
)
@click.option(
"--no-browser",
is_flag=True,
help="Skip automatically opening the browser when the server starts",
)
@click.option(
"--debug-port",
default=None,
type=int,
help="Enable remote debugging by listening on specified port. Requires debugpy to be installed",
)
@cli.command(
"dev",
help="🏃‍♀️‍➡️ Run LangGraph API server in development mode with hot reloading and debugging support",
)
@log_command
def dev(
host: str,
port: int,
no_reload: bool,
config: pathlib.Path,
n_jobs_per_worker: Optional[int],
no_browser: bool,
debug_port: Optional[int],
):
"""CLI entrypoint for running the LangGraph API server."""
try:
from langgraph_api.cli import run_server
except ImportError:
try:
import pkg_resources
pkg_resources.require("langgraph-api-inmem")
except (ImportError, pkg_resources.DistributionNotFound):
raise click.UsageError(
"Required package 'langgraph-api-inmem' is not installed.\n"
"Please install it with:\n\n"
' pip install -U "langgraph-cli[inmem]"\n\n'
"If you're developing the langgraph-cli package locally, you can install in development mode:\n"
" pip install -e ."
) from None
raise click.UsageError(
"Could not import run_server. This likely means your installation is incomplete.\n"
"Please ensure langgraph-cli is installed with the 'inmem' extra: pip install -U \"langgraph-cli[inmem]\""
) from None
config_json = langgraph_cli.config.validate_config_file(config)
cwd = os.getcwd()
sys.path.append(cwd)
dependencies = config_json.get("dependencies", [])
for dep in dependencies:
dep_path = pathlib.Path(cwd) / dep
if dep_path.is_dir() and dep_path.exists():
sys.path.append(str(dep_path))
graphs = config_json.get("graphs", {})
run_server(
host,
port,
not no_reload,
graphs,
n_jobs_per_worker=n_jobs_per_worker,
open_browser=not no_browser,
debug_port=debug_port,
env=config_json.get("env", None),
)
def prepare_args_and_stdin(
*,
capabilities: DockerCapabilities,
@@ -556,9 +653,11 @@ def prepare_args_and_stdin(
config_path,
config,
watch=watch,
base_image="langchain/langgraphjs-api"
if config.get("node_version")
else "langchain/langgraph-api",
base_image=(
"langchain/langgraphjs-api"
if config.get("node_version")
else "langchain/langgraph-api"
),
)
return args, stdin
@@ -577,17 +676,18 @@ def prepare(
debugger_base_url: Optional[str] = None,
postgres_uri: Optional[str] = None,
):
with open(config_path) as f:
config = langgraph_cli.config.validate_config(json.load(f))
config_json = langgraph_cli.config.validate_config_file(config_path)
# pull latest images
if pull:
runner.run(
subp_exec(
"docker",
"pull",
f"langchain/langgraphjs-api:{config['node_version']}"
if config.get("node_version")
else f"langchain/langgraph-api:{config['python_version']}",
(
f"langchain/langgraphjs-api:{config_json['node_version']}"
if config_json.get("node_version")
else f"langchain/langgraph-api:{config_json['python_version']}"
),
verbose=verbose,
)
)
@@ -595,7 +695,7 @@ def prepare(
args, stdin = prepare_args_and_stdin(
capabilities=capabilities,
config_path=config_path,
config=config,
config=config_json,
docker_compose=docker_compose,
port=port,
watch=watch,
+90 -10
View File
@@ -6,6 +6,9 @@ from typing import NamedTuple, Optional, TypedDict, Union
import click
MIN_NODE_VERSION = "20"
MIN_PYTHON_VERSION = "3.11"
class Config(TypedDict):
python_version: str
@@ -17,6 +20,28 @@ class Config(TypedDict):
env: Union[dict[str, str], str]
def _parse_version(version_str: str) -> tuple[int, int]:
"""Parse a version string into a tuple of (major, minor)."""
try:
major, minor = map(int, version_str.split("."))
return (major, minor)
except ValueError:
raise click.UsageError(f"Invalid version format: {version_str}") from None
def _parse_node_version(version_str: str) -> int:
"""Parse a Node.js version string into a major version number."""
try:
if "." in version_str:
raise ValueError("Node.js version must be major version only")
return int(version_str)
except ValueError:
raise click.UsageError(
f"Invalid Node.js version format: {version_str}. "
"Use major version only (e.g., '20')."
) from None
def validate_config(config: Config) -> Config:
config = (
{
@@ -37,21 +62,34 @@ def validate_config(config: Config) -> Config:
)
if config.get("node_version"):
if config["node_version"] not in ("20",):
raise click.UsageError(
f"Unsupported Node.js version: {config['node_version']}. "
"Currently only `node_version: \"20\"` is supported."
)
node_version = config["node_version"]
try:
major = _parse_node_version(node_version)
min_major = _parse_node_version(MIN_NODE_VERSION)
if major < min_major:
raise click.UsageError(
f"Node.js version {node_version} is not supported. "
f"Minimum required version is {MIN_NODE_VERSION}."
)
except ValueError as e:
raise click.UsageError(str(e)) from None
if config.get("python_version"):
if config["python_version"] not in (
"3.11",
"3.12",
pyversion = config["python_version"]
if not pyversion.count(".") == 1 or not all(
part.isdigit() for part in pyversion.split(".")
):
raise click.UsageError(
f"Unsupported Python version: {config['python_version']}. "
"Supported versions are 3.11 and 3.12."
f"Invalid Python version format: {pyversion}. "
"Use 'major.minor' format (e.g., '3.11'). "
"Patch version cannot be specified."
)
if _parse_version(pyversion) < _parse_version(MIN_PYTHON_VERSION):
raise click.UsageError(
f"Python version {pyversion} is not supported. "
f"Minimum required version is {MIN_PYTHON_VERSION}."
)
if not config["dependencies"]:
raise click.UsageError(
"No dependencies found in config. "
@@ -66,6 +104,48 @@ def validate_config(config: Config) -> Config:
return config
def validate_config_file(config_path: pathlib.Path) -> Config:
with open(config_path) as f:
config = json.load(f)
validated = validate_config(config)
# Enforce the package.json doesn't enforce an
# incompatible Node.js version
if validated.get("node_version"):
package_json_path = config_path.parent / "package.json"
if package_json_path.is_file():
try:
with open(package_json_path) as f:
package_json = json.load(f)
if "engines" in package_json:
engines = package_json["engines"]
if any(engine != "node" for engine in engines.keys()):
raise click.UsageError(
"Only 'node' engine is supported in package.json engines."
f" Got engines: {list(engines.keys())}"
)
if engines:
node_version = engines["node"]
try:
major = _parse_node_version(node_version)
min_major = _parse_node_version(MIN_NODE_VERSION)
if major < min_major:
raise click.UsageError(
f"Node.js version in package.json engines must be >= {MIN_NODE_VERSION} "
f"(major version only), got '{node_version}'. Minor/patch versions "
"(like '20.x.y') are not supported to prevent deployment issues "
"when new Node.js versions are released."
)
except ValueError as e:
raise click.UsageError(str(e)) from None
except json.JSONDecodeError:
raise click.UsageError(
"Invalid package.json found in langgraph "
f"config directory {package_json_path}: file is not valid JSON"
) from None
return validated
class LocalDeps(NamedTuple):
pip_reqs: list[tuple[pathlib.Path, str]]
real_pkgs: dict[pathlib.Path, str]
+1295 -111
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-cli"
version = "0.1.54"
version = "0.1.59"
description = "CLI for interacting with LangGraph API"
authors = []
license = "MIT"
@@ -14,6 +14,7 @@ langgraph = "langgraph_cli.cli:cli"
[tool.poetry.dependencies]
python = "^3.9.0,<4.0"
click = "^8.1.7"
langgraph-api = { version = ">=0.0.2,<0.1.0", optional = true , python=">=3.11,<4.0" }
[tool.poetry.group.dev.dependencies]
ruff = "^0.6.2"
@@ -24,6 +25,9 @@ pytest-mock = "^3.11.1"
pytest-watch = "^4.2.0"
mypy = "^1.10.0"
[tool.poetry.extras]
inmem = ["langgraph-api"]
[tool.pytest.ini_options]
# --strict-markers will raise errors on unknown marks.
# https://docs.pytest.org/en/7.1.x/how-to/mark.html#raising-errors-on-unknown-marks
+91 -1
View File
@@ -1,10 +1,17 @@
import json
import os
import pathlib
import tempfile
import click
import pytest
from langgraph_cli.config import config_to_compose, config_to_docker, validate_config
from langgraph_cli.config import (
config_to_compose,
config_to_docker,
validate_config,
validate_config_file,
)
from langgraph_cli.util import clean_empty_lines
PATH_TO_CONFIG = pathlib.Path(__file__).parent / "test_config.json"
@@ -42,6 +49,9 @@ def test_validate_config():
}
actual_config = validate_config(expected_config)
assert actual_config == expected_config
expected_config["python_version"] = "3.13"
actual_config = validate_config(expected_config)
assert actual_config == expected_config
# check wrong python version raises
with pytest.raises(click.UsageError):
@@ -61,6 +71,86 @@ def test_validate_config():
with pytest.raises(click.UsageError):
validate_config({"python_version": "3.9", "dependencies": ["."]})
with pytest.raises(click.UsageError) as exc_info:
validate_config({"python_version": "3.11.0"})
assert "Invalid Python version format" in str(exc_info.value)
with pytest.raises(click.UsageError) as exc_info:
validate_config({"python_version": "3"})
assert "Invalid Python version format" in str(exc_info.value)
with pytest.raises(click.UsageError) as exc_info:
validate_config({"python_version": "abc.def"})
assert "Invalid Python version format" in str(exc_info.value)
with pytest.raises(click.UsageError) as exc_info:
validate_config({"python_version": "3.10"})
assert "Minimum required version" in str(exc_info.value)
def test_validate_config_file():
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir_path = pathlib.Path(tmpdir)
config_path = tmpdir_path / "langgraph.json"
node_config = {"node_version": "20", "graphs": {"agent": "./agent.js:graph"}}
with open(config_path, "w") as f:
json.dump(node_config, f)
validate_config_file(config_path)
package_json = {"name": "test", "engines": {"node": "20"}}
with open(tmpdir_path / "package.json", "w") as f:
json.dump(package_json, f)
validate_config_file(config_path)
package_json["engines"]["node"] = "20.18"
with open(tmpdir_path / "package.json", "w") as f:
json.dump(package_json, f)
with pytest.raises(click.UsageError, match="Use major version only"):
validate_config_file(config_path)
package_json["engines"] = {"node": "18"}
with open(tmpdir_path / "package.json", "w") as f:
json.dump(package_json, f)
with pytest.raises(click.UsageError, match="must be >= 20"):
validate_config_file(config_path)
package_json["engines"] = {"node": "20", "deno": "1.0"}
with open(tmpdir_path / "package.json", "w") as f:
json.dump(package_json, f)
with pytest.raises(click.UsageError, match="Only 'node' engine is supported"):
validate_config_file(config_path)
with open(tmpdir_path / "package.json", "w") as f:
f.write("{invalid json")
with pytest.raises(click.UsageError, match="Invalid package.json"):
validate_config_file(config_path)
python_config = {
"python_version": "3.11",
"dependencies": ["."],
"graphs": {"agent": "./agent.py:graph"},
}
with open(config_path, "w") as f:
json.dump(python_config, f)
validate_config_file(config_path)
for package_content in [
{"name": "test"},
{"engines": {"node": "18"}},
{"engines": {"node": "20", "deno": "1.0"}},
"{invalid json",
]:
with open(tmpdir_path / "package.json", "w") as f:
if isinstance(package_content, dict):
json.dump(package_content, f)
else:
f.write(package_content)
validate_config_file(config_path)
# config_to_docker
def test_config_to_docker_simple():
+6 -1
View File
@@ -48,8 +48,13 @@ test:
make stop-postgres; \
exit $$EXIT_CODE
WORKERS ?= auto
XDIST_ARGS := $(if $(WORKERS),-n $(WORKERS) --dist worksteal,)
MAXFAIL ?=
MAXFAIL_ARGS := $(if $(MAXFAIL),--maxfail $(MAXFAIL),)
test_watch:
make start-postgres && poetry run ptw . -- --ff -vv -x -n auto --dist worksteal --snapshot-update --tb short $(TEST); \
make start-postgres && poetry run ptw . -- --ff -vv -x $(XDIST_ARGS) $(MAXFAIL_ARGS) --snapshot-update --tb short $(TEST); \
EXIT_CODE=$$?; \
make stop-postgres; \
exit $$EXIT_CODE
+1 -1
View File
@@ -238,7 +238,7 @@ final_state["messages"][-1].content
* [How-to Guides](https://langchain-ai.github.io/langgraph/how-tos/): Accomplish specific things within LangGraph, from streaming, to adding memory & persistence, to common design patterns (branching, subgraphs, etc.), these are the place to go if you want to copy and run a specific code snippet.
* [Conceptual Guides](https://langchain-ai.github.io/langgraph/concepts/high_level/): In-depth explanations of the key concepts and principles behind LangGraph, such as nodes, edges, state and more.
* [API Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Review important classes and methods, simple examples of how to use the graph and checkpointing APIs, higher-level prebuilt components and more.
* [Cloud (beta)](https://langchain-ai.github.io/langgraph/cloud/): With one click, deploy LangGraph applications to LangGraph Cloud.
* [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/#langgraph-platform): LangGraph Platform is a commercial solution for deploying agentic applications in production, built on the open-source LangGraph framework.
## Contributing
+14 -3
View File
@@ -2,7 +2,7 @@ from enum import Enum
from typing import Any, Sequence
from langgraph.checkpoint.base import EmptyChannelError # noqa: F401
from langgraph.types import Interrupt
from langgraph.types import Command, Interrupt
# EmptyChannelError re-exported for backwards compatibility
@@ -58,7 +58,11 @@ class InvalidUpdateError(Exception):
pass
class GraphInterrupt(Exception):
class GraphBubbleUp(Exception):
pass
class GraphInterrupt(GraphBubbleUp):
"""Raised when a subgraph is interrupted, suppressed by the root graph.
Never raised directly, or surfaced to the user."""
@@ -73,13 +77,20 @@ class NodeInterrupt(GraphInterrupt):
super().__init__([Interrupt(value=value)])
class GraphDelegate(Exception):
class GraphDelegate(GraphBubbleUp):
"""Raised when a graph is delegated (for distributed mode)."""
def __init__(self, *args: dict[str, Any]) -> None:
super().__init__(*args)
class ParentCommand(GraphBubbleUp):
args: tuple[Command]
def __init__(self, command: Command) -> None:
super().__init__(command)
class EmptyInputError(Exception):
"""Raised when graph receives an empty input."""
+18 -2
View File
@@ -37,7 +37,12 @@ from langgraph.channels.ephemeral_value import EphemeralValue
from langgraph.channels.last_value import LastValue
from langgraph.channels.named_barrier_value import NamedBarrierValue
from langgraph.constants import EMPTY_SEQ, NS_END, NS_SEP, SELF, TAG_HIDDEN
from langgraph.errors import ErrorCode, InvalidUpdateError, create_error_message
from langgraph.errors import (
ErrorCode,
InvalidUpdateError,
ParentCommand,
create_error_message,
)
from langgraph.graph.graph import END, START, Branch, CompiledGraph, Graph, Send
from langgraph.managed.base import (
ChannelKeyPlaceholder,
@@ -623,20 +628,27 @@ class CompiledStateGraph(CompiledGraph):
def _get_root(input: Any) -> Any:
if isinstance(input, Command):
if input.graph == Command.PARENT:
return SKIP_WRITE
return input.update
else:
return input
# to avoid name collision below
node_key = key
def _get_state_key(input: Union[None, dict, Any], *, key: str) -> Any:
if input is None:
return SKIP_WRITE
elif isinstance(input, dict):
if all(k not in output_keys for k in input):
raise InvalidUpdateError(
f"Expected node {key} to update at least one of {output_keys}, got {input}"
f"Expected node {node_key} to update at least one of {output_keys}, got {input}"
)
return input.get(key, SKIP_WRITE)
elif isinstance(input, Command):
if input.graph == Command.PARENT:
return SKIP_WRITE
return _get_state_key(input.update, key=key)
elif get_type_hints(type(input)):
value = getattr(input, key, SKIP_WRITE)
@@ -819,6 +831,8 @@ def _control_branch(value: Any) -> Sequence[Union[str, Send]]:
return [value]
if not isinstance(value, GraphCommand):
return EMPTY_SEQ
if value.graph == Command.PARENT:
raise ParentCommand(value)
rtn: list[Union[str, Send]] = []
if isinstance(value.goto, str):
rtn.append(value.goto)
@@ -836,6 +850,8 @@ async def _acontrol_branch(value: Any) -> Sequence[Union[str, Send]]:
return [value]
if not isinstance(value, GraphCommand):
return EMPTY_SEQ
if value.graph == Command.PARENT:
raise ParentCommand(value)
rtn: list[Union[str, Send]] = []
if isinstance(value.goto, str):
rtn.append(value.goto)
@@ -37,7 +37,7 @@ from langchain_core.tools import tool as create_tool
from langchain_core.tools.base import get_all_basemodel_annotations
from typing_extensions import Annotated, get_args, get_origin
from langgraph.errors import GraphInterrupt
from langgraph.errors import GraphBubbleUp
from langgraph.store.base import BaseStore
from langgraph.utils.runnable import RunnableCallable
@@ -275,7 +275,7 @@ class ToolNode(RunnableCallable):
# (2) a NodeInterrupt is raised inside a graph node for a graph called as a tool
# (3) a GraphInterrupt is raised when a subgraph is interrupted inside a graph called as a tool
# (2 and 3 can happen in a "supervisor w/ tools" multi-agent architecture)
except GraphInterrupt as e:
except GraphBubbleUp as e:
raise e
except Exception as e:
if isinstance(self.handle_tool_errors, tuple):
@@ -316,7 +316,7 @@ class ToolNode(RunnableCallable):
# (2) a NodeInterrupt is raised inside a graph node for a graph called as a tool
# (3) a GraphInterrupt is raised when a subgraph is interrupted inside a graph called as a tool
# (2 and 3 can happen in a "supervisor w/ tools" multi-agent architecture)
except GraphInterrupt as e:
except GraphBubbleUp as e:
raise e
except Exception as e:
if isinstance(self.handle_tool_errors, tuple):
+13 -4
View File
@@ -107,6 +107,7 @@ from langgraph.types import (
Checkpointer,
LoopProtocol,
StateSnapshot,
StreamChunk,
StreamMode,
)
from langgraph.utils.config import (
@@ -1752,6 +1753,10 @@ class Pregel(PregelProtocol):
stream = AsyncQueue()
aioloop = asyncio.get_running_loop()
stream_put = cast(
Callable[[StreamChunk], None],
partial(aioloop.call_soon_threadsafe, stream.put_nowait),
)
def output() -> Iterator:
while True:
@@ -1806,12 +1811,14 @@ class Pregel(PregelProtocol):
# set up messages stream mode
if "messages" in stream_modes:
run_manager.inheritable_handlers.append(
StreamMessagesHandler(stream.put_nowait)
StreamMessagesHandler(stream_put)
)
# set up custom stream mode
if "custom" in stream_modes:
config[CONF][CONFIG_KEY_STREAM_WRITER] = lambda c: stream.put_nowait(
((), "custom", c)
config[CONF][CONFIG_KEY_STREAM_WRITER] = (
lambda c: aioloop.call_soon_threadsafe(
stream.put_nowait, ((), "custom", c)
)
)
async with AsyncPregelLoop(
input,
@@ -1838,7 +1845,9 @@ class Pregel(PregelProtocol):
)
# enable subgraph streaming
if subgraphs:
loop.config[CONF][CONFIG_KEY_STREAM] = loop.stream
loop.config[CONF][CONFIG_KEY_STREAM] = StreamProtocol(
stream_put, stream_modes
)
# enable concurrent streaming
if subgraphs or "messages" in stream_modes or "custom" in stream_modes:
+2
View File
@@ -602,6 +602,7 @@ def prepare_single_task(
None,
task_id,
task_path,
writers=proc.flat_writers,
)
else:
@@ -720,6 +721,7 @@ def prepare_single_task(
None,
task_id,
task_path,
writers=proc.flat_writers,
)
else:
return PregelTask(task_id, name, task_path)
+8
View File
@@ -191,6 +191,14 @@ def map_debug_checkpoint(
"state": t.state,
}
if t.error
else {
"id": t.id,
"name": t.name,
"result": t.result,
"interrupts": tuple(asdict(i) for i in t.interrupts),
"state": t.state,
}
if t.result
else {
"id": t.id,
"name": t.name,
+3 -3
View File
@@ -20,7 +20,7 @@ from langchain_core.runnables import RunnableConfig
from langchain_core.runnables.config import get_executor_for_config
from typing_extensions import ParamSpec
from langgraph.errors import GraphInterrupt
from langgraph.errors import GraphBubbleUp
P = ParamSpec("P")
T = TypeVar("T")
@@ -68,7 +68,7 @@ class BackgroundExecutor(ContextManager):
def done(self, task: concurrent.futures.Future) -> None:
try:
task.result()
except GraphInterrupt:
except GraphBubbleUp:
# This exception is an interruption signal, not an error
# so we don't want to re-raise it on exit
self.tasks.pop(task)
@@ -155,7 +155,7 @@ class AsyncBackgroundExecutor(AsyncContextManager):
if exc := task.exception():
# This exception is an interruption signal, not an error
# so we don't want to re-raise it on exit
if isinstance(exc, GraphInterrupt):
if isinstance(exc, GraphBubbleUp):
self.tasks.pop(task)
else:
self.tasks.pop(task)
+3
View File
@@ -15,6 +15,7 @@ from langgraph.constants import (
TAG_HIDDEN,
TASKS,
)
from langgraph.errors import InvalidUpdateError
from langgraph.pregel.log import logger
from langgraph.types import Command, PregelExecutableTask, Send
@@ -68,6 +69,8 @@ def map_command(
cmd: Command,
) -> Iterator[tuple[str, str, Any]]:
"""Map input chunk to a sequence of pending writes in the form (channel, value)."""
if cmd.graph == Command.PARENT:
raise InvalidUpdateError("There is not parent graph")
if cmd.send:
if isinstance(cmd.send, (tuple, list)):
sends = cmd.send
+1 -1
View File
@@ -110,13 +110,13 @@ from langgraph.types import (
Command,
LoopProtocol,
PregelExecutableTask,
StreamChunk,
StreamProtocol,
)
from langgraph.utils.config import patch_configurable
V = TypeVar("V")
P = ParamSpec("P")
StreamChunk = tuple[tuple[str, ...], str, Any]
INPUT_DONE = object()
INPUT_RESUMING = object()
+1 -1
View File
@@ -18,7 +18,7 @@ from langchain_core.outputs import ChatGenerationChunk, LLMResult
from langchain_core.tracers._streaming import T, _StreamingCallbackHandler
from langgraph.constants import NS_SEP, TAG_HIDDEN, TAG_NOSTREAM
from langgraph.pregel.loop import StreamChunk
from langgraph.types import StreamChunk
Meta = tuple[tuple[str, ...], dict[str, Any]]
+34 -4
View File
@@ -2,6 +2,7 @@ import asyncio
import logging
import random
import time
from dataclasses import replace
from functools import partial
from typing import Any, Callable, Optional, Sequence
@@ -10,9 +11,10 @@ from langgraph.constants import (
CONFIG_KEY_CHECKPOINT_NS,
CONFIG_KEY_RESUMING,
CONFIG_KEY_SEND,
NS_SEP,
)
from langgraph.errors import _SEEN_CHECKPOINT_NS, GraphInterrupt
from langgraph.types import PregelExecutableTask, RetryPolicy
from langgraph.errors import _SEEN_CHECKPOINT_NS, GraphBubbleUp, ParentCommand
from langgraph.types import Command, PregelExecutableTask, RetryPolicy
from langgraph.utils.config import patch_configurable
logger = logging.getLogger(__name__)
@@ -40,7 +42,21 @@ def run_with_retry(
task.proc.invoke(task.input, config)
# if successful, end
break
except GraphInterrupt:
except ParentCommand as exc:
ns: str = config[CONF][CONFIG_KEY_CHECKPOINT_NS]
cmd = exc.args[0]
if cmd.graph == ns:
# this command is for the current graph, handle it
for w in task.writers:
w.invoke(cmd, config)
break
elif cmd.graph == Command.PARENT:
# this command is for the parent graph, assign it to the parent
parent_ns = NS_SEP.join(ns.split(NS_SEP)[:-1])
exc.args = (replace(cmd, graph=parent_ns),)
# bubble up
raise
except GraphBubbleUp:
# if interrupted, end
raise
except Exception as exc:
@@ -118,7 +134,21 @@ async def arun_with_retry(
await task.proc.ainvoke(task.input, config)
# if successful, end
break
except GraphInterrupt:
except ParentCommand as exc:
ns: str = config[CONF][CONFIG_KEY_CHECKPOINT_NS]
cmd = exc.args[0]
if cmd.graph == ns:
# this command is for the current graph, handle it
for w in task.writers:
w.invoke(cmd, config)
break
elif cmd.graph == Command.PARENT:
# this command is for the parent graph, assign it to the parent
parent_ns = NS_SEP.join(ns.split(NS_SEP)[:-1])
exc.args = (replace(cmd, graph=parent_ns),)
# bubble up
raise
except GraphBubbleUp:
# if interrupted, end
raise
except Exception as exc:
+3 -3
View File
@@ -23,7 +23,7 @@ from langgraph.constants import (
PUSH,
TAG_HIDDEN,
)
from langgraph.errors import GraphDelegate, GraphInterrupt
from langgraph.errors import GraphBubbleUp, GraphInterrupt
from langgraph.pregel.executor import Submit
from langgraph.pregel.retry import arun_with_retry, run_with_retry
from langgraph.types import PregelExecutableTask, RetryPolicy
@@ -298,7 +298,7 @@ class PregelRunner:
# save interrupt to checkpointer
if interrupts := [(INTERRUPT, i) for i in exception.args[0]]:
self.put_writes(task.id, interrupts)
elif isinstance(exception, GraphDelegate):
elif isinstance(exception, GraphBubbleUp):
raise exception
else:
# save error to checkpointer
@@ -324,7 +324,7 @@ def _should_stop_others(
if fut.cancelled():
return True
if exc := fut.exception():
return not isinstance(exc, GraphInterrupt)
return not isinstance(exc, GraphBubbleUp)
else:
return False
+1 -1
View File
@@ -48,7 +48,7 @@ def find_subgraph_pregel(candidate: Runnable) -> Optional[Runnable]:
nl.__self__ if hasattr(nl, "__self__") else nl
for nl in get_function_nonlocals(c.func)
)
if c.afunc is not None:
elif c.afunc is not None:
candidates.extend(
nl.__self__ if hasattr(nl, "__self__") else nl
for nl in get_function_nonlocals(c.afunc)
+5
View File
@@ -5,6 +5,7 @@ from typing import (
TYPE_CHECKING,
Any,
Callable,
ClassVar,
Generic,
Hashable,
Literal,
@@ -140,6 +141,7 @@ class PregelExecutableTask(NamedTuple):
id: str
path: tuple[Union[str, int, tuple], ...]
scheduled: bool = False
writers: Sequence[Runnable] = ()
class StateSnapshot(NamedTuple):
@@ -239,6 +241,7 @@ N = TypeVar("N", bound=Hashable)
class Command(Generic[N]):
"""One or more commands to update the graph's state and send messages to nodes."""
graph: Optional[str] = None
update: Optional[dict[str, Any]] = None
send: Union[Send, Sequence[Send]] = ()
resume: Optional[Union[Any, dict[str, Any]]] = None
@@ -252,6 +255,8 @@ class Command(Generic[N]):
)
return f"Command({contents})"
PARENT: ClassVar[Literal["__parent__"]] = "__parent__"
StreamChunk = tuple[tuple[str, ...], str, Any]
+5 -2
View File
@@ -1,7 +1,7 @@
import asyncio
import sys
from collections import ChainMap
from typing import Any, Optional, Sequence
from typing import Any, Optional, Sequence, cast
from langchain_core.callbacks import (
AsyncCallbackManager,
@@ -280,7 +280,10 @@ def ensure_config(*configs: Optional[RunnableConfig]) -> RunnableConfig:
continue
for k, v in config.items():
if v is not None and k in CONFIG_KEYS:
empty[k] = v # type: ignore[literal-required]
if k == CONF:
empty[k] = cast(dict, v).copy()
else:
empty[k] = v # type: ignore[literal-required]
for k, v in config.items():
if v is not None and k not in CONFIG_KEYS:
empty[CONF][k] = v
+13 -13
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "aiosqlite"
@@ -1253,13 +1253,13 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (>
[[package]]
name = "jupyterlab"
version = "4.3.0"
version = "4.2.5"
description = "JupyterLab computational environment"
optional = false
python-versions = ">=3.8"
files = [
{file = "jupyterlab-4.3.0-py3-none-any.whl", hash = "sha256:f67e1095ad61ae04349024f0b40345062ab108a0c6998d9810fec6a3c1a70cd5"},
{file = "jupyterlab-4.3.0.tar.gz", hash = "sha256:7c6835cbf8df0af0ec8a39332e85ff11693fb9a468205343b4fc0bfbc74817e5"},
{file = "jupyterlab-4.2.5-py3-none-any.whl", hash = "sha256:73b6e0775d41a9fee7ee756c80f58a6bed4040869ccc21411dc559818874d321"},
{file = "jupyterlab-4.2.5.tar.gz", hash = "sha256:ae7f3a1b8cb88b4f55009ce79fa7c06f99d70cd63601ee4aa91815d054f46f75"},
]
[package.dependencies]
@@ -1280,9 +1280,9 @@ tornado = ">=6.2.0"
traitlets = "*"
[package.extras]
dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.6.9)"]
docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<8.1.0)", "sphinx-copybutton"]
docs-screenshots = ["altair (==5.4.1)", "ipython (==8.16.1)", "ipywidgets (==8.1.5)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.2.post3)", "matplotlib (==3.9.2)", "nbconvert (>=7.0.0)", "pandas (==2.2.3)", "scipy (==1.14.1)", "vega-datasets (==0.9.0)"]
dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.3.5)"]
docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<7.3.0)", "sphinx-copybutton"]
docs-screenshots = ["altair (==5.3.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.2)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.1.post2)", "matplotlib (==3.8.3)", "nbconvert (>=7.0.0)", "pandas (==2.2.1)", "scipy (==1.12.0)", "vega-datasets (==0.9.0)"]
test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"]
upgrade-extension = ["copier (>=9,<10)", "jinja2-time (<0.3)", "pydantic (<3.0)", "pyyaml-include (<3.0)", "tomli-w (<2.0)"]
@@ -1792,26 +1792,26 @@ files = [
[[package]]
name = "notebook"
version = "7.0.7"
version = "7.2.2"
description = "Jupyter Notebook - A web-based notebook environment for interactive computing"
optional = false
python-versions = ">=3.8"
files = [
{file = "notebook-7.0.7-py3-none-any.whl", hash = "sha256:289b606d7e173f75a18beb1406ef411b43f97f7a9c55ba03efa3622905a62346"},
{file = "notebook-7.0.7.tar.gz", hash = "sha256:3bcff00c17b3ac142ef5f436d50637d936b274cfa0b41f6ac0175363de9b4e09"},
{file = "notebook-7.2.2-py3-none-any.whl", hash = "sha256:c89264081f671bc02eec0ed470a627ed791b9156cad9285226b31611d3e9fe1c"},
{file = "notebook-7.2.2.tar.gz", hash = "sha256:2ef07d4220421623ad3fe88118d687bc0450055570cdd160814a59cf3a1c516e"},
]
[package.dependencies]
jupyter-server = ">=2.4.0,<3"
jupyterlab = ">=4.0.2,<5"
jupyterlab-server = ">=2.22.1,<3"
jupyterlab = ">=4.2.0,<4.3"
jupyterlab-server = ">=2.27.1,<3"
notebook-shim = ">=0.2,<0.3"
tornado = ">=6.2.0"
[package.extras]
dev = ["hatch", "pre-commit"]
docs = ["myst-parser", "nbsphinx", "pydata-sphinx-theme", "sphinx (>=1.3.6)", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"]
test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.22.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"]
test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.27.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"]
[[package]]
name = "notebook-shim"
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph"
version = "0.2.50"
version = "0.2.53"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
license = "MIT"
+107 -2
View File
@@ -272,6 +272,54 @@ async def _store_postgres_aio():
await conn.execute(f"DROP DATABASE {database}")
@asynccontextmanager
async def _store_postgres_aio_pipe():
if sys.version_info < (3, 10):
pytest.skip("Async Postgres tests require Python 3.10+")
database = f"test_{uuid4().hex[:16]}"
async with await AsyncConnection.connect(
DEFAULT_POSTGRES_URI, autocommit=True
) as conn:
await conn.execute(f"CREATE DATABASE {database}")
try:
async with AsyncPostgresStore.from_conn_string(
DEFAULT_POSTGRES_URI + database
) as store:
await store.setup() # Run in its own transaction
async with AsyncPostgresStore.from_conn_string(
DEFAULT_POSTGRES_URI + database, pipeline=True
) as store:
yield store
finally:
async with await AsyncConnection.connect(
DEFAULT_POSTGRES_URI, autocommit=True
) as conn:
await conn.execute(f"DROP DATABASE {database}")
@asynccontextmanager
async def _store_postgres_aio_pool():
if sys.version_info < (3, 10):
pytest.skip("Async Postgres tests require Python 3.10+")
database = f"test_{uuid4().hex[:16]}"
async with await AsyncConnection.connect(
DEFAULT_POSTGRES_URI, autocommit=True
) as conn:
await conn.execute(f"CREATE DATABASE {database}")
try:
async with AsyncPostgresStore.from_conn_string(
DEFAULT_POSTGRES_URI + database,
pool_config={"max_size": 10},
) as store:
await store.setup()
yield store
finally:
async with await AsyncConnection.connect(
DEFAULT_POSTGRES_URI, autocommit=True
) as conn:
await conn.execute(f"DROP DATABASE {database}")
@asynccontextmanager
async def _store_duckdb_aio():
async with AsyncDuckDBStore.from_conn_string(":memory:") as store:
@@ -296,6 +344,45 @@ def store_postgres():
conn.execute(f"DROP DATABASE {database}")
@pytest.fixture(scope="function")
def store_postgres_pipe():
database = f"test_{uuid4().hex[:16]}"
# create unique db
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
conn.execute(f"CREATE DATABASE {database}")
try:
# yield store
with PostgresStore.from_conn_string(DEFAULT_POSTGRES_URI + database) as store:
store.setup() # Run in its own transaction
with PostgresStore.from_conn_string(
DEFAULT_POSTGRES_URI + database, pipeline=True
) as store:
yield store
finally:
# drop unique db
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
conn.execute(f"DROP DATABASE {database}")
@pytest.fixture(scope="function")
def store_postgres_pool():
database = f"test_{uuid4().hex[:16]}"
# create unique db
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
conn.execute(f"CREATE DATABASE {database}")
try:
# yield store
with PostgresStore.from_conn_string(
DEFAULT_POSTGRES_URI + database, pool_config={"max_size": 10}
) as store:
store.setup()
yield store
finally:
# drop unique db
with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn:
conn.execute(f"DROP DATABASE {database}")
@pytest.fixture(scope="function")
def store_duckdb():
with DuckDBStore.from_conn_string(":memory:") as store:
@@ -317,6 +404,12 @@ async def awith_store(store_name: Optional[str]) -> AsyncIterator[BaseStore]:
elif store_name == "postgres_aio":
async with _store_postgres_aio() as store:
yield store
elif store_name == "postgres_aio_pipe":
async with _store_postgres_aio_pipe() as store:
yield store
elif store_name == "postgres_aio_pool":
async with _store_postgres_aio_pool() as store:
yield store
elif store_name == "duckdb_aio":
async with _store_duckdb_aio() as store:
yield store
@@ -342,5 +435,17 @@ ALL_CHECKPOINTERS_ASYNC_PLUS_NONE = [
*ALL_CHECKPOINTERS_ASYNC,
None,
]
ALL_STORES_SYNC = ["in_memory", "postgres", "duckdb"]
ALL_STORES_ASYNC = ["in_memory", "postgres_aio", "duckdb_aio"]
ALL_STORES_SYNC = [
"in_memory",
"postgres",
"postgres_pipe",
"postgres_pool",
"duckdb",
]
ALL_STORES_ASYNC = [
"in_memory",
"postgres_aio",
"postgres_aio_pipe",
"postgres_aio_pool",
"duckdb_aio",
]
+53 -2
View File
@@ -1,7 +1,10 @@
import re
from typing import Any, Iterator, List, Optional, cast
from typing import Any, AsyncIterator, Iterator, List, Optional, cast
from langchain_core.callbacks import CallbackManagerForLLMRun
from langchain_core.callbacks import (
AsyncCallbackManagerForLLMRun,
CallbackManagerForLLMRun,
)
from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
from langchain_core.messages import AIMessage, AIMessageChunk, BaseMessage
from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult
@@ -84,3 +87,51 @@ class FakeChatModel(GenericFakeChatModel):
if run_manager:
run_manager.on_llm_new_token("", chunk=chunk)
yield chunk
async def _astream(
self,
messages: List[BaseMessage],
stop: Optional[List[str]] = None,
run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
**kwargs: Any,
) -> AsyncIterator[ChatGenerationChunk]:
"""Stream the output of the model."""
chat_result = self._generate(
messages, stop=stop, run_manager=run_manager, **kwargs
)
if not isinstance(chat_result, ChatResult):
raise ValueError(
f"Expected generate to return a ChatResult, "
f"but got {type(chat_result)} instead."
)
message = chat_result.generations[0].message
if not isinstance(message, AIMessage):
raise ValueError(
f"Expected invoke to return an AIMessage, "
f"but got {type(message)} instead."
)
content = message.content
if content:
# Use a regular expression to split on whitespace with a capture group
# so that we can preserve the whitespace in the output.
assert isinstance(content, str)
content_chunks = cast(list[str], re.split(r"(\s)", content))
for token in content_chunks:
chunk = ChatGenerationChunk(
message=AIMessageChunk(content=token, id=message.id)
)
if run_manager:
run_manager.on_llm_new_token(token, chunk=chunk)
yield chunk
else:
args = message.__dict__
args.pop("type")
chunk = ChatGenerationChunk(message=AIMessageChunk(**args))
if run_manager:
await run_manager.on_llm_new_token("", chunk=chunk)
yield chunk
+142 -49
View File
@@ -1,5 +1,6 @@
import enum
import json
import logging
import operator
import re
import time
@@ -67,16 +68,9 @@ from langgraph.errors import InvalidUpdateError, MultipleSubgraphsError, NodeInt
from langgraph.graph import END, Graph, GraphCommand, StateGraph
from langgraph.graph.message import MessageGraph, MessagesState, add_messages
from langgraph.managed.shared_value import SharedValue
from langgraph.prebuilt.chat_agent_executor import (
create_tool_calling_executor,
)
from langgraph.prebuilt.chat_agent_executor import create_tool_calling_executor
from langgraph.prebuilt.tool_node import ToolNode
from langgraph.pregel import (
Channel,
GraphRecursionError,
Pregel,
StateSnapshot,
)
from langgraph.pregel import Channel, GraphRecursionError, Pregel, StateSnapshot
from langgraph.pregel.retry import RetryPolicy
from langgraph.store.base import BaseStore
from langgraph.store.memory import InMemoryStore
@@ -104,6 +98,8 @@ from tests.messages import (
_AnyIdToolMessage,
)
logger = logging.getLogger(__name__)
# define these objects to avoid importing langchain_core.agents
# and therefore avoid relying on core Pydantic version
@@ -6628,11 +6624,7 @@ def test_message_graph(
from langchain_core.language_models.fake_chat_models import (
FakeMessagesListChatModel,
)
from langchain_core.messages import (
AIMessage,
BaseMessage,
HumanMessage,
)
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
from langchain_core.outputs import ChatGeneration, ChatResult
from langchain_core.tools import tool
@@ -13937,50 +13929,75 @@ def test_store_injected(
doc_id = str(uuid.uuid4())
doc = {"some-key": "this-is-a-val"}
def node(input: State, config: RunnableConfig, store: BaseStore):
assert isinstance(store, BaseStore)
store.put(
("foo", "bar"),
doc_id,
{
**doc,
"from_thread": config["configurable"]["thread_id"],
"some_val": input["count"],
},
)
return {"count": 1}
builder = StateGraph(State)
builder.add_node("node", node)
builder.add_edge("__start__", "node")
graph = builder.compile(store=the_store, checkpointer=checkpointer)
uid = uuid.uuid4().hex
namespace = (f"foo-{uid}", "bar")
thread_1 = str(uuid.uuid4())
result = graph.invoke({"count": 0}, {"configurable": {"thread_id": thread_1}})
assert result == {"count": 1}
returned_doc = the_store.get(("foo", "bar"), doc_id).value
assert returned_doc == {**doc, "from_thread": thread_1, "some_val": 0}
assert len(the_store.search(("foo", "bar"))) == 1
# Check update on existing thread
result = graph.invoke({"count": 0}, {"configurable": {"thread_id": thread_1}})
assert result == {"count": 2}
returned_doc = the_store.get(("foo", "bar"), doc_id).value
assert returned_doc == {**doc, "from_thread": thread_1, "some_val": 1}
assert len(the_store.search(("foo", "bar"))) == 1
thread_2 = str(uuid.uuid4())
class Node:
def __init__(self, i: Optional[int] = None):
self.i = i
def __call__(self, inputs: State, config: RunnableConfig, store: BaseStore):
assert isinstance(store, BaseStore)
store.put(
namespace
if self.i is not None
and config["configurable"]["thread_id"] in (thread_1, thread_2)
else (f"foo_{self.i}", "bar"),
doc_id,
{
**doc,
"from_thread": config["configurable"]["thread_id"],
"some_val": inputs["count"],
},
)
return {"count": 1}
builder = StateGraph(State)
builder.add_node("node", Node())
builder.add_edge("__start__", "node")
N = 500
M = 1
if "duckdb" in store_name:
logger.warning(
"DuckDB store implementation has a known issue that does not"
" support concurrent writes, so we're reducing the test scope"
)
N = M = 1
for i in range(N):
builder.add_node(f"node_{i}", Node(i))
builder.add_edge("__start__", f"node_{i}")
graph = builder.compile(store=the_store, checkpointer=checkpointer)
results = graph.batch(
[{"count": 0}] * M,
([{"configurable": {"thread_id": str(uuid.uuid4())}}] * (M - 1))
+ [{"configurable": {"thread_id": thread_1}}],
)
result = results[-1]
assert result == {"count": N + 1}
returned_doc = the_store.get(namespace, doc_id).value
assert returned_doc == {**doc, "from_thread": thread_1, "some_val": 0}
assert len(the_store.search(namespace)) == 1
# Check results after another turn of the same thread
result = graph.invoke({"count": 0}, {"configurable": {"thread_id": thread_1}})
assert result == {"count": (N + 1) * 2}
returned_doc = the_store.get(namespace, doc_id).value
assert returned_doc == {**doc, "from_thread": thread_1, "some_val": N + 1}
assert len(the_store.search(namespace)) == 1
result = graph.invoke({"count": 0}, {"configurable": {"thread_id": thread_2}})
assert result == {"count": 1}
returned_doc = the_store.get(("foo", "bar"), doc_id).value
assert result == {"count": N + 1}
returned_doc = the_store.get(namespace, doc_id).value
assert returned_doc == {
**doc,
"from_thread": thread_2,
"some_val": 0,
} # Overwrites the whole doc
assert len(the_store.search(("foo", "bar"))) == 1 # still overwriting the same one
assert len(the_store.search(namespace)) == 1 # still overwriting the same one
def test_enum_node_names():
@@ -14378,3 +14395,79 @@ def test_runnable_passthrough_node_graph() -> None:
graph = graph_builder.compile()
assert graph.get_graph(xray=True).to_json() == graph.get_graph(xray=False).to_json()
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
def test_parent_command(request: pytest.FixtureRequest, checkpointer_name: str) -> None:
from langchain_core.messages import BaseMessage
from langchain_core.tools import tool
@tool(return_direct=True)
def get_user_name() -> GraphCommand:
"""Retrieve user name"""
return GraphCommand(update={"user_name": "Meow"}, graph=GraphCommand.PARENT)
subgraph_builder = StateGraph(MessagesState)
subgraph_builder.add_node("tool", get_user_name)
subgraph_builder.add_edge(START, "tool")
subgraph = subgraph_builder.compile()
class CustomParentState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
# this key is not available to the child graph
user_name: str
builder = StateGraph(CustomParentState)
builder.add_node("alice", subgraph)
builder.add_edge(START, "alice")
checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}")
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "1"}}
assert graph.invoke({"messages": [("user", "get user name")]}, config) == {
"messages": [
_AnyIdHumanMessage(
content="get user name", additional_kwargs={}, response_metadata={}
),
],
"user_name": "Meow",
}
assert graph.get_state(config) == StateSnapshot(
values={
"messages": [
_AnyIdHumanMessage(
content="get user name", additional_kwargs={}, response_metadata={}
),
],
"user_name": "Meow",
},
next=(),
config={
"configurable": {
"thread_id": "1",
"checkpoint_ns": "",
"checkpoint_id": AnyStr(),
}
},
metadata={
"source": "loop",
"writes": {
"alice": {
"user_name": "Meow",
}
},
"thread_id": "1",
"step": 1,
"parents": {},
},
created_at=AnyStr(),
parent_config={
"configurable": {
"thread_id": "1",
"checkpoint_ns": "",
"checkpoint_id": AnyStr(),
}
},
tasks=(),
)
+141 -29
View File
@@ -1,4 +1,5 @@
import asyncio
import logging
import operator
import random
import re
@@ -100,6 +101,8 @@ from tests.messages import (
_AnyIdToolMessage,
)
logger = logging.getLogger(__name__)
pytestmark = pytest.mark.anyio
@@ -12272,60 +12275,89 @@ async def test_store_injected_async(checkpointer_name: str, store_name: str) ->
doc_id = str(uuid.uuid4())
doc = {"some-key": "this-is-a-val"}
uid = uuid.uuid4().hex
namespace = (f"foo-{uid}", "bar")
thread_1 = str(uuid.uuid4())
thread_2 = str(uuid.uuid4())
async def node(input: State, config: RunnableConfig, store: BaseStore):
assert isinstance(store, BaseStore)
await store.aput(
("foo", "bar"),
doc_id,
{
**doc,
"from_thread": config["configurable"]["thread_id"],
"some_val": input["count"],
},
)
return {"count": 1}
class Node:
def __init__(self, i: Optional[int] = None):
self.i = i
async def __call__(
self, inputs: State, config: RunnableConfig, store: BaseStore
):
assert isinstance(store, BaseStore)
await store.aput(
namespace
if self.i is not None
and config["configurable"]["thread_id"] in (thread_1, thread_2)
else (f"foo_{self.i}", "bar"),
doc_id,
{
**doc,
"from_thread": config["configurable"]["thread_id"],
"some_val": inputs["count"],
},
)
return {"count": 1}
builder = StateGraph(State)
builder.add_node("node", node)
builder.add_node("node", Node())
builder.add_edge("__start__", "node")
N = 500
M = 1
if "duckdb" in store_name:
logger.warning(
"DuckDB store implementation has a known issue that does not"
" support concurrent writes, so we're reducing the test scope"
)
N = M = 1
for i in range(N):
builder.add_node(f"node_{i}", Node(i))
builder.add_edge("__start__", f"node_{i}")
async with awith_checkpointer(checkpointer_name) as checkpointer, awith_store(
store_name
) as the_store:
graph = builder.compile(store=the_store, checkpointer=checkpointer)
thread_1 = str(uuid.uuid4())
result = await graph.ainvoke(
{"count": 0}, {"configurable": {"thread_id": thread_1}}
# Test batch operations with multiple threads
results = await graph.abatch(
[{"count": 0}] * M,
([{"configurable": {"thread_id": str(uuid.uuid4())}}] * (M - 1))
+ [{"configurable": {"thread_id": thread_1}}],
)
assert result == {"count": 1}
returned_doc = (await the_store.aget(("foo", "bar"), doc_id)).value
result = results[-1]
assert result == {"count": N + 1}
returned_doc = (await the_store.aget(namespace, doc_id)).value
assert returned_doc == {**doc, "from_thread": thread_1, "some_val": 0}
assert len((await the_store.asearch(("foo", "bar")))) == 1
assert len((await the_store.asearch(namespace))) == 1
# Check update on existing thread
# Check results after another turn of the same thread
result = await graph.ainvoke(
{"count": 0}, {"configurable": {"thread_id": thread_1}}
)
assert result == {"count": 2}
returned_doc = (await the_store.aget(("foo", "bar"), doc_id)).value
assert returned_doc == {**doc, "from_thread": thread_1, "some_val": 1}
assert len((await the_store.asearch(("foo", "bar")))) == 1
thread_2 = str(uuid.uuid4())
assert result == {"count": (N + 1) * 2}
returned_doc = (await the_store.aget(namespace, doc_id)).value
assert returned_doc == {**doc, "from_thread": thread_1, "some_val": N + 1}
assert len((await the_store.asearch(namespace))) == 1
# Test with a different thread
result = await graph.ainvoke(
{"count": 0}, {"configurable": {"thread_id": thread_2}}
)
assert result == {"count": 1}
returned_doc = (await the_store.aget(("foo", "bar"), doc_id)).value
assert result == {"count": N + 1}
returned_doc = (await the_store.aget(namespace, doc_id)).value
assert returned_doc == {
**doc,
"from_thread": thread_2,
"some_val": 0,
} # Overwrites the whole doc
assert (
len((await the_store.asearch(("foo", "bar")))) == 1
len((await the_store.asearch(namespace))) == 1
) # still overwriting the same one
@@ -12565,3 +12597,83 @@ async def test_debug_nested_subgraphs():
assert stream_task["interrupts"] == history_task.interrupts
assert stream_task.get("error") == history_task.error
assert stream_task.get("state") == history_task.state
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC)
async def test_parent_command(checkpointer_name: str) -> None:
from langchain_core.messages import BaseMessage
from langchain_core.tools import tool
@tool(return_direct=True)
def get_user_name() -> GraphCommand:
"""Retrieve user name"""
return GraphCommand(update={"user_name": "Meow"}, graph=GraphCommand.PARENT)
subgraph_builder = StateGraph(MessagesState)
subgraph_builder.add_node("tool", get_user_name)
subgraph_builder.add_edge(START, "tool")
subgraph = subgraph_builder.compile()
class CustomParentState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
# this key is not available to the child graph
user_name: str
builder = StateGraph(CustomParentState)
builder.add_node("alice", subgraph)
builder.add_edge(START, "alice")
async with awith_checkpointer(checkpointer_name) as checkpointer:
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "1"}}
assert await graph.ainvoke(
{"messages": [("user", "get user name")]}, config
) == {
"messages": [
_AnyIdHumanMessage(
content="get user name", additional_kwargs={}, response_metadata={}
),
],
"user_name": "Meow",
}
assert await graph.aget_state(config) == StateSnapshot(
values={
"messages": [
_AnyIdHumanMessage(
content="get user name",
additional_kwargs={},
response_metadata={},
),
],
"user_name": "Meow",
},
next=(),
config={
"configurable": {
"thread_id": "1",
"checkpoint_ns": "",
"checkpoint_id": AnyStr(),
}
},
metadata={
"source": "loop",
"writes": {
"alice": {
"user_name": "Meow",
}
},
"thread_id": "1",
"step": 1,
"parents": {},
},
created_at=AnyStr(),
parent_config={
"configurable": {
"thread_id": "1",
"checkpoint_ns": "",
"checkpoint_id": AnyStr(),
}
},
tasks=(),
)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@langchain/langgraph-sdk",
"version": "0.0.25",
"version": "0.0.26",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
+34 -3
View File
@@ -34,6 +34,35 @@ import {
} from "./types.js";
import { mergeSignals } from "./utils/signals.js";
/**
* Get the API key from the environment.
* Precedence:
* 1. explicit argument
* 2. LANGGRAPH_API_KEY
* 3. LANGSMITH_API_KEY
* 4. LANGCHAIN_API_KEY
*
* @param apiKey - Optional API key provided as an argument
* @returns The API key if found, otherwise undefined
*/
export function getApiKey(apiKey?: string): string | undefined {
if (apiKey) {
return apiKey;
}
const prefixes = ["LANGGRAPH", "LANGSMITH", "LANGCHAIN"];
for (const prefix of prefixes) {
const envKey = process.env[`${prefix}_API_KEY`];
if (envKey) {
// Remove surrounding quotes
return envKey.trim().replace(/^["']|["']$/g, "");
}
}
return undefined;
}
interface ClientConfig {
apiUrl?: string;
apiKey?: string;
@@ -62,10 +91,12 @@ class BaseClient {
// default limit being capped by Chrome
// https://github.com/nodejs/undici/issues/1373
this.apiUrl = config?.apiUrl || "http://localhost:8123";
// Regex to remove trailing slash, if present
this.apiUrl = config?.apiUrl?.replace(/\/$/, "") || "http://localhost:8123";
this.defaultHeaders = config?.defaultHeaders || {};
if (config?.apiKey != null) {
this.defaultHeaders["X-Api-Key"] = config.apiKey;
const apiKey = getApiKey(config?.apiKey);
if (apiKey) {
this.defaultHeaders["X-Api-Key"] = apiKey;
}
}
+1 -1
View File
@@ -17,4 +17,4 @@ export type {
Checkpoint,
} from "./schema.js";
export type { OnConflictBehavior } from "./types.js";
export type { OnConflictBehavior, Command } from "./types.js";
+9
View File
@@ -29,10 +29,19 @@ export interface Send {
}
export interface Command {
/**
* An object to update the thread state with.
*/
update?: Record<string, unknown>;
/**
* The value to return from an `interrupt` function call.
*/
resume?: unknown;
/**
* A single, or array of `Send` commands to trigger nodes.
*/
send?: Send | Send[];
}
Generated
+256 -111
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
[[package]]
name = "aiohappyeyeballs"
@@ -13,108 +13,108 @@ files = [
[[package]]
name = "aiohttp"
version = "3.10.6"
version = "3.10.11"
description = "Async http client/server framework (asyncio)"
optional = false
python-versions = ">=3.8"
files = [
{file = "aiohttp-3.10.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:682836fc672972cc3101cc9e30d49c5f7e8f1d010478d46119fe725a4545acfd"},
{file = "aiohttp-3.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:289fa8a20018d0d5aa9e4b35d899bd51bcb80f0d5f365d9a23e30dac3b79159b"},
{file = "aiohttp-3.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8617c96a20dd57e7e9d398ff9d04f3d11c4d28b1767273a5b1a018ada5a654d3"},
{file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdbeff1b062751c2a2a55b171f7050fb7073633c699299d042e962aacdbe1a07"},
{file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ea35d849cdd4a9268f910bff4497baebbc1aa3f2f625fd8ccd9ac99c860c621"},
{file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473961b3252f3b949bb84873d6e268fb6d8aa0ccc6eb7404fa58c76a326bb8e1"},
{file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d2665c5df629eb2f981dab244c01bfa6cdc185f4ffa026639286c4d56fafb54"},
{file = "aiohttp-3.10.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25d92f794f1332f656e3765841fc2b7ad5c26c3f3d01e8949eeb3495691cf9f4"},
{file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9bd6b2033993d5ae80883bb29b83fb2b432270bbe067c2f53cc73bb57c46065f"},
{file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d7f408c43f5e75ea1edc152fb375e8f46ef916f545fb66d4aebcbcfad05e2796"},
{file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:cf8b8560aa965f87bf9c13bf9fed7025993a155ca0ce8422da74bf46d18c2f5f"},
{file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14477c4e52e2f17437b99893fd220ffe7d7ee41df5ebf931a92b8ca82e6fd094"},
{file = "aiohttp-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb138fbf9f53928e779650f5ed26d0ea1ed8b2cab67f0ea5d63afa09fdc07593"},
{file = "aiohttp-3.10.6-cp310-cp310-win32.whl", hash = "sha256:9843d683b8756971797be171ead21511d2215a2d6e3c899c6e3107fbbe826791"},
{file = "aiohttp-3.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:f8b8e49fe02f744d38352daca1dbef462c3874900bd8166516f6ea8e82b5aacf"},
{file = "aiohttp-3.10.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52e54fd776ad0da1006708762213b079b154644db54bcfc62f06eaa5b896402"},
{file = "aiohttp-3.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:995ab1a238fd0d19dc65f2d222e5eb064e409665c6426a3e51d5101c1979ee84"},
{file = "aiohttp-3.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0749c4d5a08a802dd66ecdf59b2df4d76b900004017468a7bb736c3b5a3dd902"},
{file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e05b39158f2af0e2438cc2075cfc271f4ace0c3cc4a81ec95b27a0432e161951"},
{file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a9f196c970db2dcde4f24317e06615363349dc357cf4d7a3b0716c20ac6d7bcd"},
{file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:47647c8af04a70e07a2462931b0eba63146a13affa697afb4ecbab9d03a480ce"},
{file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c0efe7e99f6d94d63274c06344bd0e9c8daf184ce5602a29bc39e00a18720"},
{file = "aiohttp-3.10.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9721cdd83a994225352ca84cd537760d41a9da3c0eacb3ff534747ab8fba6d0"},
{file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0b82c8ebed66ce182893e7c0b6b60ba2ace45b1df104feb52380edae266a4850"},
{file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b169f8e755e541b72e714b89a831b315bbe70db44e33fead28516c9e13d5f931"},
{file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0be3115753baf8b4153e64f9aa7bf6c0c64af57979aa900c31f496301b374570"},
{file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e1f80cd17d81a404b6e70ef22bfe1870bafc511728397634ad5f5efc8698df56"},
{file = "aiohttp-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6419728b08fb6380c66a470d2319cafcec554c81780e2114b7e150329b9a9a7f"},
{file = "aiohttp-3.10.6-cp311-cp311-win32.whl", hash = "sha256:bd294dcdc1afdc510bb51d35444003f14e327572877d016d576ac3b9a5888a27"},
{file = "aiohttp-3.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:bf861da9a43d282d6dd9dcd64c23a0fccf2c5aa5cd7c32024513c8c79fb69de3"},
{file = "aiohttp-3.10.6-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:2708baccdc62f4b1251e59c2aac725936a900081f079b88843dabcab0feeeb27"},
{file = "aiohttp-3.10.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7475da7a5e2ccf1a1c86c8fee241e277f4874c96564d06f726d8df8e77683ef7"},
{file = "aiohttp-3.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:02108326574ff60267b7b35b17ac5c0bbd0008ccb942ce4c48b657bb90f0b8aa"},
{file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:029a019627b37fa9eac5c75cc54a6bb722c4ebbf5a54d8c8c0fb4dd8facf2702"},
{file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a637d387db6fdad95e293fab5433b775fd104ae6348d2388beaaa60d08b38c4"},
{file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc1a16f3fc1944c61290d33c88dc3f09ba62d159b284c38c5331868425aca426"},
{file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81b292f37969f9cc54f4643f0be7dacabf3612b3b4a65413661cf6c350226787"},
{file = "aiohttp-3.10.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0754690a3a26e819173a34093798c155bafb21c3c640bff13be1afa1e9d421f9"},
{file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:164ecd32e65467d86843dbb121a6666c3deb23b460e3f8aefdcaacae79eb718a"},
{file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:438c5863feb761f7ca3270d48c292c334814459f61cc12bab5ba5b702d7c9e56"},
{file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ba18573bb1de1063d222f41de64a0d3741223982dcea863b3f74646faf618ec7"},
{file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c82a94ddec996413a905f622f3da02c4359952aab8d817c01cf9915419525e95"},
{file = "aiohttp-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92351aa5363fc3c1f872ca763f86730ced32b01607f0c9662b1fa711087968d0"},
{file = "aiohttp-3.10.6-cp312-cp312-win32.whl", hash = "sha256:3e15e33bfc73fa97c228f72e05e8795e163a693fd5323549f49367c76a6e5883"},
{file = "aiohttp-3.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:fe517113fe4d35d9072b826c3e147d63c5f808ca8167d450b4f96c520c8a1d8d"},
{file = "aiohttp-3.10.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:482f74057ea13d387a7549d7a7ecb60e45146d15f3e58a2d93a0ad2d5a8457cd"},
{file = "aiohttp-3.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:03fa40d1450ee5196e843315ddf74a51afc7e83d489dbfc380eecefea74158b1"},
{file = "aiohttp-3.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e52e59ed5f4cc3a3acfe2a610f8891f216f486de54d95d6600a2c9ba1581f4d"},
{file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b3935a22c9e41a8000d90588bed96cf395ef572dbb409be44c6219c61d900d"},
{file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bef1480ee50f75abcfcb4b11c12de1005968ca9d0172aec4a5057ba9f2b644f"},
{file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:671745ea7db19693ce867359d503772177f0b20fa8f6ee1e74e00449f4c4151d"},
{file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b50b367308ca8c12e0b50cba5773bc9abe64c428d3fd2bbf5cd25aab37c77bf"},
{file = "aiohttp-3.10.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a504d7cdb431a777d05a124fd0b21efb94498efa743103ea01b1e3136d2e4fb"},
{file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66bc81361131763660b969132a22edce2c4d184978ba39614e8f8f95db5c95f8"},
{file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:27cf19a38506e2e9f12fc17e55f118f04897b0a78537055d93a9de4bf3022e3d"},
{file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3468b39f977a11271517c6925b226720e148311039a380cc9117b1e2258a721f"},
{file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9d26da22a793dfd424be1050712a70c0afd96345245c29aced1e35dbace03413"},
{file = "aiohttp-3.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:844d48ff9173d0b941abed8b2ea6a412f82b56d9ab1edb918c74000c15839362"},
{file = "aiohttp-3.10.6-cp313-cp313-win32.whl", hash = "sha256:2dd56e3c43660ed3bea67fd4c5025f1ac1f9ecf6f0b991a6e5efe2e678c490c5"},
{file = "aiohttp-3.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:c91781d969fbced1993537f45efe1213bd6fccb4b37bfae2a026e20d6fbed206"},
{file = "aiohttp-3.10.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4407a80bca3e694f2d2a523058e20e1f9f98a416619e04f6dc09dc910352ac8b"},
{file = "aiohttp-3.10.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1cb045ec5961f51af3e2c08cd6fe523f07cc6e345033adee711c49b7b91bb954"},
{file = "aiohttp-3.10.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4fabdcdc781a36b8fd7b2ca9dea8172f29a99e11d00ca0f83ffeb50958da84a1"},
{file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a9f42efcc2681790595ab3d03c0e52d01edc23a0973ea09f0dc8d295e12b8e"},
{file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cca776a440795db437d82c07455761c85bbcf3956221c3c23b8c93176c278ce7"},
{file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5582de171f0898139cf51dd9fcdc79b848e28d9abd68e837f0803fc9f30807b1"},
{file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:370e2d47575c53c817ee42a18acc34aad8da4dbdaac0a6c836d58878955f1477"},
{file = "aiohttp-3.10.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:444d1704e2af6b30766debed9be8a795958029e552fe77551355badb1944012c"},
{file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:40271a2a375812967401c9ca8077de9368e09a43a964f4dce0ff603301ec9358"},
{file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f3af26f86863fad12e25395805bb0babbd49d512806af91ec9708a272b696248"},
{file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4752df44df48fd42b80f51d6a97553b482cda1274d9dc5df214a3a1aa5d8f018"},
{file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:2cd5290ab66cfca2f90045db2cc6434c1f4f9fbf97c9f1c316e785033782e7d2"},
{file = "aiohttp-3.10.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3427031064b0d5c95647e6369c4aa3c556402f324a3e18107cb09517abe5f962"},
{file = "aiohttp-3.10.6-cp38-cp38-win32.whl", hash = "sha256:614fc21e86adc28e4165a6391f851a6da6e9cbd7bb232d0df7718b453a89ee98"},
{file = "aiohttp-3.10.6-cp38-cp38-win_amd64.whl", hash = "sha256:58c5d7318a136a3874c78717dd6de57519bc64f6363c5827c2b1cb775bea71dd"},
{file = "aiohttp-3.10.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5db26bbca8e7968c4c977a0c640e0b9ce7224e1f4dcafa57870dc6ee28e27de6"},
{file = "aiohttp-3.10.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3fb4216e3ec0dbc01db5ba802f02ed78ad8f07121be54eb9e918448cc3f61b7c"},
{file = "aiohttp-3.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a976ef488f26e224079deb3d424f29144c6d5ba4ded313198169a8af8f47fb82"},
{file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a86610174de8a85a920e956e2d4f9945e7da89f29a00e95ac62a4a414c4ef4e"},
{file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:217791c6a399cc4f2e6577bb44344cba1f5714a2aebf6a0bea04cfa956658284"},
{file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba3662d41abe2eab0eeec7ee56f33ef4e0b34858f38abf24377687f9e1fb00a5"},
{file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4dfa5ad4bce9ca30a76117fbaa1c1decf41ebb6c18a4e098df44298941566f9"},
{file = "aiohttp-3.10.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0009258e97502936d3bd5bf2ced15769629097d0abb81e6495fba1047824fe0"},
{file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0a75d5c9fb4f06c41d029ae70ad943c3a844c40c0a769d12be4b99b04f473d3d"},
{file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8198b7c002aae2b40b2d16bfe724b9a90bcbc9b78b2566fc96131ef4e382574d"},
{file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4611db8c907f90fe86be112efdc2398cd7b4c8eeded5a4f0314b70fdea8feab0"},
{file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ff99ae06eef85c7a565854826114ced72765832ee16c7e3e766c5e4c5b98d20e"},
{file = "aiohttp-3.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7641920bdcc7cd2d3ddfb8bb9133a6c9536b09dbd49490b79e125180b2d25b93"},
{file = "aiohttp-3.10.6-cp39-cp39-win32.whl", hash = "sha256:e2e7d5591ea868d5ec82b90bbeb366a198715672841d46281b623e23079593db"},
{file = "aiohttp-3.10.6-cp39-cp39-win_amd64.whl", hash = "sha256:b504c08c45623bf5c7ca41be380156d925f00199b3970efd758aef4a77645feb"},
{file = "aiohttp-3.10.6.tar.gz", hash = "sha256:d2578ef941be0c2ba58f6f421a703527d08427237ed45ecb091fed6f83305336"},
{file = "aiohttp-3.10.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5077b1a5f40ffa3ba1f40d537d3bec4383988ee51fbba6b74aa8fb1bc466599e"},
{file = "aiohttp-3.10.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d6a14a4d93b5b3c2891fca94fa9d41b2322a68194422bef0dd5ec1e57d7d298"},
{file = "aiohttp-3.10.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffbfde2443696345e23a3c597049b1dd43049bb65337837574205e7368472177"},
{file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20b3d9e416774d41813bc02fdc0663379c01817b0874b932b81c7f777f67b217"},
{file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b943011b45ee6bf74b22245c6faab736363678e910504dd7531a58c76c9015a"},
{file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48bc1d924490f0d0b3658fe5c4b081a4d56ebb58af80a6729d4bd13ea569797a"},
{file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e12eb3f4b1f72aaaf6acd27d045753b18101524f72ae071ae1c91c1cd44ef115"},
{file = "aiohttp-3.10.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f14ebc419a568c2eff3c1ed35f634435c24ead2fe19c07426af41e7adb68713a"},
{file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:72b191cdf35a518bfc7ca87d770d30941decc5aaf897ec8b484eb5cc8c7706f3"},
{file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5ab2328a61fdc86424ee540d0aeb8b73bbcad7351fb7cf7a6546fc0bcffa0038"},
{file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa93063d4af05c49276cf14e419550a3f45258b6b9d1f16403e777f1addf4519"},
{file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:30283f9d0ce420363c24c5c2421e71a738a2155f10adbb1a11a4d4d6d2715cfc"},
{file = "aiohttp-3.10.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e5358addc8044ee49143c546d2182c15b4ac3a60be01c3209374ace05af5733d"},
{file = "aiohttp-3.10.11-cp310-cp310-win32.whl", hash = "sha256:e1ffa713d3ea7cdcd4aea9cddccab41edf6882fa9552940344c44e59652e1120"},
{file = "aiohttp-3.10.11-cp310-cp310-win_amd64.whl", hash = "sha256:778cbd01f18ff78b5dd23c77eb82987ee4ba23408cbed233009fd570dda7e674"},
{file = "aiohttp-3.10.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:80ff08556c7f59a7972b1e8919f62e9c069c33566a6d28586771711e0eea4f07"},
{file = "aiohttp-3.10.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c8f96e9ee19f04c4914e4e7a42a60861066d3e1abf05c726f38d9d0a466e695"},
{file = "aiohttp-3.10.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fb8601394d537da9221947b5d6e62b064c9a43e88a1ecd7414d21a1a6fba9c24"},
{file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ea224cf7bc2d8856d6971cea73b1d50c9c51d36971faf1abc169a0d5f85a382"},
{file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db9503f79e12d5d80b3efd4d01312853565c05367493379df76d2674af881caa"},
{file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0f449a50cc33f0384f633894d8d3cd020e3ccef81879c6e6245c3c375c448625"},
{file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82052be3e6d9e0c123499127782a01a2b224b8af8c62ab46b3f6197035ad94e9"},
{file = "aiohttp-3.10.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20063c7acf1eec550c8eb098deb5ed9e1bb0521613b03bb93644b810986027ac"},
{file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:489cced07a4c11488f47aab1f00d0c572506883f877af100a38f1fedaa884c3a"},
{file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ea9b3bab329aeaa603ed3bf605f1e2a6f36496ad7e0e1aa42025f368ee2dc07b"},
{file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ca117819d8ad113413016cb29774b3f6d99ad23c220069789fc050267b786c16"},
{file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2dfb612dcbe70fb7cdcf3499e8d483079b89749c857a8f6e80263b021745c730"},
{file = "aiohttp-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9b615d3da0d60e7d53c62e22b4fd1c70f4ae5993a44687b011ea3a2e49051b8"},
{file = "aiohttp-3.10.11-cp311-cp311-win32.whl", hash = "sha256:29103f9099b6068bbdf44d6a3d090e0a0b2be6d3c9f16a070dd9d0d910ec08f9"},
{file = "aiohttp-3.10.11-cp311-cp311-win_amd64.whl", hash = "sha256:236b28ceb79532da85d59aa9b9bf873b364e27a0acb2ceaba475dc61cffb6f3f"},
{file = "aiohttp-3.10.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7480519f70e32bfb101d71fb9a1f330fbd291655a4c1c922232a48c458c52710"},
{file = "aiohttp-3.10.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f65267266c9aeb2287a6622ee2bb39490292552f9fbf851baabc04c9f84e048d"},
{file = "aiohttp-3.10.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7400a93d629a0608dc1d6c55f1e3d6e07f7375745aaa8bd7f085571e4d1cee97"},
{file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f34b97e4b11b8d4eb2c3a4f975be626cc8af99ff479da7de49ac2c6d02d35725"},
{file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e7b825da878464a252ccff2958838f9caa82f32a8dbc334eb9b34a026e2c636"},
{file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9f92a344c50b9667827da308473005f34767b6a2a60d9acff56ae94f895f385"},
{file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f1ab987a27b83c5268a17218463c2ec08dbb754195113867a27b166cd6087"},
{file = "aiohttp-3.10.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1dc0f4ca54842173d03322793ebcf2c8cc2d34ae91cc762478e295d8e361e03f"},
{file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7ce6a51469bfaacff146e59e7fb61c9c23006495d11cc24c514a455032bcfa03"},
{file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:aad3cd91d484d065ede16f3cf15408254e2469e3f613b241a1db552c5eb7ab7d"},
{file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f4df4b8ca97f658c880fb4b90b1d1ec528315d4030af1ec763247ebfd33d8b9a"},
{file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2e4e18a0a2d03531edbc06c366954e40a3f8d2a88d2b936bbe78a0c75a3aab3e"},
{file = "aiohttp-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ce66780fa1a20e45bc753cda2a149daa6dbf1561fc1289fa0c308391c7bc0a4"},
{file = "aiohttp-3.10.11-cp312-cp312-win32.whl", hash = "sha256:a919c8957695ea4c0e7a3e8d16494e3477b86f33067478f43106921c2fef15bb"},
{file = "aiohttp-3.10.11-cp312-cp312-win_amd64.whl", hash = "sha256:b5e29706e6389a2283a91611c91bf24f218962717c8f3b4e528ef529d112ee27"},
{file = "aiohttp-3.10.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:703938e22434d7d14ec22f9f310559331f455018389222eed132808cd8f44127"},
{file = "aiohttp-3.10.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9bc50b63648840854e00084c2b43035a62e033cb9b06d8c22b409d56eb098413"},
{file = "aiohttp-3.10.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f0463bf8b0754bc744e1feb61590706823795041e63edf30118a6f0bf577461"},
{file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6c6dec398ac5a87cb3a407b068e1106b20ef001c344e34154616183fe684288"},
{file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcaf2d79104d53d4dcf934f7ce76d3d155302d07dae24dff6c9fffd217568067"},
{file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25fd5470922091b5a9aeeb7e75be609e16b4fba81cdeaf12981393fb240dd10e"},
{file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbde2ca67230923a42161b1f408c3992ae6e0be782dca0c44cb3206bf330dee1"},
{file = "aiohttp-3.10.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:249c8ff8d26a8b41a0f12f9df804e7c685ca35a207e2410adbd3e924217b9006"},
{file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:878ca6a931ee8c486a8f7b432b65431d095c522cbeb34892bee5be97b3481d0f"},
{file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8663f7777ce775f0413324be0d96d9730959b2ca73d9b7e2c2c90539139cbdd6"},
{file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6cd3f10b01f0c31481fba8d302b61603a2acb37b9d30e1d14e0f5a58b7b18a31"},
{file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4e8d8aad9402d3aa02fdc5ca2fe68bcb9fdfe1f77b40b10410a94c7f408b664d"},
{file = "aiohttp-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38e3c4f80196b4f6c3a85d134a534a56f52da9cb8d8e7af1b79a32eefee73a00"},
{file = "aiohttp-3.10.11-cp313-cp313-win32.whl", hash = "sha256:fc31820cfc3b2863c6e95e14fcf815dc7afe52480b4dc03393c4873bb5599f71"},
{file = "aiohttp-3.10.11-cp313-cp313-win_amd64.whl", hash = "sha256:4996ff1345704ffdd6d75fb06ed175938c133425af616142e7187f28dc75f14e"},
{file = "aiohttp-3.10.11-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:74baf1a7d948b3d640badeac333af581a367ab916b37e44cf90a0334157cdfd2"},
{file = "aiohttp-3.10.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:473aebc3b871646e1940c05268d451f2543a1d209f47035b594b9d4e91ce8339"},
{file = "aiohttp-3.10.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c2f746a6968c54ab2186574e15c3f14f3e7f67aef12b761e043b33b89c5b5f95"},
{file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d110cabad8360ffa0dec8f6ec60e43286e9d251e77db4763a87dcfe55b4adb92"},
{file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0099c7d5d7afff4202a0c670e5b723f7718810000b4abcbc96b064129e64bc7"},
{file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0316e624b754dbbf8c872b62fe6dcb395ef20c70e59890dfa0de9eafccd2849d"},
{file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a5f7ab8baf13314e6b2485965cbacb94afff1e93466ac4d06a47a81c50f9cca"},
{file = "aiohttp-3.10.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c891011e76041e6508cbfc469dd1a8ea09bc24e87e4c204e05f150c4c455a5fa"},
{file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9208299251370ee815473270c52cd3f7069ee9ed348d941d574d1457d2c73e8b"},
{file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:459f0f32c8356e8125f45eeff0ecf2b1cb6db1551304972702f34cd9e6c44658"},
{file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:14cdc8c1810bbd4b4b9f142eeee23cda528ae4e57ea0923551a9af4820980e39"},
{file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:971aa438a29701d4b34e4943e91b5e984c3ae6ccbf80dd9efaffb01bd0b243a9"},
{file = "aiohttp-3.10.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9a309c5de392dfe0f32ee57fa43ed8fc6ddf9985425e84bd51ed66bb16bce3a7"},
{file = "aiohttp-3.10.11-cp38-cp38-win32.whl", hash = "sha256:9ec1628180241d906a0840b38f162a3215114b14541f1a8711c368a8739a9be4"},
{file = "aiohttp-3.10.11-cp38-cp38-win_amd64.whl", hash = "sha256:9c6e0ffd52c929f985c7258f83185d17c76d4275ad22e90aa29f38e211aacbec"},
{file = "aiohttp-3.10.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cdc493a2e5d8dc79b2df5bec9558425bcd39aff59fc949810cbd0832e294b106"},
{file = "aiohttp-3.10.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3e70f24e7d0405be2348da9d5a7836936bf3a9b4fd210f8c37e8d48bc32eca6"},
{file = "aiohttp-3.10.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968b8fb2a5eee2770eda9c7b5581587ef9b96fbdf8dcabc6b446d35ccc69df01"},
{file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deef4362af9493d1382ef86732ee2e4cbc0d7c005947bd54ad1a9a16dd59298e"},
{file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:686b03196976e327412a1b094f4120778c7c4b9cff9bce8d2fdfeca386b89829"},
{file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bf6d027d9d1d34e1c2e1645f18a6498c98d634f8e373395221121f1c258ace8"},
{file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:099fd126bf960f96d34a760e747a629c27fb3634da5d05c7ef4d35ef4ea519fc"},
{file = "aiohttp-3.10.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c73c4d3dae0b4644bc21e3de546530531d6cdc88659cdeb6579cd627d3c206aa"},
{file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0c5580f3c51eea91559db3facd45d72e7ec970b04528b4709b1f9c2555bd6d0b"},
{file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fdf6429f0caabfd8a30c4e2eaecb547b3c340e4730ebfe25139779b9815ba138"},
{file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d97187de3c276263db3564bb9d9fad9e15b51ea10a371ffa5947a5ba93ad6777"},
{file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0acafb350cfb2eba70eb5d271f55e08bd4502ec35e964e18ad3e7d34d71f7261"},
{file = "aiohttp-3.10.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c13ed0c779911c7998a58e7848954bd4d63df3e3575f591e321b19a2aec8df9f"},
{file = "aiohttp-3.10.11-cp39-cp39-win32.whl", hash = "sha256:22b7c540c55909140f63ab4f54ec2c20d2635c0289cdd8006da46f3327f971b9"},
{file = "aiohttp-3.10.11-cp39-cp39-win_amd64.whl", hash = "sha256:7b26b1551e481012575dab8e3727b16fe7dd27eb2711d2e63ced7368756268fb"},
{file = "aiohttp-3.10.11.tar.gz", hash = "sha256:9dc2b8f3dcab2e39e0fa309c8da50c3b55e6f34ab25f1a71d3288f24924d33a7"},
]
[package.dependencies]
aiohappyeyeballs = ">=2.3.0"
aiosignal = ">=1.1.2"
async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""}
async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""}
attrs = ">=17.3.0"
frozenlist = ">=1.1.1"
multidict = ">=4.5,<7.0"
@@ -390,6 +390,59 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi
tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
[[package]]
name = "autogen"
version = "0.3.2"
description = "A programming framework for agentic AI"
optional = false
python-versions = "<3.13,>=3.8"
files = [
{file = "autogen-0.3.2-py3-none-any.whl", hash = "sha256:e37a9df0ad84cde3429ec63298b8e9eb4e6306a28eec2627171e14b9a61ea64d"},
{file = "autogen-0.3.2.tar.gz", hash = "sha256:9f8a1170ac2e5a1fc9efc3cfa6e23261dd014db97b17c8c416f97ee14951bc7b"},
]
[package.dependencies]
diskcache = "*"
docker = "*"
flaml = "*"
numpy = ">=1.17.0,<2"
openai = ">=1.3"
packaging = "*"
pydantic = ">=1.10,<2.6.0 || >2.6.0,<3"
python-dotenv = "*"
termcolor = "*"
tiktoken = "*"
[package.extras]
anthropic = ["anthropic (>=0.23.1)"]
autobuild = ["chromadb", "huggingface-hub", "pysqlite3", "sentence-transformers"]
bedrock = ["boto3 (>=1.34.149)"]
blendsearch = ["flaml[blendsearch]"]
cerebras = ["cerebras-cloud-sdk (>=1.0.0)"]
cohere = ["cohere (>=5.5.8)"]
cosmosdb = ["azure-cosmos (>=4.2.0)"]
gemini = ["google-auth", "google-cloud-aiplatform", "google-generativeai (>=0.5,<1)", "pillow", "pydantic"]
graph = ["matplotlib", "networkx"]
graph-rag-falkor-db = ["graphrag-sdk"]
groq = ["groq (>=0.9.0)"]
jupyter-executor = ["ipykernel (>=6.29.0)", "jupyter-client (>=8.6.0)", "jupyter-kernel-gateway", "requests", "websocket-client"]
lmm = ["pillow", "replicate"]
long-context = ["llmlingua (<0.3)"]
mathchat = ["pydantic (==1.10.9)", "sympy", "wolframalpha"]
mistral = ["mistralai (>=1.0.1)"]
ollama = ["fix-busted-json (>=0.0.18)", "ollama (>=0.3.3)"]
redis = ["redis"]
retrievechat = ["beautifulsoup4", "chromadb (==0.5.3)", "ipython", "markdownify", "protobuf (==4.25.3)", "pypdf", "sentence-transformers"]
retrievechat-mongodb = ["beautifulsoup4", "chromadb (==0.5.3)", "ipython", "markdownify", "protobuf (==4.25.3)", "pymongo (>=4.0.0)", "pypdf", "sentence-transformers"]
retrievechat-pgvector = ["beautifulsoup4", "chromadb (==0.5.3)", "ipython", "markdownify", "pgvector (>=0.2.5)", "protobuf (==4.25.3)", "psycopg (>=3.1.18)", "pypdf", "sentence-transformers"]
retrievechat-qdrant = ["beautifulsoup4", "chromadb (==0.5.3)", "fastembed (>=0.3.1)", "ipython", "markdownify", "protobuf (==4.25.3)", "pypdf", "qdrant-client", "sentence-transformers"]
teachable = ["chromadb"]
test = ["ipykernel", "nbconvert", "nbformat", "pandas", "pre-commit", "pytest (>=6.1.1,<8)", "pytest-asyncio", "pytest-cov (>=5)"]
together = ["together (>=1.2)"]
types = ["ipykernel (>=6.29.0)", "jupyter-client (>=8.6.0)", "jupyter-kernel-gateway", "mypy (==1.9.0)", "pytest (>=6.1.1,<8)", "requests", "websocket-client"]
websockets = ["websockets (>=12.0,<13)"]
websurfer = ["beautifulsoup4", "markdownify", "pathvalidate", "pdfminer.six"]
[[package]]
name = "babel"
version = "2.16.0"
@@ -1135,6 +1188,17 @@ wrapt = ">=1.10,<2"
[package.extras]
dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"]
[[package]]
name = "diskcache"
version = "5.6.3"
description = "Disk Cache -- Disk and file backed persistent cache."
optional = false
python-versions = ">=3"
files = [
{file = "diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19"},
{file = "diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc"},
]
[[package]]
name = "distro"
version = "1.9.0"
@@ -1166,6 +1230,28 @@ idna = ["idna (>=3.6)"]
trio = ["trio (>=0.23)"]
wmi = ["wmi (>=1.5.1)"]
[[package]]
name = "docker"
version = "7.1.0"
description = "A Python library for the Docker Engine API."
optional = false
python-versions = ">=3.8"
files = [
{file = "docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0"},
{file = "docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c"},
]
[package.dependencies]
pywin32 = {version = ">=304", markers = "sys_platform == \"win32\""}
requests = ">=2.26.0"
urllib3 = ">=1.26.0"
[package.extras]
dev = ["coverage (==7.2.7)", "pytest (==7.4.2)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.1.0)", "ruff (==0.1.8)"]
docs = ["myst-parser (==0.18.0)", "sphinx (==5.1.1)"]
ssh = ["paramiko (>=2.4.3)"]
websockets = ["websocket-client (>=1.3.0)"]
[[package]]
name = "durationpy"
version = "0.7"
@@ -1206,18 +1292,18 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth
[[package]]
name = "fastapi"
version = "0.115.0"
version = "0.115.5"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
files = [
{file = "fastapi-0.115.0-py3-none-any.whl", hash = "sha256:17ea427674467486e997206a5ab25760f6b09e069f099b96f5b55a32fb6f1631"},
{file = "fastapi-0.115.0.tar.gz", hash = "sha256:f93b4ca3529a8ebc6fc3fcf710e5efa8de3df9b41570958abf1d97d843138004"},
{file = "fastapi-0.115.5-py3-none-any.whl", hash = "sha256:596b95adbe1474da47049e802f9a65ab2ffa9c2b07e7efee70eb8a66c9f2f796"},
{file = "fastapi-0.115.5.tar.gz", hash = "sha256:0e7a4d0dc0d01c68df21887cce0945e72d3c48b9f4f79dfe7a7d53aa08fbb289"},
]
[package.dependencies]
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
starlette = ">=0.37.2,<0.39.0"
starlette = ">=0.40.0,<0.42.0"
typing-extensions = ">=4.8.0"
[package.extras]
@@ -1271,6 +1357,43 @@ httpx-sse = "*"
Pillow = "*"
pydantic = "*"
[[package]]
name = "flaml"
version = "2.3.2"
description = "A fast library for automated machine learning and tuning"
optional = false
python-versions = ">=3.8"
files = [
{file = "FLAML-2.3.2-py3-none-any.whl", hash = "sha256:1ee6e8e76bf1d741b4da41e2a2a8c0638b36d90b0f60aac323b5568f54dcb9e7"},
{file = "flaml-2.3.2.tar.gz", hash = "sha256:4a1ec289ddaec36850cfc66f6fb335b8521df49ea31f6adb54ea63a5cebb6865"},
]
[package.dependencies]
NumPy = ">=1.17"
[package.extras]
autogen = ["diskcache", "openai (==0.27.8)", "termcolor"]
automl = ["lightgbm (>=2.3.1)", "pandas (>=1.1.4)", "scikit-learn (>=1.0.0)", "scipy (>=1.4.1)", "xgboost (>=0.90,<3.0.0)"]
autozero = ["packaging", "pandas", "scikit-learn"]
azureml = ["azureml-mlflow"]
benchmark = ["catboost (>=0.26)", "pandas (==1.1.4)", "psutil (==5.8.0)", "xgboost (==1.3.3)"]
blendsearch = ["optuna (>=2.8.0,<=3.6.1)", "packaging"]
catboost = ["catboost (>=0.26,<1.2)", "catboost (>=0.26,<=1.2.5)"]
forecast = ["hcrystalball (==0.1.10)", "holidays (<0.14)", "prophet (>=1.0.1)", "pytorch-forecasting (>=0.9.0)", "pytorch-lightning (==1.9.0)", "statsmodels (>=0.12.2)", "tensorboardX (==2.6)"]
hf = ["datasets", "nltk (<=3.8.1)", "rouge-score", "seqeval", "transformers[torch] (==4.26)"]
mathchat = ["diskcache", "openai (==0.27.8)", "pydantic (==1.10.9)", "sympy", "termcolor", "wolframalpha"]
nlp = ["datasets", "nltk (<=3.8.1)", "rouge-score", "seqeval", "transformers[torch] (==4.26)"]
nni = ["nni"]
notebook = ["jupyter"]
openai = ["diskcache", "openai (==0.27.8)"]
ray = ["ray[tune] (>=1.13,<2.0)"]
retrievechat = ["chromadb", "diskcache", "openai (==0.27.8)", "sentence-transformers", "termcolor", "tiktoken"]
spark = ["joblib (<=1.3.2)", "joblibspark (>=0.5.0)", "pyspark (>=3.2.0)"]
synapse = ["joblibspark (>=0.5.0)", "optuna (>=2.8.0,<=3.6.1)", "pyspark (>=3.2.0)"]
test = ["catboost (>=0.26)", "catboost (>=0.26,<1.2)", "coverage (>=5.3)", "dataclasses", "datasets", "dill", "hcrystalball (==0.1.10)", "ipykernel", "joblib (<=1.3.2)", "joblibspark (>=0.5.0)", "jupyter", "lightgbm (>=2.3.1)", "mlflow (==2.15.1)", "nbconvert", "nbformat", "nltk (<=3.8.1)", "openml", "optuna (>=2.8.0,<=3.6.1)", "packaging", "pandas (>=1.1.4)", "pandas (>=1.1.4,<2.0.0)", "pre-commit", "psutil (==5.8.0)", "pydantic (==1.10.9)", "pytest (>=6.1.1)", "pytorch-forecasting (>=0.9.0,<=0.10.1)", "pytorch-lightning (<1.9.1)", "requests (<2.29.0)", "rgf-python", "rouge-score", "scikit-learn (>=1.0.0)", "scipy (>=1.4.1)", "seqeval", "statsmodels (>=0.12.2)", "sympy", "tensorboardX (==2.6)", "thop", "torch", "torchvision", "transformers[torch] (==4.26)", "wolframalpha", "xgboost (>=0.90,<2.0.0)"]
ts-forecast = ["hcrystalball (==0.1.10)", "holidays (<0.14)", "prophet (>=1.0.1)", "statsmodels (>=0.12.2)"]
vw = ["scikit-learn", "vowpalwabbit (>=8.10.0,<9.0.0)"]
[[package]]
name = "flatbuffers"
version = "24.3.25"
@@ -2912,7 +3035,7 @@ langchain-core = ">=0.3.0,<0.4.0"
[[package]]
name = "langgraph"
version = "0.2.34"
version = "0.2.52"
description = "Building stateful, multi-actor applications with LLMs"
optional = false
python-versions = ">=3.9.0,<4.0"
@@ -2920,8 +3043,9 @@ files = []
develop = true
[package.dependencies]
langchain-core = ">=0.2.39,<0.4"
langgraph-checkpoint = "^2.0.0"
langchain-core = ">=0.2.43,<0.4.0,!=0.3.0,!=0.3.1,!=0.3.2,!=0.3.3,!=0.3.4,!=0.3.5,!=0.3.6,!=0.3.7,!=0.3.8,!=0.3.9,!=0.3.10,!=0.3.11,!=0.3.12,!=0.3.13,!=0.3.14"
langgraph-checkpoint = "^2.0.4"
langgraph-sdk = "^0.1.32"
[package.source]
type = "directory"
@@ -2929,7 +3053,7 @@ url = "libs/langgraph"
[[package]]
name = "langgraph-checkpoint"
version = "2.0.1"
version = "2.0.5"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -2946,7 +3070,7 @@ url = "libs/checkpoint"
[[package]]
name = "langgraph-checkpoint-postgres"
version = "2.0.1"
version = "2.0.3"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -2954,7 +3078,7 @@ files = []
develop = true
[package.dependencies]
langgraph-checkpoint = "^2.0.0"
langgraph-checkpoint = "^2.0.2"
orjson = ">=3.10.1"
psycopg = "^3.0.0"
psycopg-pool = "^3.0.0"
@@ -2965,7 +3089,7 @@ url = "libs/checkpoint-postgres"
[[package]]
name = "langgraph-checkpoint-sqlite"
version = "2.0.0"
version = "2.0.1"
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
optional = false
python-versions = "^3.9.0"
@@ -2974,7 +3098,7 @@ develop = true
[package.dependencies]
aiosqlite = "^0.20.0"
langgraph-checkpoint = "^2.0.0"
langgraph-checkpoint = "^2.0.2"
[package.source]
type = "directory"
@@ -2982,7 +3106,7 @@ url = "libs/checkpoint-sqlite"
[[package]]
name = "langgraph-sdk"
version = "0.1.32"
version = "0.1.36"
description = "SDK for interacting with LangGraph API"
optional = false
python-versions = "^3.9.0,<4.0"
@@ -4974,6 +5098,7 @@ description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs
optional = false
python-versions = ">=3.8"
files = [
{file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"},
{file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"},
]
@@ -4984,6 +5109,7 @@ description = "A collection of ASN.1-based protocols modules"
optional = false
python-versions = ">=3.8"
files = [
{file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"},
{file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"},
]
@@ -6043,6 +6169,11 @@ files = [
{file = "scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f60021ec1574e56632be2a36b946f8143bf4e5e6af4a06d85281adc22938e0dd"},
{file = "scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:394397841449853c2290a32050382edaec3da89e35b3e03d6cc966aebc6a8ae6"},
{file = "scikit_learn-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:57cc1786cfd6bd118220a92ede80270132aa353647684efa385a74244a41e3b1"},
{file = "scikit_learn-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9a702e2de732bbb20d3bad29ebd77fc05a6b427dc49964300340e4c9328b3f5"},
{file = "scikit_learn-1.5.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:b0768ad641981f5d3a198430a1d31c3e044ed2e8a6f22166b4d546a5116d7908"},
{file = "scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:178ddd0a5cb0044464fc1bfc4cca5b1833bfc7bb022d70b05db8530da4bb3dd3"},
{file = "scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7284ade780084d94505632241bf78c44ab3b6f1e8ccab3d2af58e0e950f9c12"},
{file = "scikit_learn-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:b7b0f9a0b1040830d38c39b91b3a44e1b643f4b36e36567b80b7c6bd2202a27f"},
{file = "scikit_learn-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:757c7d514ddb00ae249832fe87100d9c73c6ea91423802872d9e74970a0e40b9"},
{file = "scikit_learn-1.5.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:52788f48b5d8bca5c0736c175fa6bdaab2ef00a8f536cda698db61bd89c551c1"},
{file = "scikit_learn-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:643964678f4b5fbdc95cbf8aec638acc7aa70f5f79ee2cdad1eec3df4ba6ead8"},
@@ -6315,13 +6446,13 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
[[package]]
name = "starlette"
version = "0.38.6"
version = "0.40.0"
description = "The little ASGI library that shines."
optional = false
python-versions = ">=3.8"
files = [
{file = "starlette-0.38.6-py3-none-any.whl", hash = "sha256:4517a1409e2e73ee4951214ba012052b9e16f60e90d73cfb06192c19203bbb05"},
{file = "starlette-0.38.6.tar.gz", hash = "sha256:863a1588f5574e70a821dadefb41e4881ea451a47a3cd1b4df359d4ffefe5ead"},
{file = "starlette-0.40.0-py3-none-any.whl", hash = "sha256:c494a22fae73805376ea6bf88439783ecfba9aac88a43911b48c653437e784c4"},
{file = "starlette-0.40.0.tar.gz", hash = "sha256:1a3139688fb298ce5e2d661d37046a66ad996ce94be4d4983be019a23a04ea35"},
]
[package.dependencies]
@@ -6362,6 +6493,20 @@ files = [
doc = ["reno", "sphinx"]
test = ["pytest", "tornado (>=4.5)", "typeguard"]
[[package]]
name = "termcolor"
version = "2.5.0"
description = "ANSI color formatting for output in terminal"
optional = false
python-versions = ">=3.9"
files = [
{file = "termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8"},
{file = "termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f"},
]
[package.extras]
tests = ["pytest", "pytest-cov"]
[[package]]
name = "terminado"
version = "0.18.1"
@@ -7331,4 +7476,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "738e69cf406b140217cc8c3c0f2ccb5c2027d4701bcf2a347ba4b107f700ab2a"
content-hash = "776ee42630769f08e3896338f18ec81830166695d32d2208dc31dedb22d3b22d"
+1
View File
@@ -54,6 +54,7 @@ motor = "^3.5.1"
grandalf = "^0.8"
pyppeteer = "^2.0.0"
networkx = "^3.3"
autogen = { version = "^0.3.0", python = "<3.13,>=3.8" }
[tool.poetry.group.test]
optional = true