Compare commits

..
Author SHA1 Message Date
William FHandGitHub fca3e4513c release: Checkpointers 3.0 (#6313)
In this PR:

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

---------

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

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

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

ended up separating lint / testing deps during this process bc I was
getting a ton of not required deps while testing that were complicating
things :/
2025-10-17 08:26:52 -04:00
Lauren Hirata SinghandGitHub cf39fa5a91 fix(docs): fix catchall redirect (#6299) 2025-10-17 07:27:41 -04:00
Mason DaughertyandGitHub 7e666b58cd style: fixes for ref docs (#6297) 2025-10-16 20:58:16 -04:00
Asamu DavidandGitHub 3f400b38d1 fix(cli): install local deps in editable mode (#6294)
**Description** 

As part of this PR #6156, local deps are no longer installed in editable
mode. This change reverts that behaviour and ensures local packages are
installed in editable mode.

**Issue:** fixes #6288
2025-10-17 01:42:11 +01:00
Sydney RunkleandGitHub 6527df688c chore: release rcs for prebuilt + langgraph (#6296) 2025-10-17 00:35:56 +00:00
Sydney RunkleandGitHub aec841bd2a chore(prebuilt): un-deprecate tool node for now (#6295) 2025-10-16 20:27:24 -04:00
Sydney RunkleandGitHub 2d3121a17c chore: drop Python 3.9 (and syntax) (#6289)
* `strict=False` is the default, pyupgrade to min version 3.10 adds this
to be explicit w/ behavior
2025-10-16 20:17:46 -04:00
Lauren Hirata SinghandGitHub 06f9142419 chore(docs): Fix redirects (#6292) 2025-10-16 14:01:54 -04:00
Lauren Hirata SinghandGitHub a926450601 docs: Redirects for new docs (#5824)
This adds redirects to new Mintlify site and should be merged when old
LangGraph docs are deprecated (for v1)
2025-10-16 12:55:30 -04:00
abb96c0e2f chore(cli): re-word schema arguments (#6243)
Clean up config docstrings

---------

Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-10-16 12:28:09 +00:00
d9e3d83894 docs: style linting (#6260)
also fixes some links

---------

Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
Co-authored-by: ccurme <chester.curme@gmail.com>
Co-authored-by: Sydney Runkle <sydneymarierunkle@gmail.com>
Co-authored-by: William FH <13333726+hinthornw@users.noreply.github.com>
2025-10-16 11:25:50 +00:00
cedecd8ed6 chore(docs): Update OpenAPI spec from LangGraph API v0.4.42 (#6287)
This PR updates the OpenAPI specification with changes detected from the
LangGraph API server.

**Changes detected as of LangGraph API version 0.4.42**

This update was automatically generated by the sync workflow in the
langgraph-api repository.

Co-authored-by: hinthornw <hinthornw@users.noreply.github.com>
2025-10-16 07:15:51 -04:00
6bf9a7a4bc docs: relocate init args to __init__ (#6259)
Griffe expects parameter documentation to be in the method where
parameters are defined, not in the class docstring.

Class docstrings describe what the class does, while `__init__`
docstrings describe how to instantiate it with specific parameters.

---------

Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
Co-authored-by: ccurme <chester.curme@gmail.com>
Co-authored-by: Sydney Runkle <sydneymarierunkle@gmail.com>
Co-authored-by: William FH <13333726+hinthornw@users.noreply.github.com>
2025-10-16 07:11:42 -04:00
bce1dcfcd2 release(langgraph): v1 working branch (#6093)
Diff viewer for langgraph v1 alpha releases

---------

Co-authored-by: ccurme <chester.curme@gmail.com>
Co-authored-by: William FH <13333726+hinthornw@users.noreply.github.com>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-10-16 07:11:22 -04:00
Sam CrowderandGitHub 9b46cba1fb fix: rename away from LangGraph Platform (#6281)
some of these changes were obvious, and some were less obvious. In a few
spots, it felt like a judgement call if we should be saying LangSmith
Deployment of LangGraph Server. But hopefully either works.
2025-10-15 11:27:15 -07:00
Kathryn MayandGitHub a6dab889d1 docs: Update lgp home redirect to deployments (#6283) 2025-10-15 10:47:28 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Eugene Yurtsev
7c6dbb3972 chore(deps): bump astral-sh/setup-uv from 6 to 7 (#6273)
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 6
to 7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/astral-sh/setup-uv/releases">astral-sh/setup-uv's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0 🌈 node24 and a lot of bugfixes</h2>
<h2>Changes</h2>
<p>This release comes with a load of bug fixes and a speed up. Because
of switching from node20 to node24 it is also a breaking change. If you
are running on GitHub hosted runners this will just work, if you are
using self-hosted runners make sure, that your runners are up to date.
If you followed the normal installation instructions your self-hosted
runner will keep itself updated.</p>
<p>This release also removes the deprecated input
<code>server-url</code> which was used to download uv releases from a
different server.
The <a
href="https://github.com/astral-sh/setup-uv?tab=readme-ov-file#manifest-file">manifest-file</a>
input supersedes that functionality by adding a flexible way to define
available versions and where they should be downloaded from.</p>
<h3>Fixes</h3>
<ul>
<li>The action now respects when the environment variable
<code>UV_CACHE_DIR</code> is already set and does not overwrite it. It
now also finds <a
href="https://docs.astral.sh/uv/reference/settings/#cache-dir">cache-dir</a>
settings in config files if you set them.</li>
<li>Some users encountered problems that <a
href="https://github.com/astral-sh/setup-uv?tab=readme-ov-file#disable-cache-pruning">cache
pruning</a> took forever because they had some <code>uv</code> processes
running in the background. Starting with uv version <code>0.8.24</code>
this action uses <code>uv cache prune --ci --force</code> to ignore the
running processes</li>
<li>If you just want to install uv but not have it available in path,
this action now respects <code>UV_NO_MODIFY_PATH</code></li>
<li>Some other actions also set the env var <code>UV_CACHE_DIR</code>.
This action can now deal with that but as this could lead to unwanted
behavior in some edgecases a warning is now displayed.</li>
</ul>
<h3>Improvements</h3>
<p>If you are using minimum version specifiers for the version of uv to
install for example</p>
<pre lang="toml"><code>[tool.uv]
required-version = &quot;&gt;=0.8.17&quot;
</code></pre>
<p>This action now detects that and directly uses the latest version.
Previously it would download all available releases from the uv repo
to determine the highest matching candidate for the version specifier,
which took much more time.</p>
<p>If you are using other specifiers like <code>0.8.x</code> this action
still needs to download all available releases because the specifier
defines an upper bound (not 0.9.0 or later) and &quot;latest&quot; would
possibly not satisfy that.</p>
<h2>🚨 Breaking changes</h2>
<ul>
<li>Use node24 instead of node20 <a
href="https://github.com/eifinger"><code>@​eifinger</code></a> (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/608">#608</a>)</li>
<li>Remove deprecated input server-url <a
href="https://github.com/eifinger"><code>@​eifinger</code></a> (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/607">#607</a>)</li>
</ul>
<h2>🐛 Bug fixes</h2>
<ul>
<li>Respect UV_CACHE_DIR and cache-dir <a
href="https://github.com/eifinger"><code>@​eifinger</code></a> (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/612">#612</a>)</li>
<li>Use --force when pruning cache <a
href="https://github.com/eifinger"><code>@​eifinger</code></a> (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/611">#611</a>)</li>
<li>Respect UV_NO_MODIFY_PATH <a
href="https://github.com/eifinger"><code>@​eifinger</code></a> (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/603">#603</a>)</li>
<li>Warn when <code>UV_CACHE_DIR</code> has changed <a
href="https://github.com/jamesbraza"><code>@​jamesbraza</code></a> (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/601">#601</a>)</li>
</ul>
<h2>🚀 Enhancements</h2>
<ul>
<li>Shortcut to latest version for minimum version specifier <a
href="https://github.com/eifinger"><code>@​eifinger</code></a> (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/598">#598</a>)</li>
</ul>
<h2>🧰 Maintenance</h2>
<ul>
<li>Bump dependencies <a
href="https://github.com/eifinger"><code>@​eifinger</code></a> (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/613">#613</a>)</li>
<li>Fix test-uv-no-modify-path <a
href="https://github.com/eifinger"><code>@​eifinger</code></a> (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/604">#604</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/astral-sh/setup-uv/commit/3259c6206f993105e3a61b142c2d97bf4b9ef83d"><code>3259c62</code></a>
Bump deps (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/633">#633</a>)</li>
<li><a
href="https://github.com/astral-sh/setup-uv/commit/bf8e8ed895b7f686f85839659243f31a7df4a977"><code>bf8e8ed</code></a>
Split up documentation (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/632">#632</a>)</li>
<li><a
href="https://github.com/astral-sh/setup-uv/commit/9c6b5e9fb575cac8e82bb437dd7fc25a094bd85d"><code>9c6b5e9</code></a>
Add resolution-strategy input to support oldest compatible version
selection ...</li>
<li><a
href="https://github.com/astral-sh/setup-uv/commit/a5129e99f44f5d2ba22cdc54770745bd6f0d9c33"><code>a5129e9</code></a>
Add copilot-instructions.md (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/630">#630</a>)</li>
<li><a
href="https://github.com/astral-sh/setup-uv/commit/d18bcc753ac29c1ed721aa4a812a90eb937852d6"><code>d18bcc7</code></a>
Add value of UV_PYTHON_INSTALL_DIR to path (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/628">#628</a>)</li>
<li><a
href="https://github.com/astral-sh/setup-uv/commit/bd1f875aba1ebb6d38211b773b094ad1dcca58df"><code>bd1f875</code></a>
Set output venv when activate-environment is used (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/627">#627</a>)</li>
<li><a
href="https://github.com/astral-sh/setup-uv/commit/1a91c3851df47749b241e3c5c696350957c93ff0"><code>1a91c38</code></a>
chore: update known checksums for 0.9.2 (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/626">#626</a>)</li>
<li><a
href="https://github.com/astral-sh/setup-uv/commit/c79f606987cb4a0f3d1a95a3e44bcebfb0a9b303"><code>c79f606</code></a>
chore: update known checksums for 0.9.1 (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/625">#625</a>)</li>
<li><a
href="https://github.com/astral-sh/setup-uv/commit/e0249f159931b41f44fc8208c9b4cff085288cc9"><code>e0249f1</code></a>
Fall back to PR for updating known versions (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/623">#623</a>)</li>
<li><a
href="https://github.com/astral-sh/setup-uv/commit/6d2eb15b4979924f7be71aa06908c6211f80ac88"><code>6d2eb15</code></a>
Cache python installs (<a
href="https://redirect.github.com/astral-sh/setup-uv/issues/621">#621</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/astral-sh/setup-uv/compare/v6...v7">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=astral-sh/setup-uv&package-manager=github_actions&previous-version=6&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2025-10-14 10:55:27 -04:00
Kathryn MayandGitHub e08b8352a8 docs: Update old LDP site redirects to Platform docs merge (#6242)
This PR updates the redirects from the old LGP site to the new address
on the Mintlify site for the platform merge.
2025-10-14 10:23:19 -04:00
Sam CrowderandGitHub f7fe7c6698 fix: Revert "fix(cli): rename studio to debugger (#6246)" (#6261) 2025-10-09 23:35:36 +00:00
Caspar BroekhuizenandGitHub c2bc6ab8e9 chore(langgraph): bump langgraph version (#6257)
- Bump langgraph version to 0.6.10
2025-10-09 09:39:42 -07:00
Caspar BroekhuizenandGitHub 420550501f fix(langgraph): revert selective interrupt task scheduling (#6252)
Reverts langchain-ai/langgraph#6158
2025-10-08 12:34:01 -07:00
Sam CrowderandGitHub a0599139b8 fix(cli): rename studio to debugger (#6246)
begin process of renaming Studio to Debugger

keep --studio-url around for now as an option as well
2025-10-08 09:40:26 -07:00
Parker J. RuleandGitHub c2f359f708 chore(cli): bump to 0.4.3 (#6251)
Releases #6193 (and a few other minor changes).
2025-10-08 15:31:54 +00:00
Caspar BroekhuizenandGitHub 7f78a011fd chore(langgraph): bump version (#6245)
- Bump langgraph version from 0.6.8 to 0.6.9
2025-10-07 13:45:25 -07:00
Caspar BroekhuizenandGitHub 7d166bfb9f chore(checkpoint): bump patch version (#6244)
- Bump `langgraph-checkpoint` to 2.1.2
- Bump `langgraph-checkpoint-postgres` to 2.0.25 and raise
`langgraph-checkpoint` dep lower bound to 2.1.2
2025-10-07 10:41:24 -07:00
6cc8899818 fix(langgraph): selective interrupt task scheduling (#6158)
### Description

Prevents interrupt tasks from executing when the resume value has not
yet been specified.

Implemented for sync and async Pregel loop

If a task execution is skipped, the skipped interrupt is still included
in the graph result for consistency:
``` python
result = graph.invoke(...)
interrupts = result.get("__interrupt__", [])   # [interrupt_1, interrupt_2]

partial_result = graph.invoke(Command(resume=interrupt_1_resume_map), ...)
remaining_interrupts = partial_result.get("__interrupt__", [])  # [interrupt_2]
```

### Tests

- `test_interrupt_with_send_payloads`: test for a single resume map that
resumes all interrupts at once
- `test_interrupt_with_send_payloads_sequential_resume`: test for two
resume maps delivered in sequence
- `test_node_with_multiple_interrupts_requires_full_resume` test
optimization for multiple interrupts within a single node

Solves https://github.com/langchain-ai/langgraph/issues/6208

---------

Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2025-10-06 13:11:52 -07:00
1ba96f49bf fix(checkpoint): handle metadata.writes when serializing old checkpoints with Jsonb (#6236)
Issue

Support for `Checkpoint.metadata.writes` was dropped in `langgraph`
v0.5.x.

In `langgraph-checkpoint-postgres` v2.0.23, metadata was serialized with
`BasePostgresSaver._dump_metadata` -> `JsonPlusSerializer.dumps` which
handles `pydantic.BaseModel`.

In v2.0.23, metadata is serialized with `psycopg.types.json.Jsonb`,
which raises `TypeError: Object of type AIMessage is not JSON
serializable` when trying to serialize `writes`.

Solution

- Add `BaseCheckpointSaver.get_serializable_checkpoint_metadata` which
pops the `writes` key.
- Log deprecation warning when strange version combinations are used 

Solves https://github.com/langchain-ai/langgraph/issues/5769

---------

Co-authored-by: Alex Kondratev <56111142+soapun@users.noreply.github.com>
2025-10-06 11:27:34 -07:00
Caspar BroekhuizenandGitHub b0958115c1 fix(langgraph): task result from stream mode debug / tasks should match format from get_state_history / get_state (#6233)
Overview

Python port of https://github.com/langchain-ai/langgraphjs/pull/1551

Introduces `map_task_result_writes` to standardize task result format
across `get_state_history` and `map_task_result_writes` response
structures.

Solves https://github.com/langchain-ai/langgraph/issues/6073
2025-10-03 09:06:58 -07:00
Mason DaughertyandGitHub 04fb14d3ae fix(langgraph): don't use rst code blocks in docstrings (#6231) 2025-10-01 00:08:07 +00:00
Mason DaughertyandGitHub efb0e8c176 docs(langgraph): standardize version-added admonitions (#6230) 2025-09-30 18:39:12 -04:00
Caspar BroekhuizenandGitHub 0584eaa5c4 fix(langgraph): fix supersteps not populating task.result field (#6195)
### Description

Fix `bulk_update_state` and `abulk_update_state` so history populates
`tasks[*].result` when creating state via supersteps.

There was a branch in these functions that I'm guessing was meant to be
triggered when a `StateUpdate.as_node` was the name of a real node (not
`"__input__"` or `"__copy__"`), but was never being triggered because of
a condition `CONFIG_KEY_CHECKPOINT_ID not in config[CONF]`:
```python
# apply pending writes, if not on specific checkpoint
if (
    CONFIG_KEY_CHECKPOINT_ID not in config[CONF]
    and saved is not None
    and saved.pending_writes
):
    next_tasks = prepare_next_tasks(...)
```

From what I can tell, in the bulk-update flow every superstep carries a
`checkpoint_id`, so the condition was always false. That skipped
`prepare_next_tasks(...)` and prevented us from discovering the task IDs
that we would need to attach the task result. So, I removed this check.

I also replaced the `pending_writes` check with a more lenient one (just
check it is not None to satisfy type checkers). I found that
`saved.pending_writes` was sometimes just `[]`, and in this case we
would skip `prepare_next_tasks(...)` and never attach the task result.

Now for each task discovered in `prepare_next_tasks(...)`, I collect the
task IDs and reuse them when running all writers of the chosen node
(applying the updates).

### Tests

- `test_supersteps_populate_task_results` for `PregelLoop` and
`AsyncPregelLoop`
 
These tests build a single node graph and compare history from two
threads: one uses `.invoke` and the other is build from supersteps. Both
tests fail on main and pass with this PR.

### Issue

Solves https://github.com/langchain-ai/langgraph/issues/6206
2025-09-30 12:52:59 -07:00
Caspar BroekhuizenandGitHub 0c73af5624 fix(langgraph): revert -- reuse cached writes on nested resume to prevent task re-execution (#6227)
Reverts langchain-ai/langgraph#6161
2025-09-30 11:28:41 -07:00
Isaac FranciscoandGitHub 9d1bb9d86c chore(checkpoint-postgres): bump version (#6222) 2025-09-30 07:41:59 -07:00
Kathryn MayandGitHub 7c69cb54a6 docs: Update redirects for studio obs consolidation (#6220)
Update the redirects from the old docs to page changes in the new docs,
namely consolidating all the observability studio guides onto one page.

Dependent on: https://github.com/langchain-ai/docs/pull/681
2025-09-29 15:18:40 -04:00
Kathryn MayandGitHub c2279cbe6f docs: Update redirects for consolidating studio content (#6219)
Contingent on this PR merging:
https://github.com/langchain-ai/docs/pull/679
2025-09-29 13:02:44 -04:00
Sydney RunkleandGitHub 3a024cff6d release(langgraph): 0.6.8 (#6215) 2025-09-29 09:16:43 +00:00
4a0b2fa0ef chore(deps): upgrade dependencies with uv lock --upgrade (#6211)
This PR updates the dependencies in all Python packages using `uv lock
--upgrade`.

This is an automated PR created by the UV Lock Upgrade workflow.

Co-authored-by: sydney-runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-09-29 09:07:51 +00:00
Sydney RunkleandGitHub 36179ab1d2 fix(langgraph): handle multiple annotations w/ BaseChannel detection (#6210)
This PR ensures that even if a type has multiple annotations, we can
still detect the `BaseChannel` subclasses attached.

```py
class State(TypedDict):
    # recognized as EphemeralValue(int)
    foo: Annotated[int, EphemeralValue]

    # now recognized as EphemeralValue(int)
    bar: Annotated[int, EphemeralValue, OtherMetadata]

    # now recognized as EphemeralValue(int)
    baz: Annotated[int, SomeMetadata, EphemeralValue, OtherMetadata]
```
2025-09-26 17:22:24 -04:00
Parker J. RuleandGitHub 20ddb2b8b4 fix(langgraph): CheckpointTask.state can be a StateSnapshot (#6201)
This adds `StateSnapshot` to the union type annotation of
`CheckpointTask.state`.

The annotation was previously incomplete: `map_debug_checkpoint()`
generates `CheckpointPayload` objects from `PregelTask` objects, and the
`state` field in `PregelTask` is of type `None | RunnableConfig |
StateSnapshot`.
2025-09-25 22:30:19 +00:00
Parker J. RuleandGitHub b0a25f2794 feat(cli): add flag in HttpConfig for auth on custom routes (#6193)
We currently only support auth on the default routes; we'd like to be
able to support it on all (non-meta/liveness probe) routes by default.
This is the first step in that direction.
2025-09-25 17:17:33 -04:00
Parker J. RuleandGitHub ea0aebaa2e chore(sdk-py): refine FilterType, add subset containment to $contains docs (#6200)
The `$contains` auth operator supports subset containment checks, but
this has previously been undocumented. This updates `FilterType` and its
associated docstring to reflect this support.
2025-09-25 15:58:35 -04:00
Mason DaughertyandGitHub 26c68aa528 docs: update README and scripts for improved clarity (#6197) 2025-09-25 17:31:27 +00:00
Mason DaughertyandGitHub 4101aebeea chore(langgraph): clean up ruff format config (#6188)
Each of the settings present are already defaults in the ruff config:

https://docs.astral.sh/ruff/settings/
2025-09-25 17:07:00 +00:00
Mason DaughertyandGitHub 90ac06deb6 style(langgraph): docstring code format pass (#6187) 2025-09-25 13:00:21 -04:00
Isaac FranciscoandGitHub 32d66d48eb chore(sdk-py): type errors nicely (#6173)
This PR types errors in a nicer way
2025-09-24 12:58:38 -04:00
Isaac FranciscoandGitHub d933d455ec fix(cli): change prerelease behavior (#6156)
respect users config, use uv defaults
2025-09-24 09:54:24 -07:00
c421afba65 chore(langgraph): adding author credit for non-ASCII text support (#6186)
Co-authored-by: dcdmm <dcdmm@users.noreply.github.com>
2025-09-24 09:05:11 -04:00
6139dacef9 fix(langgraph): cleanup orphaned waiter task in AsyncPregelLoop (#6167)
### Summary

This PR fixes an issue where `AsyncPregelLoop` could leave behind an
orphaned `stream.wait()` task, resulting in warnings like:

```
Task was destroyed but it is pending!
```

### Related Discussion
This PR is in response to:
[langchain-ai/langgraph#6163](https://github.com/langchain-ai/langgraph/discussions/6163)


### Problem

* In the async path, `get_waiter()` was creating a new `asyncio.Task`
via

  ```python
  aioloop.create_task(stream.wait())
  ```

  but never tracked or cleaned it up.
* On cancellation or shutdown, these tasks remained pending and produced
warnings.

### Solution

* Changed `get_waiter()` to:

  * Maintain a **single waiter task** (similar to the sync path).
  * Auto-clear the reference when the task finishes.
* Added `_cleanup_waiter()`:

* On exit, attempt to wake the waiter (`stream._count.release()` if
available).
* Otherwise, cancel and `await` the pending task to ensure proper
cleanup.
* Wrapped the `while loop.tick():` block in a `try/finally` to guarantee
`_cleanup_waiter()` runs on exit.
* Added missing `import contextlib`.

### Impact

* Prevents orphaned `stream.wait()` tasks.
* Removes noisy `"Task was destroyed but it is pending!"` warnings.
* Behavior of async streaming remains unchanged, only lifecycle
management improved.

### Test Plan

* Reproduced the issue by running async streaming with cancellation.
* Verified warnings no longer appear after the fix.
* Ran existing test suite (all passing).

### Notes

* Sync and Async implementations now follow the same principle: *only
one waiter at a time, always cleaned up on exit*.
* Backwards-compatible; no API changes.


### Repro & Verification

To confirm the issue and the fix I used the following minimal repro
snippet:

```python
# lg_repro.py
import asyncio
import os

# Enable asyncio debug logs to surface pending task warnings
os.environ.setdefault("PYTHONASYNCIODEBUG", "1")

from langgraph.graph import START, END, StateGraph

State = dict

# Slow async node: processes once, then sleeps to keep the waiter alive
async def slow_node(state: State) -> State:
    await asyncio.sleep(0.2)  # simulate work
    state["count"] = state.get("count", 0) + 1
    await asyncio.sleep(1.0)  # keep stream.wait() waiter active
    return state

# Build simple graph: START -> slow_node -> END
builder = StateGraph(State)
builder.add_node("slow", slow_node)
builder.add_edge(START, "slow")
builder.add_edge("slow", END)
graph = builder.compile()

async def run_and_cancel():
    # astream with messages mode triggers internal stream.wait() waiter
    async def consumer():
        async for _ in graph.astream({"msg": "hi"}, stream_mode="messages"):
            await asyncio.sleep(0.05)

    t = asyncio.create_task(consumer(), name="astream-consumer")

    # Allow the stream to start, then cancel the consumer
    await asyncio.sleep(0.1)
    t.cancel()
    try:
        await t
    except asyncio.CancelledError:
        pass

    # Let loop settle to show pending waiter task if not cleaned
    await asyncio.sleep(0.05)

def main():
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    loop.set_debug(True)
    try:
        loop.run_until_complete(run_and_cancel())
    finally:
        # If the internal waiter is not cleaned, closing the loop will warn
        loop.close()

if __name__ == "__main__":
    main()
````

**How to run**

```powershell
# Before (main branch)
git checkout main
pip install -e libs/langgraph
$env:PYTHONASYNCIODEBUG=1; python lg_repro.py

# After (patched branch)
git checkout async-waiter-cleanup
pip install -e libs/langgraph
$env:PYTHONASYNCIODEBUG=1; python lg_repro.py
```

**Observed results**

* **main branch (before fix):**
  Shows warnings like:

  ```
  Task was destroyed but it is pending!
  ... coro=<AsyncQueue.wait() ...>
  created at langgraph/pregel/main.py:2927
  ```
* **patched branch (after fix):**
No warnings. The single waiter is properly cleaned up on exit via
`_cleanup_waiter()` (release semaphore if available, then cancel/await).

---

This confirms that the patch removes the orphaned `stream.wait()` task
and prevents
`"Task was destroyed but it is pending!"` warnings during
cancellation/shutdown.

---------

Co-authored-by: Caspar Broekhuizen <caspar@langchain.dev>
2025-09-23 17:11:17 -07:00
Caspar BroekhuizenandGitHub affaa90d2a fix(langgraph): fix graph rendering for defer=True (#6130)
### Description

Some graphs with `defer=True` nodes rendered incorrectly. E.g.:
* edge C2 -> E1 is missing and edge C2 -> END should not appear in #5772
* edge E3 -> END is missing and edge E -> END should not appear in #5182
* extra edge #5369

Fix:
* Record the destinations declared by get_static_writes for each node.
Build step_sources as a union of the runtime writes and the static
writes (instead of just runtime writes).
* Label deferred nodes with 'deferred'

### https://github.com/langchain-ai/langgraph/issues/5772

'Before' is how they were rendered before this PR

| No defer    | Before (defer `E1`) | After (defer `E1`)
| -------- | ------- | ------- |
| <img height="400" alt="defer_after"
src="https://github.com/user-attachments/assets/0a9fc992-1b6a-4c6d-8752-de54c703c329"
/> | <img height="400" alt="defer_before"
src="https://github.com/user-attachments/assets/825b09fc-3fb8-461a-9928-20c8d9cfc533"
/> | <img height="400" alt="defer_after"
src="https://github.com/user-attachments/assets/ce5334f7-b469-47b0-8f1e-35bda2544a4e"
/> |

Before:
* For deferred joins (NamedBarrierValueAfterFinish), a writer from an
upstream node may not produce a runtime task.writes entry until the
barrier opens. draw_graph() builds edges from task.writes, so one side
of the join (here C2) never gets recorded as a source, and C2 is seen as
a sink, so there is an implicit edge: C2 -> END edge added.

After:
* C2's write to the join channel is recorded even if the barrier hasn’t
opened. When E1 finally schedules, we correctly find both sources B2 and
C2 for the same trigger and emit edges: B2 -> E1 and C2 -> E1.

With C2 -> E1 present, C2 is no longer a terminus, so the unexpected
edge: C2 -> END is not added.

### Other graphs

Graphs for the most part remain unchanged. See: 

### #5182 

| No defer    | Before (defer `d`) | After (defer `d`)
| -------- | ------- | ------- |
| <img height="400" alt="defer_after"
src="https://github.com/user-attachments/assets/3509d25c-f3ad-473c-b877-c155b8008cd5"
/> | <img height="400" alt="defer_before"
src="https://github.com/user-attachments/assets/7af38e77-eb70-414d-b8fe-667da943f9e0"
/> | <img height="400" alt="defer_after"
src="https://github.com/user-attachments/assets/bc87a19f-b4fb-42d3-a6ee-5b0982d9af71"
/> |

### https://github.com/langchain-ai/langgraph/issues/5369

| No defer | Before (defer `595577`, `52642`) | After (defer `595577`,
`52642`)
| -------- | ------- | ------- |
| <img height="400" alt="defer_after"
src="https://github.com/user-attachments/assets/7c0824ce-3921-4dce-bc16-278f64289d28"
/> | <img height="400" alt="defer_before"
src="https://github.com/user-attachments/assets/28661079-7502-4912-874b-c086c0204a87"
/> | <img height="400" alt="defer_after"
src="https://github.com/user-attachments/assets/2a04956a-ed79-40e5-98b8-f6ecb2597a2e"
/> |
2025-09-23 12:47:50 -07:00
shaktiman101GitHubgoogle-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>William FHCaspar Broekhuizen
9f969f5fe1 fix(checkpoint-sqlite): Handle TTL refresh correctly in AsyncSqliteStore.asearch (#5213)
The original implementation for `refresh_on_read=True` in `asearch` for
AsyncSqliteStore used a CTE with an UPDATE statement, which is not
well-supported by SQLite in that specific construction, leading to a
syntax error.

This commit changes the approach:
1. `_prepare_batch_search_queries` in `BaseSqliteStore` no longer
constructs a CTE-based UPDATE. Instead, it returns a flag indicating if
TTL refresh is needed for the searched items.
2. `_batch_search_ops` in both `AsyncSqliteStore` and `SqliteStore` now
check this flag. If true, they perform a separate UPDATE statement after
fetching the search results to refresh the TTL of those items.

Additionally, a new test case `test_async_asearch_refresh_ttl` was added
and existing test logic was refined to accurately verify this behavior.

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: William FH <13333726+hinthornw@users.noreply.github.com>
Co-authored-by: Caspar Broekhuizen <caspar@langchain.dev>
2025-09-23 10:25:06 -07:00
Parker J. RuleandGitHub fb531b2473 feat(cli): add configuration for server customization ordering (#6179)
This adds a configuration option in `HttpConfig` that allows LangGraph
Platform users to apply custom authentication hooks before (other)
custom middleware. Currently, the order is fixed (custom middleware is
always evaluated before custom auth).

(Apologies for the noise in
[de187a9](https://github.com/langchain-ai/langgraph/pull/6179/commits/de187a989e807c5687c22db1fc065d24030fa6b7),
apparently from the forced application of new linter rules.)
2025-09-22 11:17:24 -04:00
fe4029b3b8 chore(deps): upgrade dependencies with uv lock --upgrade (#6176)
This PR updates the dependencies in all Python packages using `uv lock
--upgrade`.

This is an automated PR created by the UV Lock Upgrade workflow.

Co-authored-by: sydney-runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-09-22 10:21:40 -04:00
Nuno CamposandGitHub 7cd9a8e5dd sdk-py 0.2.9 2025-09-20 19:47:04 +01:00
Nuno CamposandGitHub 5ba02d5b46 feat: sdk-py: Reconnect to long-lived responses on wait/join/cancel endpoints (#6168)
- When connection is dropped while waiting, reconnect up to 5 times if a
Location header is present
2025-09-20 19:44:07 +01:00
Caspar BroekhuizenandGitHub 11834512db test(cli): add tests for util.py (#6172)
### Description

Added unit tests for util.py.

Authored by @oumizx. Had to copy #6113 into this separate PR because
langgraph/libs/cli was having issues with secrets.
2025-09-19 17:17:05 -07:00
eeb731c07e test: Add tests for before and limit parameters for list SqliteSaver (#5816)
**Description:** 

Add test for before and limit parameters for the list in SqliteSaver
which was marked as TODO.

---------

Co-authored-by: Caspar Broekhuizen <caspar@langchain.dev>
2025-09-19 16:53:30 -07:00
f0fced262a fix(langgraph): fix PostgresSaver crashing when loading older checkpoints (#6162)
### Description

https://github.com/langchain-ai/langgraph/issues/6137 and
https://github.com/langchain-ai/langgraph/issues/5677 reported issues
where older checkpoints read by AsyncPostgresSaver/PostgresSaver from
`langgraph-checkpoint-postgres==2.0.19` fail to read channel values,
throwing `NoneType object is not a mapping`. This was due to a bug in
how `channel_values` is assembled:
```python
"channel_values": {
    **value["checkpoint"].get("channel_values"),  # <--- if channel_values doesn't exist (old checkpoint), **None errors
    **self._load_blobs(value["channel_values"]),
},
```
This bug was observed for checkpoints generated by
`langgraph-checkpoint-postgres<=2.0.19`.

Fixed by providing a fallback to
`value["checkpoint"].get("channel_values")`:
```python
**value["checkpoint"],
"channel_values": {
    **(
        value["checkpoint"].get("channel_values") or {}
    ),  # 'or {}' needed for backwards compat with v3 checkpoints and below, as v4 introduced channel_values key
    **self._load_blobs(value["channel_values"]),
},
```

### Tests
Added test for AsyncPostgresSaver and test for PostgresSaver, using
monkeypatch to remove `channel_values` before CheckpointTuple is
assembled in `_load_checkpoint_tuple`.

### Solves
https://github.com/langchain-ai/langgraph/issues/6137 and
https://github.com/langchain-ai/langgraph/issues/5677

---------

Co-authored-by: Shahrukh Shaik <144558473+shahrukh-shaik@users.noreply.github.com>
2025-09-17 17:50:39 -07:00
8dc4465d05 fix(langgraph): reuse cached writes on nested resume to prevent task re-execution (#6161)
**Description**: fix #6050. 

Root cause: In nested graphs, the first tick after resume often included
a checkpoint_id, which set skip_done_tasks=False. This skipped matching
pending writes and re-executed already-completed helper @task on
subsequent resumes.

Change: Initialize skip_done_tasks=True when resuming inside a nested
graph. Use original config[CONF] for checkpoint_id presence, and
self.config[CONF] for resuming (current loop state). Added a concise
comment clarifying the different config sources.

**Issue**: #6050 

**Tests**: 
Add regression test `test_nested_graph_resume_reuses_cached_task_writes`

---------

Signed-off-by: jitokim <pigberger70@gmail.com>
Co-authored-by: Caspar Broekhuizen <casparbroekhuizen@gmail.com>
2025-09-17 12:35:07 -07:00
Nuno CamposandGitHub d0a3eaf601 sdk-py 0.2.8 2025-09-17 18:23:53 +01:00
Nuno CamposandGitHub 6f45f13952 fix: Handle SSE stream reconnection in Python SDK (#6159)
## Summary
- add a public accessor for the last received SSE event id
- retry async and sync SSE streams using the Location reconnect path and
Last-Event-ID while skipping empty events
- add regression tests that simulate interrupted SSE streams for both
async and sync clients

## Testing
- make format
- make lint
- make test

------
https://chatgpt.com/codex/tasks/task_e_68ca8bfa26cc832d98bcb359884962ec
2025-09-17 13:21:35 -04:00
Isaac FranciscoandGitHub 328129e5bd chore(sdk-py): allow UUIDs in config (#6151) 2025-09-17 09:57:47 -04:00
Caspar BroekhuizenandGitHub 2d05a17dfb fix(checkpoint): use tolerant float comparison to fix test failing on x86_64 architecture (#6157)
### Description

`test_embed_with_path` was failing on x86_64 architecture due to numeric
precision differences. `pytest.approx` was already used later on in this
test for float comparison, so this PR just updates a missed assertion.

Fixes https://github.com/langchain-ai/langgraph/issues/5845
2025-09-16 16:42:20 -07:00
Nuno Campos 5a36229e38 sdk-py 0.2.7 2025-09-16 16:29:27 +01:00
Nuno CamposandGitHub eeadeb282e fix: Ensure SSE streams flush trailing events (#6155)
## Summary
- ensure both async and sync HTTP clients flush the SSE decoder after
streaming
- add regression tests covering trailing SSE events without a
terminating blank line

## Testing
- make format
- make lint
- make test

------
https://chatgpt.com/codex/tasks/task_e_68c9727ca9f8832d9f207323c5e02a72
2025-09-16 16:25:47 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3a22aa0af3 chore(deps): bump actions/github-script from 7 to 8 (#6150)
Bumps [actions/github-script](https://github.com/actions/github-script)
from 7 to 8.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/github-script/releases">actions/github-script's
releases</a>.</em></p>
<blockquote>
<h2>v8.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update Node.js version support to 24.x by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/637">actions/github-script#637</a></li>
<li>README for updating actions/github-script from v7 to v8 by <a
href="https://github.com/sneha-krip"><code>@​sneha-krip</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/653">actions/github-script#653</a></li>
</ul>
<h2>⚠️ Minimum Compatible Runner Version</h2>
<p><strong>v2.327.1</strong><br />
<a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Release
Notes</a></p>
<p>Make sure your runner is updated to this version or newer to use this
release.</p>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/637">actions/github-script#637</a></li>
<li><a
href="https://github.com/sneha-krip"><code>@​sneha-krip</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/653">actions/github-script#653</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/github-script/compare/v7.1.0...v8.0.0">https://github.com/actions/github-script/compare/v7.1.0...v8.0.0</a></p>
<h2>v7.1.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Upgrade husky to v9 by <a
href="https://github.com/benelan"><code>@​benelan</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/482">actions/github-script#482</a></li>
<li>Add workflow file for publishing releases to immutable action
package by <a
href="https://github.com/Jcambass"><code>@​Jcambass</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/485">actions/github-script#485</a></li>
<li>Upgrade IA Publish by <a
href="https://github.com/Jcambass"><code>@​Jcambass</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/486">actions/github-script#486</a></li>
<li>Fix workflow status badges by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/497">actions/github-script#497</a></li>
<li>Update usage of <code>actions/upload-artifact</code> by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/512">actions/github-script#512</a></li>
<li>Clear up package name confusion by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/514">actions/github-script#514</a></li>
<li>Update dependencies with <code>npm audit fix</code> by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/515">actions/github-script#515</a></li>
<li>Specify that the used script is JavaScript by <a
href="https://github.com/timotk"><code>@​timotk</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/478">actions/github-script#478</a></li>
<li>chore: Add Dependabot for NPM and Actions by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/472">actions/github-script#472</a></li>
<li>Define <code>permissions</code> in workflows and update actions by
<a href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in
<a
href="https://redirect.github.com/actions/github-script/pull/531">actions/github-script#531</a></li>
<li>chore: Add Dependabot for .github/actions/install-dependencies by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/532">actions/github-script#532</a></li>
<li>chore: Remove .vscode settings by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/533">actions/github-script#533</a></li>
<li>ci: Use github/setup-licensed by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/473">actions/github-script#473</a></li>
<li>make octokit instance available as octokit on top of github, to make
it easier to seamlessly copy examples from GitHub rest api or octokit
documentations by <a
href="https://github.com/iamstarkov"><code>@​iamstarkov</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/508">actions/github-script#508</a></li>
<li>Remove <code>octokit</code> README updates for v7 by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/557">actions/github-script#557</a></li>
<li>docs: add &quot;exec&quot; usage examples by <a
href="https://github.com/neilime"><code>@​neilime</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/546">actions/github-script#546</a></li>
<li>Bump ruby/setup-ruby from 1.213.0 to 1.222.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/github-script/pull/563">actions/github-script#563</a></li>
<li>Bump ruby/setup-ruby from 1.222.0 to 1.229.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/github-script/pull/575">actions/github-script#575</a></li>
<li>Clearly document passing inputs to the <code>script</code> by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/603">actions/github-script#603</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/610">actions/github-script#610</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/benelan"><code>@​benelan</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/482">actions/github-script#482</a></li>
<li><a href="https://github.com/Jcambass"><code>@​Jcambass</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/485">actions/github-script#485</a></li>
<li><a href="https://github.com/timotk"><code>@​timotk</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/478">actions/github-script#478</a></li>
<li><a
href="https://github.com/iamstarkov"><code>@​iamstarkov</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/508">actions/github-script#508</a></li>
<li><a href="https://github.com/neilime"><code>@​neilime</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/546">actions/github-script#546</a></li>
<li><a href="https://github.com/nebuk89"><code>@​nebuk89</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/610">actions/github-script#610</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/github-script/compare/v7...v7.1.0">https://github.com/actions/github-script/compare/v7...v7.1.0</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/github-script/commit/ed597411d8f924073f98dfc5c65a23a2325f34cd"><code>ed59741</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/github-script/issues/653">#653</a>
from actions/sneha-krip/readme-for-v8</li>
<li><a
href="https://github.com/actions/github-script/commit/2dc352e4baefd91bec0d06f6ae2f1045d1687ca3"><code>2dc352e</code></a>
Bold minimum Actions Runner version in README</li>
<li><a
href="https://github.com/actions/github-script/commit/01e118c8d0d22115597e46514b5794e7bc3d56f1"><code>01e118c</code></a>
Update README for Node 24 runtime requirements</li>
<li><a
href="https://github.com/actions/github-script/commit/8b222ac82eda86dcad7795c9d49b839f7bf5b18b"><code>8b222ac</code></a>
Apply suggestion from <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a></li>
<li><a
href="https://github.com/actions/github-script/commit/adc0eeac992408a7b276994ca87edde1c8ce4d25"><code>adc0eea</code></a>
README for updating actions/github-script from v7 to v8</li>
<li><a
href="https://github.com/actions/github-script/commit/20fe497b3fe0c7be8aae5c9df711ac716dc9c425"><code>20fe497</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/github-script/issues/637">#637</a>
from actions/node24</li>
<li><a
href="https://github.com/actions/github-script/commit/e7b7f222b11a03e8b695c4c7afba89a02ea20164"><code>e7b7f22</code></a>
update licenses</li>
<li><a
href="https://github.com/actions/github-script/commit/2c81ba05f308415d095291e6eeffe983d822345b"><code>2c81ba0</code></a>
Update Node.js version support to 24.x</li>
<li>See full diff in <a
href="https://github.com/actions/github-script/compare/v7...v8">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-15 14:00:34 -04:00
Caspar BroekhuizenandGitHub 9467a0e2bb revert(langgraph): restore logic to surface interrupts for stream_mod… (#6141)
### Description

Revert change in #5201 that prevented the surfacing of interrupts when
`stream_mode="values"`. [Comment highlighting affected
lines](https://github.com/langchain-ai/langgraph/pull/5201#discussion_r2344884841)

Resolves #5409 

### Test

Add test to verify interrupts are properly surfaced when
`stream_mode="values"` (`test_interrupt_stream_mode_values`)
2025-09-14 19:11:19 -07:00
8b55dff7a5 chore(deps): upgrade dependencies with uv lock --upgrade (#6146)
This PR updates the dependencies in all Python packages using `uv lock
--upgrade`.

This is an automated PR created by the UV Lock Upgrade workflow.

To make tests pass:
* linting fixes
* whitespace fixes in snapshots

---------

Co-authored-by: sydney-runkle <54324534+sydney-runkle@users.noreply.github.com>
Co-authored-by: Sydney Runkle <sydneymarierunkle@gmail.com>
2025-09-14 19:36:43 -04:00
Huaiwu LiandGitHub a19b74154a docs: Add missing merge parameter documentation in push_ui_message (#6145)
**Description:**
This PR adds missing documentation for the `merge` parameter in the
`push_ui_message` function. The parameter was present in the function
signature but lacked documentation in the docstring, which could confuse
API users.

  Changes made:
  - Added clear documentation for the `merge` parameter
  - Explains the behavior difference between `merge=True`
  (merges props) and `merge=False` (replaces props)
  - Includes default value information

  **Issue:**
  N/A - Documentation improvement

  **Dependencies:**
  None
2025-09-14 23:34:34 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0607dc4611 chore(deps): bump hono from 4.9.6 to 4.9.7 in /docs/_scripts/js_translation/codeblocks (#6143)
Bumps [hono](https://github.com/honojs/hono) from 4.9.6 to 4.9.7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/honojs/hono/releases">hono's
releases</a>.</em></p>
<blockquote>
<h2>v4.9.7</h2>
<h2>Security</h2>
<ul>
<li>Fixed an issue in the <code>bodyLimit</code> middleware where the
body size limit could be bypassed when both <code>Content-Length</code>
and <code>Transfer-Encoding</code> headers were present. If you are
using this middleware, please update immediately. <a
href="https://github.com/honojs/hono/security/advisories/GHSA-92vj-g62v-jqhh">Security
Advisory</a></li>
</ul>
<h2>What's Changed</h2>
<ul>
<li>fix(client): Fix <code>parseResponse</code> not parsing json in
react native by <a
href="https://github.com/lr0pb"><code>@​lr0pb</code></a> in <a
href="https://redirect.github.com/honojs/hono/pull/4399">honojs/hono#4399</a></li>
<li>chore: add <code>.tool-versions</code> file by <a
href="https://github.com/3w36zj6"><code>@​3w36zj6</code></a> in <a
href="https://redirect.github.com/honojs/hono/pull/4397">honojs/hono#4397</a></li>
<li>chore: update <code>bun install</code> commands to use
<code>--frozen-lockfile</code> by <a
href="https://github.com/3w36zj6"><code>@​3w36zj6</code></a> in <a
href="https://redirect.github.com/honojs/hono/pull/4398">honojs/hono#4398</a></li>
<li>test(jwk): Add tests of JWK token verification by <a
href="https://github.com/buckett"><code>@​buckett</code></a> in <a
href="https://redirect.github.com/honojs/hono/pull/4402">honojs/hono#4402</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/lr0pb"><code>@​lr0pb</code></a> made
their first contribution in <a
href="https://redirect.github.com/honojs/hono/pull/4399">honojs/hono#4399</a></li>
<li><a href="https://github.com/buckett"><code>@​buckett</code></a> made
their first contribution in <a
href="https://redirect.github.com/honojs/hono/pull/4402">honojs/hono#4402</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/honojs/hono/compare/v4.9.6...v4.9.7">https://github.com/honojs/hono/compare/v4.9.6...v4.9.7</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/honojs/hono/commit/5ece99500fbcdc1026ab7f458f65cbe9eab29a6b"><code>5ece995</code></a>
4.9.7</li>
<li><a
href="https://github.com/honojs/hono/commit/605c70560b52f13af10379f79b76717042fafe8d"><code>605c705</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/honojs/hono/commit/6792789ec06bd14c96ecdf38a368f7d7526e601a"><code>6792789</code></a>
test(jwk): Add tests of JWK token verification (<a
href="https://redirect.github.com/honojs/hono/issues/4402">#4402</a>)</li>
<li><a
href="https://github.com/honojs/hono/commit/2f489b3562cd0d29075062b2ea0648ab92b88727"><code>2f489b3</code></a>
chore: update <code>bun install</code> commands to use
<code>--frozen-lockfile</code> (<a
href="https://redirect.github.com/honojs/hono/issues/4398">#4398</a>)</li>
<li><a
href="https://github.com/honojs/hono/commit/9b0a8f51ed15910b86cd2a6dd8f15b16b45e1c06"><code>9b0a8f5</code></a>
chore: add <code>.tool-versions</code> file (<a
href="https://redirect.github.com/honojs/hono/issues/4397">#4397</a>)</li>
<li><a
href="https://github.com/honojs/hono/commit/5b277d811cc655667683ae60141f739fa40b65e1"><code>5b277d8</code></a>
fix(client): Fix <code>parseResponse</code> not parsing json in react
native (<a
href="https://redirect.github.com/honojs/hono/issues/4399">#4399</a>)</li>
<li>See full diff in <a
href="https://github.com/honojs/hono/compare/v4.9.6...v4.9.7">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-14 19:11:30 -04:00
William FHandGitHub a3ee814539 chore(langgraph): Log when no values event is emitted from RemoteGraph (#6140) 2025-09-12 14:05:07 -07:00
William FHandGitHub b65140a892 chore(cli): Add config schema (#6142)
So you can IDE LSP support / autocompletion
2025-09-12 12:16:07 -07:00
Tadeo Donegana BraunschweigandGitHub 77a63608d1 docs: Update broken link in persistence_postgres.ipynb (#6135)
* Updated the relocation notice in `examples/persistence_postgres.ipynb`
to reference the correct new documentation at `add-memory.md` instead of
the previous notebook link.
2025-09-12 10:56:01 -04:00
Isaac FranciscoandGitHub c6179ca9d5 fix(cli): fix CLI integration test (#6129)
CLI integration tests were failing due to missing env vars, just needed
to copy to the places where the build commands were running from
2025-09-10 16:45:49 -07:00
f087567853 fix(cli): handle Docker SemVer build metadata in version parsing #5965 (#6024)
Description:
Corrects _parse_version to support Docker versions with SemVer build
metadata (e.g., 28.1.1+1), resolving #5965. Adds comprehensive unit
tests for version parsing, including normal, v-prefixed, prerelease,
build metadata, combined prerelease/build metadata, and edge cases with
missing components.

Issue:
Closes #5965

Dependencies:
None

---------

Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-09-10 14:31:07 -04:00
bdef6b3f5d fix(langgraph): get_graph generates unexpected conditional edge (#6122)
### Description

* Fix `get_graph()` generating an unexpected conditional edge to
`__end__` when the last step has a single (non-terminal) source and the
graph is cyclic.

### Issue
* There was a fallback path that was triggered in `draw_graph()` when,
for a Pregel instance; no termini exist and there is only a single step
source (the last one). In this case an edge was added: (last source) ->
`__end__`, even when another node already had a valid edge: (node) ->
`__end__`.

See this example:
<details>

<summary>code</summary>

```python
from langgraph.graph import END, START, StateGraph
from pydantic import BaseModel

class State(TypedDict):
    messages: list[str]

def chatbot_node(state: State) -> State:
    return {"messages": state["messages"] + ["chatbot"]}

def tools_node(state: State) -> State:
    return {"messages": state["messages"] + ["tools"]}

def human_node(state: State) -> State:
    return {"messages": state["messages"] + ["human"]}

def tools_condition(_: State) -> str:
    return "tools"

def end_condition(_: State) -> str:
    return "chatbot"

workflow = StateGraph(State)
workflow.add_node("chatbot", chatbot_node)
workflow.add_node("tools", tools_node)
workflow.add_node("human", human_node)

workflow.add_edge(START, "human")
workflow.add_edge("tools", "chatbot")
# graph_builder.add_edge("chatbot", "human") !!!

workflow.add_conditional_edges(
    "chatbot", tools_condition, {"tools": "tools", "human": "human"}
)
workflow.add_conditional_edges(
    "human", end_condition, {"chatbot": "chatbot", END: END}
)

app = workflow.compile()
mermaid = app.get_graph().draw_mermaid()
```

</details>

The code above, as-is, generates the graph on the left. There is an
unexpected conditional edge: chatbot -> `__end__`. If you uncomment the
commented line and introduce a static edge: chatbot -> human,
`get_graph()` returns the correct representation:

1 Without `graph_builder.add_edge("chatbot", "human")` | 2 With
`graph_builder.add_edge("chatbot", "human")`
:-------------------------:|:-------------------------:

![](https://github.com/user-attachments/assets/aa3149c2-ceee-4c0c-9c0c-e999caf042f0)
|
![](https://github.com/user-attachments/assets/ea53287f-1d68-47d6-8b36-9ec7ca1d52fa)

* In case 1), the graph is cyclic so termini is empty, and the last
`step_sources` set during the static walk contains only the chatbot
node, so an edge is added: chatbot -> `__end__`.
* In case 2), the graph is cyclic so termini is empty, and the last
`step_sources` set during the static walk contains only the human node,
so an edge is added: human -> `__end__`, but `add_edge()` dedups (the
edge already exists) so the graph appears correct.

### Solution
* Check that no valid edges: (node) -> `__end__` exist before triggering
the fallback path and creating an edge.

Before             |  After
:-------------------------:|:-------------------------:

![](https://github.com/user-attachments/assets/aa3149c2-ceee-4c0c-9c0c-e999caf042f0)
|
![](https://github.com/user-attachments/assets/9de4ab4f-6503-4894-bfda-37aba1d1be05)

After: The graph is cyclic so termini is empty, and the last
`step_sources` contains the chatbot node, but an edge already exists:
human -> `__end__`, so no more edges are added.

### Tests
* `test_get_graph_nonterminal_last_step_source()` which asserts no
unexpected edge to `__end__` is produced from the last nonterminal step
source.

### Issue

Closes #4394

---------

Co-authored-by: Sydney Runkle <sydneymarierunkle@gmail.com>
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-09-10 11:28:33 -04:00
Sydney RunkleandGitHub 677d941bb6 fix(langgraph): type checking for async w/ functional API (#6126)
Fixes https://github.com/langchain-ai/langgraph/issues/4140
Fixes https://github.com/langchain-ai/langgraph/issues/3310
2025-09-10 11:26:02 -04:00
Sydney RunkleandGitHub a43acc33bd feat(langgraph): prevent arbitrary resumes w/ multiple pending interrupts (#6108)
The idea here is that we don't want to allow resuming a graph w/ an
arbitrary resume value if there are multiple interrupts in the queue,
because the order in which interrupts enter the queue is not
deterministic. We want to instead enforce that each resume value is
mapped to an interrupt id.

Instead, when multiple interrupts are present, a user should invoke w/ a
resume map, mapping interrupt id -> resume value.

The logic was more complex than expected because there are 2 copies of
an interrupt in `checkpoint_pending_writes` for the cases w/ the
functional API, because an interrupt in a task interrupts the task and
entrypoint.

This is technically breaking (users resuming multiple hanging interrupts
w/ multiple resume calls can no longer do this... but the behavior for
this case was non-deterministic in the first place so we can sell this
as a fix).
2025-09-10 08:31:11 -04:00
Sydney RunkleandGitHub 326fd55e4f fix(langgraph): key error on runtime for config w/o configurable (#6106)
Fixes https://github.com/langchain-ai/langgraph/issues/6072

Long term we probably want a more robust approach to configurable
management in terms of required / not required attributes.
2025-09-10 12:19:33 +00:00
Lauren Hirata SinghandGitHub 6037f0210f docs: Update banner for docs deprecation notice (#6120) 2025-09-09 20:33:11 -04:00
Sydney RunkleandGitHub d9328027f9 fix: use langgraph template for docs issue (#6121) 2025-09-09 18:20:15 -04:00
Sydney RunkleandGitHub 7170e04aa6 chore: update issue templates to redirect docs stuff (#6119) 2025-09-09 18:17:11 -04:00
20581e61c0 fix(checkpoint-postgres): export PoolConfig from package init (#5934)
### Description
Export PoolConfig from langgraph.store.postgres.__init__ so the
documented import from langgraph.store.postgres import
AsyncPostgresStore, PoolConfig works as shown in the AsyncPostgresStore
examples. This resolves a docs vs. code inconsistency without changing
behavior.

### Issue
N/A

### Dependencies:
None

---------

Co-authored-by: William FH <13333726+hinthornw@users.noreply.github.com>
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-09-09 21:41:51 +00:00
4af07942ed chore(ci): Run CI int tests in parallel (#5976)
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-09-09 19:01:12 +00:00
Caspar BroekhuizenandGitHub 682f39e0d3 fix(checkpoint): preserve non-ascii text in InMemoryStore embeddings (#6111)
### Description
* Set `ensure_ascii=False` for all `json.dumps` calls in
`get_text_at_path`. Preserves non-ASCII text instead of embedding
`\uXXXX` escapes.

**Before**
```python
store.put(("user_123", "memories"), "1", {"text": "这是中文"})
# embeds {"text": "\\u8fd9\\u662f\\u4e2d\\u6587"}
```

**After**
```python
store.put(("user_123", "memories"), "1", {"text": "这是中文"})
# embeds {"text": "这是中文"}
```

### Tests & Docs

* Add unit test `test_non_ascii` that writes three records (Chinese,
Japanese, Korean) to an `InMemoryStore`, searches with the same strings,
and asserts the correct top hit with a score >= 0.15 for each.

### Issue
Fixes #5946
2025-09-09 17:52:11 +00:00
Sakshi GuptaandGitHub 7bbe8d8628 docs: Update graph-api.md for "Extended example: specifying LLM at runtime" (#5938)
docs (graphapi) : Handle Missing Context in LLM Invocation - Invoking
the LLM without explicitly passing a context parameter resulted in the
following error:
`AttributeError: 'NoneType' object has no attribute 'model_provider'`
This occurred because the context was None, and the system attempted to
access model_provider. This PR ensures that when context is not
provided, an empty context is passed explicitly. This allows the system
to correctly fall back to the default value defined in the
ContextSchema.model_provider attribute.
2025-09-09 15:43:42 +00:00
4dfd1c368c Fix to graph-api docs Send API example (#5576)
Very small update to docs, I think there is an add_edge that shouldn't
be there and a bug.

Adding the Annotated, resolves this error I received running the
example.

Traceback (most recent call last):
File "/Users/toddchaney/repos/work/importal-apps/python-worker/main.py",
line 52, in <module>
for step in graph.stream({"topic": "animals"}, stream_mode = ["updates",
"values"]):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File
"/Users/toddchaney/repos/work/importal-apps/python-worker/.venv/lib/python3.12/site-packages/langgraph/pregel/__init__.py",
line 2544, in stream
    loop.after_tick()
File
"/Users/toddchaney/repos/work/importal-apps/python-worker/.venv/lib/python3.12/site-packages/langgraph/pregel/loop.py",
line 526, in after_tick
    self.updated_channels = apply_writes(
                            ^^^^^^^^^^^^^
File
"/Users/toddchaney/repos/work/importal-apps/python-worker/.venv/lib/python3.12/site-packages/langgraph/pregel/algo.py",
line 299, in apply_writes
    if channels[chan].update(vals) and next_version is not None:
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^
File
"/Users/toddchaney/repos/work/importal-apps/python-worker/.venv/lib/python3.12/site-packages/langgraph/channels/last_value.py",
line 58, in update
    raise InvalidUpdateError(msg)
langgraph.errors.InvalidUpdateError: At key 'jokes': Can receive only
one value per step. Use an Annotated key to handle multiple values.
For troubleshooting, visit:
https://python.langchain.com/docs/troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE

Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2025-09-09 15:27:23 +00:00
b75daf093e docs(multi-agent page): fix a couple of broken links (#5813)
On this page:
https://langchain-ai.github.io/langgraph/concepts/multi_agent/
The last couple of links are broken

**First link:**

https://langchain-ai.github.io/langgraph/concepts/how-tos/subgraph.ipynb#different-state-schemas
should be updated to

https://langchain-ai.github.io/langgraph/how-tos/subgraph/#different-state-schemas

**Second link:**

https://langchain-ai.github.io/langgraph/concepts/how-tos/graph-api.ipynb#pass-private-state-between-nodes
should be updated to

https://langchain-ai.github.io/langgraph/how-tos/graph-api/#pass-private-state-between-nodes

---------

Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-09-09 15:14:25 +00:00
Mohammad MohtashimandGitHub faacbc1570 docs(prebuilt): remaining_steps explanation added in create_react_agent (#5847)
- **Description:** A better explanation of `remaining_steps` to clarify
what it does.
- **Issue:** #5548
2025-09-09 15:07:07 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sydney Runkle
f5f536ba78 chore(deps): bump actions/download-artifact from 4 to 5 (#5868)
Bumps
[actions/download-artifact](https://github.com/actions/download-artifact)
from 4 to 5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/download-artifact/releases">actions/download-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/407">actions/download-artifact#407</a></li>
<li>BREAKING fix: inconsistent path behavior for single artifact
downloads by ID by <a
href="https://github.com/GrantBirki"><code>@​GrantBirki</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/416">actions/download-artifact#416</a></li>
</ul>
<h2>v5.0.0</h2>
<h3>🚨 Breaking Change</h3>
<p>This release fixes an inconsistency in path behavior for single
artifact downloads by ID. <strong>If you're downloading single artifacts
by ID, the output path may change.</strong></p>
<h4>What Changed</h4>
<p>Previously, <strong>single artifact downloads</strong> behaved
differently depending on how you specified the artifact:</p>
<ul>
<li><strong>By name</strong>: <code>name: my-artifact</code> → extracted
to <code>path/</code> (direct)</li>
<li><strong>By ID</strong>: <code>artifact-ids: 12345</code> → extracted
to <code>path/my-artifact/</code> (nested)</li>
</ul>
<p>Now both methods are consistent:</p>
<ul>
<li><strong>By name</strong>: <code>name: my-artifact</code> → extracted
to <code>path/</code> (unchanged)</li>
<li><strong>By ID</strong>: <code>artifact-ids: 12345</code> → extracted
to <code>path/</code> (fixed - now direct)</li>
</ul>
<h4>Migration Guide</h4>
<h5> No Action Needed If:</h5>
<ul>
<li>You download artifacts by <strong>name</strong></li>
<li>You download <strong>multiple</strong> artifacts by ID</li>
<li>You already use <code>merge-multiple: true</code> as a
workaround</li>
</ul>
<h5>⚠️ Action Required If:</h5>
<p>You download <strong>single artifacts by ID</strong> and your
workflows expect the nested directory structure.</p>
<p><strong>Before v5 (nested structure):</strong></p>
<pre lang="yaml"><code>- uses: actions/download-artifact@v4
  with:
    artifact-ids: 12345
    path: dist
# Files were in: dist/my-artifact/
</code></pre>
<blockquote>
<p>Where <code>my-artifact</code> is the name of the artifact you
previously uploaded</p>
</blockquote>
<p><strong>To maintain old behavior (if needed):</strong></p>
<pre lang="yaml"><code>&lt;/tr&gt;&lt;/table&gt; 
</code></pre>
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/download-artifact/commit/634f93cb2916e3fdff6788551b99b062d0335ce0"><code>634f93c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/416">#416</a>
from actions/single-artifact-id-download-path</li>
<li><a
href="https://github.com/actions/download-artifact/commit/b19ff4302770b82aa4694b63703b547756dacce6"><code>b19ff43</code></a>
refactor: resolve download path correctly in artifact download tests
(mainly ...</li>
<li><a
href="https://github.com/actions/download-artifact/commit/e262cbee4ab8c473c61c59a81ad8e9dc760e90db"><code>e262cbe</code></a>
bundle dist</li>
<li><a
href="https://github.com/actions/download-artifact/commit/bff23f9308ceb2f06d673043ea6311519be6a87b"><code>bff23f9</code></a>
update docs</li>
<li><a
href="https://github.com/actions/download-artifact/commit/fff8c148a8fdd56aa81fcb019f0b5f6c65700c4d"><code>fff8c14</code></a>
fix download path logic when downloading a single artifact by id</li>
<li><a
href="https://github.com/actions/download-artifact/commit/448e3f862ab3ef47aa50ff917776823c9946035b"><code>448e3f8</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/407">#407</a>
from actions/nebuk89-patch-1</li>
<li><a
href="https://github.com/actions/download-artifact/commit/47225c44b359a5155efdbbbc352041b3e249fb1b"><code>47225c4</code></a>
Update README.md</li>
<li>See full diff in <a
href="https://github.com/actions/download-artifact/compare/v4...v5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/download-artifact&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-09-09 10:48:03 -04:00
Kenta MurataandGitHub 7284326160 fix(docs): Embed appropriate image files in graph-api.md (#6082)
This fixes some images that refer to incorrect files.
2025-09-09 14:42:26 +00:00
Shelton CuiandGitHub 62f7548532 docs(docs): fix incorrect return type in should_continue example (#6068)
Description:
The documentation for the should_continue function contained an
incorrect return type annotation.
It was shown as Literal["environment", END], but the actual logic
returns "Action" or END.

This PR updates the example to use Literal["Action", END] so that the
documentation matches the intended behavior of the function.

Issue:
N/A

Dependencies:
None
2025-09-09 14:33:27 +00:00
Sydney RunkleandGitHub e6a9e1d1c1 chore: update examples with context API (#5865) 2025-09-09 10:31:40 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sydney Runkle
94fa329100 chore(deps): bump actions/checkout from 4 to 5 (#5930)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to
5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
<li>Prepare v5.0.0 release by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2238">actions/checkout#2238</a></li>
</ul>
<h2>⚠️ Minimum Compatible Runner Version</h2>
<p><strong>v2.327.1</strong><br />
<a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Release
Notes</a></p>
<p>Make sure your runner is updated to this version or newer to use this
release.</p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4...v5.0.0">https://github.com/actions/checkout/compare/v4...v5.0.0</a></p>
<h2>v4.3.0</h2>
<h2>What's Changed</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
<li>Prepare release v4.3.0 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2237">actions/checkout#2237</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/motss"><code>@​motss</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li><a href="https://github.com/mouismail"><code>@​mouismail</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li><a href="https://github.com/benwells"><code>@​benwells</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li><a href="https://github.com/nebuk89"><code>@​nebuk89</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4...v4.3.0">https://github.com/actions/checkout/compare/v4...v4.3.0</a></p>
<h2>v4.2.2</h2>
<h2>What's Changed</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4.2.1...v4.2.2">https://github.com/actions/checkout/compare/v4.2.1...v4.2.2</a></p>
<h2>v4.2.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Jcambass"><code>@​Jcambass</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/1919">actions/checkout#1919</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4.2.0...v4.2.1">https://github.com/actions/checkout/compare/v4.2.0...v4.2.1</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>V5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>V4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>v4.2.0</h2>
<ul>
<li>Add Ref and Commit outputs by <a
href="https://github.com/lucacome"><code>@​lucacome</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1180">actions/checkout#1180</a></li>
<li>Dependency updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>- <a
href="https://redirect.github.com/actions/checkout/pull/1777">actions/checkout#1777</a>,
<a
href="https://redirect.github.com/actions/checkout/pull/1872">actions/checkout#1872</a></li>
</ul>
<h2>v4.1.7</h2>
<ul>
<li>Bump the minor-npm-dependencies group across 1 directory with 4
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1739">actions/checkout#1739</a></li>
<li>Bump actions/checkout from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1697">actions/checkout#1697</a></li>
<li>Check out other refs/* by commit by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1774">actions/checkout#1774</a></li>
<li>Pin actions/checkout's own workflows to a known, good, stable
version. by <a href="https://github.com/jww3"><code>@​jww3</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1776">actions/checkout#1776</a></li>
</ul>
<h2>v4.1.6</h2>
<ul>
<li>Check platform to set archive extension appropriately by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1732">actions/checkout#1732</a></li>
</ul>
<h2>v4.1.5</h2>
<ul>
<li>Update NPM dependencies by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1703">actions/checkout#1703</a></li>
<li>Bump github/codeql-action from 2 to 3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1694">actions/checkout#1694</a></li>
<li>Bump actions/setup-node from 1 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1696">actions/checkout#1696</a></li>
<li>Bump actions/upload-artifact from 2 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1695">actions/checkout#1695</a></li>
<li>README: Suggest <code>user.email</code> to be
<code>41898282+github-actions[bot]@users.noreply.github.com</code> by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1707">actions/checkout#1707</a></li>
</ul>
<h2>v4.1.4</h2>
<ul>
<li>Disable <code>extensions.worktreeConfig</code> when disabling
<code>sparse-checkout</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1692">actions/checkout#1692</a></li>
<li>Add dependabot config by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1688">actions/checkout#1688</a></li>
<li>Bump the minor-actions-dependencies group with 2 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1693">actions/checkout#1693</a></li>
<li>Bump word-wrap from 1.2.3 to 1.2.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1643">actions/checkout#1643</a></li>
</ul>
<h2>v4.1.3</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/checkout/commit/08c6903cd8c0fde910a37f88322edcfb5dd907a8"><code>08c6903</code></a>
Prepare v5.0.0 release (<a
href="https://redirect.github.com/actions/checkout/issues/2238">#2238</a>)</li>
<li><a
href="https://github.com/actions/checkout/commit/9f265659d3bb64ab1440b03b12f4d47a24320917"><code>9f26565</code></a>
Update actions checkout to use node 24 (<a
href="https://redirect.github.com/actions/checkout/issues/2226">#2226</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/checkout/compare/v4...v5">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-09-09 14:28:41 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sydney Runkle
264caa0684 chore(deps): bump amannn/action-semantic-pull-request from 5 to 6 (#5929)
Bumps
[amannn/action-semantic-pull-request](https://github.com/amannn/action-semantic-pull-request)
from 5 to 6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/amannn/action-semantic-pull-request/releases">amannn/action-semantic-pull-request's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.0</h2>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.5.3...v6.0.0">6.0.0</a>
(2025-08-13)</h2>
<h3>⚠ BREAKING CHANGES</h3>
<ul>
<li>Upgrade action to use Node.js 24 and ESM (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/287">#287</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>Upgrade action to use Node.js 24 and ESM (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/287">#287</a>)
(<a
href="https://github.com/amannn/action-semantic-pull-request/commit/bc0c9a79abfe07c0f08c498dd4a040bd22fe9b79">bc0c9a7</a>)</li>
</ul>
<h2>v5.5.3</h2>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.5.2...v5.5.3">5.5.3</a>
(2024-06-28)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Bump <code>braces</code> dependency (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/269">#269</a>.
by <a href="https://github.com/EelcoLos"><code>@​EelcoLos</code></a>)
(<a
href="https://github.com/amannn/action-semantic-pull-request/commit/2d952a1bf90a6a7ab8f0293dc86f5fdf9acb1915">2d952a1</a>)</li>
</ul>
<h2>v5.5.2</h2>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.5.1...v5.5.2">5.5.2</a>
(2024-04-24)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Bump tar from 6.1.11 to 6.2.1 (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/262">#262</a>
by <a href="https://github.com/EelcoLos"><code>@​EelcoLos</code></a>)
(<a
href="https://github.com/amannn/action-semantic-pull-request/commit/9a90d5a5ac979326e3bb9272750cdd4f192ce24a">9a90d5a</a>)</li>
</ul>
<h2>v5.5.1</h2>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.5.0...v5.5.1">5.5.1</a>
(2024-04-24)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Bump ip from 2.0.0 to 2.0.1 (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/263">#263</a>
by <a href="https://github.com/EelcoLos"><code>@​EelcoLos</code></a>)
(<a
href="https://github.com/amannn/action-semantic-pull-request/commit/5e7e9acca3ddc6a9d7b640fe1f905c4fff131f4a">5e7e9ac</a>)</li>
</ul>
<h2>v5.5.0</h2>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.4.0...v5.5.0">5.5.0</a>
(2024-04-23)</h2>
<h3>Features</h3>
<ul>
<li>Add outputs for <code>type</code>, <code>scope</code> and
<code>subject</code> (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/261">#261</a>
by <a href="https://github.com/bcaurel"><code>@​bcaurel</code></a>) (<a
href="https://github.com/amannn/action-semantic-pull-request/commit/b05f5f6423ef5cdfc7fdff00c4c10dd9a4f54aff">b05f5f6</a>)</li>
</ul>
<h2>v5.4.0</h2>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.3.0...v5.4.0">5.4.0</a>
(2023-11-03)</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/amannn/action-semantic-pull-request/blob/main/CHANGELOG.md">amannn/action-semantic-pull-request's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.2.0...v5.3.0">5.3.0</a>
(2023-09-25)</h2>
<h3>Features</h3>
<ul>
<li>Use Node.js 20 in action (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/240">#240</a>)
(<a
href="https://github.com/amannn/action-semantic-pull-request/commit/4c0d5a21fc86635c67cc57ffe89d842c34ade284">4c0d5a2</a>)</li>
</ul>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.1.0...v5.2.0">5.2.0</a>
(2023-03-16)</h2>
<h3>Features</h3>
<ul>
<li>Update dependencies by <a
href="https://github.com/EelcoLos"><code>@​EelcoLos</code></a> (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/229">#229</a>)
(<a
href="https://github.com/amannn/action-semantic-pull-request/commit/e797448a07516738bcfdd6f26ad1d1f84c58d0cc">e797448</a>)</li>
</ul>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.0.2...v5.1.0">5.1.0</a>
(2023-02-10)</h2>
<h3>Features</h3>
<ul>
<li>Add regex support to <code>scope</code> and
<code>disallowScopes</code> configuration (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/226">#226</a>)
(<a
href="https://github.com/amannn/action-semantic-pull-request/commit/403a6f89242a0d0d3acde94e6141b2e0f4da8838">403a6f8</a>)</li>
</ul>
<h3><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.0.1...v5.0.2">5.0.2</a>
(2022-10-17)</h3>
<h3>Bug Fixes</h3>
<ul>
<li>Upgrade <code>@actions/core</code> to avoid deprecation warnings (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/208">#208</a>)
(<a
href="https://github.com/amannn/action-semantic-pull-request/commit/91f4126c9e8625b9cadd64b02a03018fa22fc498">91f4126</a>)</li>
</ul>
<h3><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5.0.0...v5.0.1">5.0.1</a>
(2022-10-14)</h3>
<h3>Bug Fixes</h3>
<ul>
<li>Upgrade GitHub Action to use Node v16 (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/207">#207</a>)
(<a
href="https://github.com/amannn/action-semantic-pull-request/commit/6282ee339b067cb8eab05026f91153f873ad37fb">6282ee3</a>)</li>
</ul>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v4.6.0...v5.0.0">5.0.0</a>
(2022-10-11)</h2>
<h3>⚠ BREAKING CHANGES</h3>
<ul>
<li>Enum options need to be newline delimited (to allow whitespace
within them) (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/205">#205</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>Enum options need to be newline delimited (to allow whitespace
within them) (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/205">#205</a>)
(<a
href="https://github.com/amannn/action-semantic-pull-request/commit/c906fe1e5a4bcc61624931ca94da9672107bd448">c906fe1</a>)</li>
</ul>
<h2><a
href="https://github.com/amannn/action-semantic-pull-request/compare/v4.5.0...v4.6.0">4.6.0</a>
(2022-09-26)</h2>
<h3>Features</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/amannn/action-semantic-pull-request/commit/fdd4d3ddf614fbcd8c29e4b106d3bbe0cb2c605d"><code>fdd4d3d</code></a>
chore: Release 6.0.1 [skip ci]</li>
<li><a
href="https://github.com/amannn/action-semantic-pull-request/commit/58e4ab40f59be79f2c432bf003e34a31174e977a"><code>58e4ab4</code></a>
fix: Actually execute action (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/289">#289</a>)</li>
<li><a
href="https://github.com/amannn/action-semantic-pull-request/commit/04a8d177d951f6d3dff9f6fbfa336354c60a1112"><code>04a8d17</code></a>
chore: Release 6.0.0 [skip ci]</li>
<li><a
href="https://github.com/amannn/action-semantic-pull-request/commit/bc0c9a79abfe07c0f08c498dd4a040bd22fe9b79"><code>bc0c9a7</code></a>
feat!: Upgrade action to use Node.js 24 and ESM (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/287">#287</a>)</li>
<li><a
href="https://github.com/amannn/action-semantic-pull-request/commit/631ffdc0283366e1d671846987cd2532bebcaba7"><code>631ffdc</code></a>
build(deps): bump the github-action-workflows group with 2 updates (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/286">#286</a>)</li>
<li><a
href="https://github.com/amannn/action-semantic-pull-request/commit/c1807ceb58850d1d23bc4bb1de293f0f07ddc198"><code>c1807ce</code></a>
build: configure Dependabot (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/231">#231</a>)</li>
<li><a
href="https://github.com/amannn/action-semantic-pull-request/commit/335288255954904a41ddda8947c8f2c844b8bfeb"><code>3352882</code></a>
docs: Remove <code>synchronize</code> trigger (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/281">#281</a>)</li>
<li><a
href="https://github.com/amannn/action-semantic-pull-request/commit/04501d43b574e4c1d23c629ffe4dcec27acfdeff"><code>04501d4</code></a>
docs: More restrictive permissions (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/280">#280</a>)</li>
<li><a
href="https://github.com/amannn/action-semantic-pull-request/commit/40166f00814508ec3201fc8595b393d451c8cd80"><code>40166f0</code></a>
chore: Update actions in release workflow (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/276">#276</a>)</li>
<li><a
href="https://github.com/amannn/action-semantic-pull-request/commit/80c0371c57c5142ed6c844270bba1864bac8a4c6"><code>80c0371</code></a>
docs: Mention <code>reopened</code> trigger in README (<a
href="https://redirect.github.com/amannn/action-semantic-pull-request/issues/272">#272</a>
by <a
href="https://github.com/garysassano"><code>@​garysassano</code></a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/amannn/action-semantic-pull-request/compare/v5...v6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=amannn/action-semantic-pull-request&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-09-09 10:28:09 -04:00
Sydney RunkleandGitHub cd33de2ad1 chore: minor CI/link fixes (#6116) 2025-09-09 14:22:19 +00:00
692f177a10 docs(langgraph): unindent text (#5906)
**Description:** unintend wrongly indented bulleted list and paragraph.

Co-authored-by: Lauren Hirata Singh <lauren@langchain.dev>
2025-09-09 14:19:24 +00:00
Sakshi GuptaandGitHub 14d4be6c4a docs: Import RetryPolicy from types not pregel (#5941)
docs (graphapi.md) : With the new langgraph version, the RetryPolicy
needs to be imported from `langgraph.types` to avoid `ImportError:
cannot import name 'RetryPolicy' from 'langgraph.pregel'`
2025-09-09 14:18:48 +00:00
Kathryn MayandGitHub 2e133d6189 docs: Add redirect to standalone server page (#5855)
add redirect for deployment name change
2025-09-09 10:17:14 -04:00
Xin ZhangandGitHub 89de950307 docs (langgraph): Fix bug in the code example (#6114)
- **Description:** Fix the wrong variable name in the example of
`memory.md`.
  - **Dependencies:** NA.
2025-09-09 12:17:07 +00:00
xindooandGitHub 788b62c0fb docs(langgraph):Correct variable name in context.md (#6022)
Ensures the variable name used in the documentation accurately reflects
the codebase.
2025-09-08 20:54:42 +00:00
zzxxj216andGitHub 1539a55d2c docs(langgraph): correct typo "runtie" to "runtime" in StateGraph (#6060)
**Description:** Fix a typo where "runtie" was incorrectly used instead
of "runtime" in line 158 of the StateGraph class in state.py. This
resolves the example error caused by the misspelled variable name.

**Dependencies:** None
2025-09-08 20:54:35 +00:00
Mohammad YehyaandGitHub f3055178f3 docs: Fix Example Code Snippet in HITL (#5999)
Description: Fixing the example code snippet at this
[url](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/add-human-in-the-loop/#resume-multiple-interrupts-with-one-invocation).

Fixes: 
- i.interrupt_id -> i.id, since interrupt_id is deprecrated
- f"human input for prompt {i.value}" -> f"edited text for
{i.value['text_to_revise']}", to match the output statement
- parent.get_state -> graph.get_state, no variable named parent
- get_state(thread_config) -> get_state(config), no variable named
thread_config
2025-09-08 20:53:22 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sydney Runkle
c0067cd304 chore: bump actions/upload-pages-artifact from 3 to 4 (#6004)
Bumps
[actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact)
from 3 to 4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/upload-pages-artifact/releases">actions/upload-pages-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v4.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Potentially breaking change: hidden files (specifically dotfiles)
will not be included in the artifact by <a
href="https://github.com/tsusdere"><code>@​tsusdere</code></a> in <a
href="https://redirect.github.com/actions/upload-pages-artifact/pull/102">actions/upload-pages-artifact#102</a>
If you need to include dotfiles in your artifact: instead of using this
action, create your own artifact according to these requirements <a
href="https://github.com/actions/upload-pages-artifact?tab=readme-ov-file#artifact-validation">https://github.com/actions/upload-pages-artifact?tab=readme-ov-file#artifact-validation</a></li>
<li>Pin <code>actions/upload-artifact</code> to SHA by <a
href="https://github.com/heavymachinery"><code>@​heavymachinery</code></a>
in <a
href="https://redirect.github.com/actions/upload-pages-artifact/pull/127">actions/upload-pages-artifact#127</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-pages-artifact/compare/v3.0.1...v4.0.0">https://github.com/actions/upload-pages-artifact/compare/v3.0.1...v4.0.0</a></p>
<h2>v3.0.1</h2>
<h1>Changelog</h1>
<ul>
<li>Group tar's output to prevent it from messing up action logs <a
href="https://github.com/SilverRainZ"><code>@​SilverRainZ</code></a> (<a
href="https://redirect.github.com/actions/upload-pages-artifact/issues/94">#94</a>)</li>
<li>Update README.md <a
href="https://github.com/uiolee"><code>@​uiolee</code></a> (<a
href="https://redirect.github.com/actions/upload-pages-artifact/issues/88">#88</a>)</li>
<li>Bump the non-breaking-changes group with 1 update <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> (<a
href="https://redirect.github.com/actions/upload-pages-artifact/issues/92">#92</a>)</li>
<li>Update Dependabot config to group non-breaking changes <a
href="https://github.com/JamesMGreene"><code>@​JamesMGreene</code></a>
(<a
href="https://redirect.github.com/actions/upload-pages-artifact/issues/91">#91</a>)</li>
<li>Bump actions/checkout from 3 to 4 <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> (<a
href="https://redirect.github.com/actions/upload-pages-artifact/issues/76">#76</a>)</li>
</ul>
<p>See details of <a
href="https://github.com/actions/upload-pages-artifact/compare/v3.0.0...v3.0.1">all
code changes</a> since previous release.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/upload-pages-artifact/commit/7b1f4a764d45c48632c6b24a0339c27f5614fb0b"><code>7b1f4a7</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-pages-artifact/issues/127">#127</a>
from heavymachinery/pin-sha</li>
<li><a
href="https://github.com/actions/upload-pages-artifact/commit/4cc19c7d3f3e6c87c68366501382a03c8b1ba6db"><code>4cc19c7</code></a>
Pin <code>actions/upload-artifact</code> to SHA</li>
<li><a
href="https://github.com/actions/upload-pages-artifact/commit/2d163be3ddce01512f3eea7ac5b7023b5d643ce1"><code>2d163be</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-pages-artifact/issues/107">#107</a>
from KittyChiu/main</li>
<li><a
href="https://github.com/actions/upload-pages-artifact/commit/c70484322b1c476728dcd37fac23c4dea2a0c51a"><code>c704843</code></a>
fix: linted README</li>
<li><a
href="https://github.com/actions/upload-pages-artifact/commit/9605915f1d2fc79418cdce4d5fbe80511c457655"><code>9605915</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-pages-artifact/issues/106">#106</a>
from KittyChiu/kittychiu/update-readme-1</li>
<li><a
href="https://github.com/actions/upload-pages-artifact/commit/e59cdfe6d6b061aab8f0619e759cded914f3ab03"><code>e59cdfe</code></a>
Update README.md</li>
<li><a
href="https://github.com/actions/upload-pages-artifact/commit/a2d67043267d885050434d297d3dd3a3a14fd899"><code>a2d6704</code></a>
doc: updated usage section in readme</li>
<li><a
href="https://github.com/actions/upload-pages-artifact/commit/984864e7b70fb5cb764344dc9c4b5c087662ef50"><code>984864e</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-pages-artifact/issues/105">#105</a>
from actions/Jcambass-patch-1</li>
<li><a
href="https://github.com/actions/upload-pages-artifact/commit/45dc78884ca148c05eddcd8ac0a804d3365e9014"><code>45dc788</code></a>
Add workflow file for publishing releases to immutable action
package</li>
<li><a
href="https://github.com/actions/upload-pages-artifact/commit/efaad07812d4b9ad2e8667cd46426fdfb7c22e22"><code>efaad07</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-pages-artifact/issues/102">#102</a>
from actions/hidden-files</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/upload-pages-artifact/compare/v3...v4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-pages-artifact&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-09-08 20:52:35 +00:00
Kenta MurataandGitHub b90d7c4e58 fix(docs): Fix indentation of info block in graph-api.md (#6081)
The "Why split application steps into a sequence with LangGraph?" info
block in `graph-api.md` is broken.

Before:
<img width="889" height="529" alt="image"
src="https://github.com/user-attachments/assets/7c60e4f0-7de2-4ffa-8493-ad958e4af211"
/>


After:
<img width="895" height="494" alt="image"
src="https://github.com/user-attachments/assets/3aefe0ca-1cb8-4d03-b1ed-7c3475204c88"
/>
2025-09-08 20:51:16 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2c14b1d658 chore bump hono from 4.8.9 to 4.9.6 in /docs/_scripts/js_translation/codeblocks (#6078)
Bumps [hono](https://github.com/honojs/hono) from 4.8.9 to 4.9.6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/honojs/hono/releases">hono's
releases</a>.</em></p>
<blockquote>
<h2>v4.9.6</h2>
<h2>Security</h2>
<p>Fixed a bug in URL path parsing (<code>getPath</code>) that could
cause path confusion under malformed requests.</p>
<p>If you rely on reverse proxies (e.g. Nginx) for ACLs or restrict
access to endpoints like <code>/admin</code>, please update
immediately.</p>
<p>See advisory for details: GHSA-9hp6-4448-45g2</p>
<h2>What's Changed</h2>
<ul>
<li>chore: update packages in the router bench by <a
href="https://github.com/yusukebe"><code>@​yusukebe</code></a> in <a
href="https://redirect.github.com/honojs/hono/pull/4386">honojs/hono#4386</a></li>
<li>chore(benchmarks): remove comment-out from router bench by <a
href="https://github.com/yusukebe"><code>@​yusukebe</code></a> in <a
href="https://redirect.github.com/honojs/hono/pull/4387">honojs/hono#4387</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/honojs/hono/compare/v4.9.5...v4.9.6">https://github.com/honojs/hono/compare/v4.9.5...v4.9.6</a></p>
<h2>v4.9.5</h2>
<h2>What's Changed</h2>
<ul>
<li>chore: replace supertest with undici by <a
href="https://github.com/BarryThePenguin"><code>@​BarryThePenguin</code></a>
in <a
href="https://redirect.github.com/honojs/hono/pull/4365">honojs/hono#4365</a></li>
<li>fix(aws-lambda): preserve percent-encoded values in query strings by
<a href="https://github.com/yusukebe"><code>@​yusukebe</code></a> in <a
href="https://redirect.github.com/honojs/hono/pull/4372">honojs/hono#4372</a></li>
<li>feat(cors): Allow async functions for <code>origin</code> and
<code>allowMethods</code> by <a
href="https://github.com/jobrk"><code>@​jobrk</code></a> in <a
href="https://redirect.github.com/honojs/hono/pull/4373">honojs/hono#4373</a></li>
<li>feat(cors): Correct origin function return type asynchronously
returning null or undefined for origin by <a
href="https://github.com/jobrk"><code>@​jobrk</code></a> in <a
href="https://redirect.github.com/honojs/hono/pull/4375">honojs/hono#4375</a></li>
<li>fix(service-worker): correct args for <code>app.fetch</code> in
<code>handle</code> by <a
href="https://github.com/yusukebe"><code>@​yusukebe</code></a> in <a
href="https://redirect.github.com/honojs/hono/pull/4374">honojs/hono#4374</a></li>
<li>fix(language-detector): Detect language from path after getPath
changed by <a
href="https://github.com/iflamed"><code>@​iflamed</code></a> in <a
href="https://redirect.github.com/honojs/hono/pull/4369">honojs/hono#4369</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/jobrk"><code>@​jobrk</code></a> made
their first contribution in <a
href="https://redirect.github.com/honojs/hono/pull/4373">honojs/hono#4373</a></li>
<li><a href="https://github.com/iflamed"><code>@​iflamed</code></a> made
their first contribution in <a
href="https://redirect.github.com/honojs/hono/pull/4369">honojs/hono#4369</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/honojs/hono/compare/v4.9.4...v4.9.5">https://github.com/honojs/hono/compare/v4.9.4...v4.9.5</a></p>
<h2>v4.9.4</h2>
<h2>What's Changed</h2>
<ul>
<li>chore: add a type cast to run <code>deno publish</code> by <a
href="https://github.com/yusukebe"><code>@​yusukebe</code></a> in <a
href="https://redirect.github.com/honojs/hono/pull/4364">honojs/hono#4364</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/honojs/hono/compare/v4.9.3...v4.9.4">https://github.com/honojs/hono/compare/v4.9.3...v4.9.4</a></p>
<h2>v4.9.3</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(csrf): Add modern CSRF protection with Fetch Metadata support
by <a href="https://github.com/meck93"><code>@​meck93</code></a> in <a
href="https://redirect.github.com/honojs/hono/pull/4353">honojs/hono#4353</a></li>
<li>tests: use vitest projects by <a
href="https://github.com/BarryThePenguin"><code>@​BarryThePenguin</code></a>
in <a
href="https://redirect.github.com/honojs/hono/pull/4359">honojs/hono#4359</a></li>
<li>feat(proxy): add <code>customFetch</code> option to allow custom
fetch function by <a
href="https://github.com/yusukebe"><code>@​yusukebe</code></a> in <a
href="https://redirect.github.com/honojs/hono/pull/4360">honojs/hono#4360</a></li>
<li>chore: update <code>typescript</code> to <code>5.9.2</code> by <a
href="https://github.com/yusukebe"><code>@​yusukebe</code></a> in <a
href="https://redirect.github.com/honojs/hono/pull/4362">honojs/hono#4362</a></li>
<li>chore: add <code>packageManager</code> field to
<code>package.json</code> by <a
href="https://github.com/yusukebe"><code>@​yusukebe</code></a> in <a
href="https://redirect.github.com/honojs/hono/pull/4363">honojs/hono#4363</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/honojs/hono/compare/v4.9.2...v4.9.3">https://github.com/honojs/hono/compare/v4.9.2...v4.9.3</a></p>
<h2>v4.9.2</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/honojs/hono/commit/7f4311c010dbd15bd25e16551ecf58059887d105"><code>7f4311c</code></a>
4.9.6</li>
<li><a
href="https://github.com/honojs/hono/commit/1d79aedc3f82d8c9969b115fe61bc4bd705ec8de"><code>1d79aed</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/honojs/hono/commit/adecab1caeb2665c8da83a470de958566890d6ba"><code>adecab1</code></a>
chore(benchmarks): remove comment-out from router bench (<a
href="https://redirect.github.com/honojs/hono/issues/4387">#4387</a>)</li>
<li><a
href="https://github.com/honojs/hono/commit/b3d5b404291a71ec088c5f5c9d6f2940150fbb5b"><code>b3d5b40</code></a>
chore: update packages in the router bench (<a
href="https://redirect.github.com/honojs/hono/issues/4386">#4386</a>)</li>
<li><a
href="https://github.com/honojs/hono/commit/98cb9633174a9a1a5279c37ee2a262658c6af60c"><code>98cb963</code></a>
4.9.5</li>
<li><a
href="https://github.com/honojs/hono/commit/b3e8cab13da828f2af84fe6dcce6e2e8a156b85f"><code>b3e8cab</code></a>
fix(language-detector): Detect language from path after getPath changed
(<a
href="https://redirect.github.com/honojs/hono/issues/4369">#4369</a>)</li>
<li><a
href="https://github.com/honojs/hono/commit/0e3db674ad3f40be215a55a18062dd8e387ce525"><code>0e3db67</code></a>
fix(service-worker): correct args for <code>app.fetch</code> in
<code>handle</code> (<a
href="https://redirect.github.com/honojs/hono/issues/4374">#4374</a>)</li>
<li><a
href="https://github.com/honojs/hono/commit/c4577e93746c4642d5e663509febcb803d20f47e"><code>c4577e9</code></a>
fix(cors): Allow returning null or undefined for origin (<a
href="https://redirect.github.com/honojs/hono/issues/4375">#4375</a>)</li>
<li><a
href="https://github.com/honojs/hono/commit/5bfbff8acf54395174d54c65ad8d796493c2b7ea"><code>5bfbff8</code></a>
feat(cors): Allow async functions for <code>origin</code> and
<code>allowMethods</code> (<a
href="https://redirect.github.com/honojs/hono/issues/4373">#4373</a>)</li>
<li><a
href="https://github.com/honojs/hono/commit/a2685692543ceb0a4cfcc1e6a95f4f9cda73f14f"><code>a268569</code></a>
fix(aws-lambda): preserve percent-encoded values in query strings (<a
href="https://redirect.github.com/honojs/hono/issues/4372">#4372</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/honojs/hono/compare/v4.8.9...v4.9.6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=hono&package-manager=npm_and_yarn&previous-version=4.8.9&new-version=4.9.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>
2025-09-08 16:49:55 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
50ce6badee chore: bump actions/setup-python from 5 to 6 (#6099)
Bumps [actions/setup-python](https://github.com/actions/setup-python)
from 5 to 6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/setup-python/releases">actions/setup-python's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.0</h2>
<h2>What's Changed</h2>
<h3>Breaking Changes</h3>
<ul>
<li>Upgrade to node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1164">actions/setup-python#1164</a></li>
</ul>
<p>Make sure your runner is on version v2.327.1 or later to ensure
compatibility with this release. <a
href="https://github.com/actions/runner/releases/tag/v2.327.1">See
Release Notes</a></p>
<h3>Enhancements:</h3>
<ul>
<li>Add support for <code>pip-version</code> by <a
href="https://github.com/priyagupta108"><code>@​priyagupta108</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1129">actions/setup-python#1129</a></li>
<li>Enhance reading from .python-version by <a
href="https://github.com/krystof-k"><code>@​krystof-k</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/787">actions/setup-python#787</a></li>
<li>Add version parsing from Pipfile by <a
href="https://github.com/aradkdj"><code>@​aradkdj</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1067">actions/setup-python#1067</a></li>
</ul>
<h3>Bug fixes:</h3>
<ul>
<li>Clarify pythonLocation behaviour for PyPy and GraalPy in environment
variables by <a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1183">actions/setup-python#1183</a></li>
<li>Change missing cache directory error to warning by <a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1182">actions/setup-python#1182</a></li>
<li>Add Architecture-Specific PATH Management for Python with --user
Flag on Windows by <a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1122">actions/setup-python#1122</a></li>
<li>Include python version in PyPy python-version output by <a
href="https://github.com/cdce8p"><code>@​cdce8p</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1110">actions/setup-python#1110</a></li>
<li>Update docs: clarification on pip authentication with setup-python
by <a
href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1156">actions/setup-python#1156</a></li>
</ul>
<h3>Dependency updates:</h3>
<ul>
<li>Upgrade idna from 2.9 to 3.7 in /<strong>tests</strong>/data by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-python/pull/843">actions/setup-python#843</a></li>
<li>Upgrade form-data to fix critical vulnerabilities <a
href="https://redirect.github.com/actions/setup-python/issues/182">#182</a>
&amp; <a
href="https://redirect.github.com/actions/setup-python/issues/183">#183</a>
by <a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1163">actions/setup-python#1163</a></li>
<li>Upgrade setuptools to 78.1.1 to fix path traversal vulnerability in
PackageIndex.download by <a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1165">actions/setup-python#1165</a></li>
<li>Upgrade actions/checkout from 4 to 5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-python/pull/1181">actions/setup-python#1181</a></li>
<li>Upgrade <code>@​actions/tool-cache</code> from 2.0.1 to 2.0.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-python/pull/1095">actions/setup-python#1095</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/krystof-k"><code>@​krystof-k</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-python/pull/787">actions/setup-python#787</a></li>
<li><a href="https://github.com/cdce8p"><code>@​cdce8p</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-python/pull/1110">actions/setup-python#1110</a></li>
<li><a href="https://github.com/aradkdj"><code>@​aradkdj</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/setup-python/pull/1067">actions/setup-python#1067</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-python/compare/v5...v6.0.0">https://github.com/actions/setup-python/compare/v5...v6.0.0</a></p>
<h2>v5.6.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Workflow updates related to Ubuntu 20.04 by <a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1065">actions/setup-python#1065</a></li>
<li>Fix for Candidate Not Iterable Error by <a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1082">actions/setup-python#1082</a></li>
<li>Upgrade semver and <code>@​types/semver</code> by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1091">actions/setup-python#1091</a></li>
<li>Upgrade prettier from 2.8.8 to 3.5.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1046">actions/setup-python#1046</a></li>
<li>Upgrade ts-jest from 29.1.2 to 29.3.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1081">actions/setup-python#1081</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-python/compare/v5...v5.6.0">https://github.com/actions/setup-python/compare/v5...v5.6.0</a></p>
<h2>v5.5.0</h2>
<h2>What's Changed</h2>
<h3>Enhancements:</h3>
<ul>
<li>Support free threaded Python versions like '3.13t' by <a
href="https://github.com/colesbury"><code>@​colesbury</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/973">actions/setup-python#973</a></li>
<li>Enhance Workflows: Include ubuntu-arm runners, Add e2e Testing for
free threaded and Upgrade <code>@​action/cache</code> from 4.0.0 to
4.0.3 by <a
href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1056">actions/setup-python#1056</a></li>
<li>Add support for .tool-versions file in setup-python by <a
href="https://github.com/mahabaleshwars"><code>@​mahabaleshwars</code></a>
in <a
href="https://redirect.github.com/actions/setup-python/pull/1043">actions/setup-python#1043</a></li>
</ul>
<h3>Bug fixes:</h3>
<ul>
<li>Fix architecture for pypy on Linux ARM64 by <a
href="https://github.com/mayeut"><code>@​mayeut</code></a> in <a
href="https://redirect.github.com/actions/setup-python/pull/1011">actions/setup-python#1011</a>
This update maps arm64 to aarch64 for Linux ARM64 PyPy
installations.</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/setup-python/commit/e797f83bcb11b83ae66e0230d6156d7c80228e7c"><code>e797f83</code></a>
Upgrade to node 24 (<a
href="https://redirect.github.com/actions/setup-python/issues/1164">#1164</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/3d1e2d2ca0a067f27da6fec484fce7f5256def85"><code>3d1e2d2</code></a>
Revert &quot;Enhance cache-dependency-path handling to support files
outside the w...</li>
<li><a
href="https://github.com/actions/setup-python/commit/65b071217a8539818fdb8b54561bcbae40380a54"><code>65b0712</code></a>
Clarify pythonLocation behavior for PyPy and GraalPy in environment
variables...</li>
<li><a
href="https://github.com/actions/setup-python/commit/5b668cf7652160527499ee14ceaff4be9306cb88"><code>5b668cf</code></a>
Bump actions/checkout from 4 to 5 (<a
href="https://redirect.github.com/actions/setup-python/issues/1181">#1181</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/f62a0e252fe7114e86949abfa6e1e89f85bb38c2"><code>f62a0e2</code></a>
Change missing cache directory error to warning (<a
href="https://redirect.github.com/actions/setup-python/issues/1182">#1182</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/9322b3ca74000aeb2c01eb777b646334015ddd72"><code>9322b3c</code></a>
Upgrade setuptools to 78.1.1 to fix path traversal vulnerability in
PackageIn...</li>
<li><a
href="https://github.com/actions/setup-python/commit/fbeb884f69f0ac1c0257302f62aa524c2824b649"><code>fbeb884</code></a>
Bump form-data to fix critical vulnerabilities <a
href="https://redirect.github.com/actions/setup-python/issues/182">#182</a>
&amp; <a
href="https://redirect.github.com/actions/setup-python/issues/183">#183</a>
(<a
href="https://redirect.github.com/actions/setup-python/issues/1163">#1163</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/03bb6152f4f691b9d64579a1bd791904a083c452"><code>03bb615</code></a>
Bump idna from 2.9 to 3.7 in /<strong>tests</strong>/data (<a
href="https://redirect.github.com/actions/setup-python/issues/843">#843</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/36da51d563b70a972897150555bb025096d65565"><code>36da51d</code></a>
Add version parsing from Pipfile (<a
href="https://redirect.github.com/actions/setup-python/issues/1067">#1067</a>)</li>
<li><a
href="https://github.com/actions/setup-python/commit/3c6f142cc0036d53007e92fa1e327564a4cfb7aa"><code>3c6f142</code></a>
update documentation (<a
href="https://redirect.github.com/actions/setup-python/issues/1156">#1156</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/setup-python/compare/v5...v6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 16:49:17 -04:00
Sydney RunkleandGitHub 6fc5b3aeda release(langgraph): 0.6.7 (#6092) 2025-09-07 13:49:43 +00:00
Harrison ChaseandGitHub b543752878 chore: update emphemeral local (#6091)
basically - for conditional edges, we use this to merge the updates from
state with the state object (before the actual update really occurs in
the tick.after)

otherwise - an emphemeral value will actually last through the logic in
the conditional edge of the node after
2025-09-07 09:42:11 -04:00
William FHandGitHub 8f6ad0b25a chore(sdk-py): Cleanup docstring indentation (#6087) 2025-09-05 22:56:03 +00:00
Isaac FranciscoandGitHub ada5d2ecb1 feat(cli): bump version (#6086)
version bump for: https://github.com/langchain-ai/langgraph/pull/6085
2025-09-05 15:52:30 -07:00
Isaac FranciscoandGitHub eaeafe54ab feat(cli): support prereleases (#6085)
We previously errored when a user had prerelease dependencies, this PR
passes the `--prereleases=allow` flag to our `uv pip install` call.

This PR also adds a test to verify that said deployments will build and
run as expected.
2025-09-05 15:45:48 -07:00
William FHandGitHub f761116de7 chore(sdk-py): Clean up docstring for get_client (#6084)
Main thing here is to call out the ASGITransport behavior
2025-09-05 13:43:31 -07:00
Nuno CamposandGitHub 36cf353d19 fix: Unwrap Required/NotRequired special forms before resolving channel/reducer annotations (#6080) 2025-09-05 10:27:10 +01:00
d503c0bf33 WIP: monorepo support in CLI (#6028)
This PR introduces the `--build-command` and `--install-command`
arguments to `langgraph build`.

`--install-command` is a custom install command. If passed, it will be
run from wherever the `langgraph build` call was made, i.e. NOT where
the langgraph.json file lives (except if these are the same place). This
will override the detected install command that we previously used.

`--build-command` is a custom build command. This will run from wherever
the langgraph.json file lives, and will be done after the install has
been run.

You don't need to provide both. Just providing one will make the install
(detected or supplied) run in the directory from where `langgraph build
was called` and then have the build command (if one exists) run in the
directory where langgraph.json exists.

I think we should probably allow configuring the directories from which
these commands get run, but I don't think this needs to be part of the
MVP.

---------

Co-authored-by: William FH <13333726+hinthornw@users.noreply.github.com>
2025-09-04 12:29:11 -07:00
William FHandGitHub 25ba4c3bda feat(sdk-py): Specify ttl on thread creation and update (#6075) 2025-09-03 18:48:57 -07:00
dfc1c59ebf chore(docs): Update OpenAPI spec from LangGraph API v0.4.11 (#6074)
This PR updates the OpenAPI specification with changes detected from the
LangGraph API server.

**Changes detected as of LangGraph API version 0.4.11**

This update was automatically generated by the sync workflow in the
langgraph-api repository.

Co-authored-by: hinthornw <hinthornw@users.noreply.github.com>
2025-09-03 18:37:39 -07:00
William FHandGitHub 6f4c5fefee feat(sdk-py): Support ids filtering in threads search (#6067) 2025-09-02 17:49:11 -07:00
7cf230defa chore(docs): Update OpenAPI spec from LangGraph API v0.4.9 (#6066)
This PR updates the OpenAPI specification with changes detected from the
LangGraph API server.

**Changes detected as of LangGraph API version 0.4.9**

This update was automatically generated by the sync workflow in the
langgraph-api repository.

Co-authored-by: hinthornw <hinthornw@users.noreply.github.com>
2025-09-02 17:04:30 -07:00
5db65e0281 chore(docs): Update OpenAPI spec from LangGraph API v0.4.8 (#6065)
This PR updates the OpenAPI specification with changes detected from the
LangGraph API server.

**Changes detected as of LangGraph API version 0.4.8**

This update was automatically generated by the sync workflow in the
langgraph-api repository.

Co-authored-by: hinthornw <hinthornw@users.noreply.github.com>
2025-09-02 15:25:52 -07:00
b08c2e092f chore(docs): Update OpenAPI spec from LangGraph API v0.4.8 (#6048)
This PR updates the OpenAPI specification with changes detected from the
LangGraph API server.

**Changes detected as of LangGraph API version 0.4.8**

This update was automatically generated by the sync workflow in the
langgraph-api repository.

Co-authored-by: hinthornw <hinthornw@users.noreply.github.com>
2025-09-02 10:10:12 -07:00
252 changed files with 14059 additions and 7852 deletions
+4 -1
View File
@@ -1,6 +1,9 @@
blank_issues_enabled: false
version: 2.1
contact_links:
- name: Documentation
url: https://github.com/langchain-ai/docs/issues/new?template=langgraph.yml
about: Report an issue related to the LangGraph documentation
- name: LangChain Forum
url: https://forum.langchain.com/
about: General community discussions, support, and feature requests
about: General community discussions and support
-19
View File
@@ -1,19 +0,0 @@
name: Documentation
description: Report an issue related to the LangGraph documentation.
title: "DOC: <Please write a comprehensive title after the 'DOC: ' prefix>"
labels: [documentation]
body:
- type: textarea
attributes:
label: "Issue with current documentation:"
description: >
Please make sure to leave a reference to the document/code you're
referring to.
- type: textarea
attributes:
label: "Idea or request for content:"
description: >
Please describe as clearly as possible what topics you think are missing
from the current documentation.
+65 -43
View File
@@ -14,12 +14,26 @@ jobs:
python-version:
- "3.10"
- "3.11"
- "3.14"
example:
- name: A
workdir: libs/cli/examples
tag: langgraph-test-a
- name: B
workdir: libs/cli/examples/graphs
tag: langgraph-test-b
- name: C
workdir: libs/cli/examples/graphs_reqs_a
tag: langgraph-test-c
- name: D
workdir: libs/cli/examples/graphs_reqs_b
tag: langgraph-test-d
name: "CLI integration test"
defaults:
run:
working-directory: libs/cli
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Get changed files
id: changed-files
uses: Ana06/get-changed-files@v2.3.0
@@ -27,63 +41,71 @@ jobs:
filter: "libs/cli/**"
- name: Set up Python ${{ matrix.python-version }}
if: steps.changed-files.outputs.all
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-suffix: "cli-integration-test"
ignore-nothing-to-cache: true
- name: Setup env
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples
run: cat .env.example > .env
- name: Install cli globally
if: steps.changed-files.outputs.all
run: pip install -e .
- name: Build and test service A
- name: Build and test service ${{ matrix.example.name }}
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples
working-directory: ${{ matrix.example.workdir }}
env:
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
run: |
# The build-arg isn't used; just testing that we accept other args
langgraph build -t langgraph-test-a
cp .env.example .env
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -c langgraph.json -t langgraph-test-a
- name: Build and test service B
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graphs
env:
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
run: |
langgraph build -t langgraph-test-b
cp ../.env.example .env
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-b
- name: Build and test service C
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graphs_reqs_a
env:
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
run: |
langgraph build -t langgraph-test-c
cp ../.env.example .env
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-c
- name: Build and test service D
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graphs_reqs_b
env:
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
run: |
langgraph build -t langgraph-test-d
cp ../.env.example .env
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-d
# Build the image for this example
langgraph build -t ${{ matrix.example.tag }}
# Prepare environment file from local or parent example directory
if [ -f .env.example ]; then cp .env.example .env; elif [ -f ../.env.example ]; then cp ../.env.example .env && cp ../.env.example ../.env; fi
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; if [ -f ../.env ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> ../.env; fi; fi
# Run the integration test using the built tag
# Compute repo root to reference the shared script robustly
REPO_ROOT=$(git rev-parse --show-toplevel)
timeout 60 python "$REPO_ROOT/.github/scripts/run_langgraph_cli_test.py" -t ${{ matrix.example.tag }}
- name: Build JS service
if: steps.changed-files.outputs.all
working-directory: libs/cli/js-examples
run: |
langgraph build -t langgraph-test-e
- name: Build JS monorepo service
if: steps.changed-files.outputs.all
working-directory: libs/cli/js-monorepo-example
run: |
langgraph build -t langgraph-test-f -c apps/agent/langgraph.json --build-command "yarn run turbo build" --install-command "yarn install"
- name: Build Python monorepo service
if: steps.changed-files.outputs.all
working-directory: libs/cli/python-monorepo-example
run: |
langgraph build -t langgraph-test-g -c apps/agent/langgraph.json
cp apps/agent/.env.example apps/agent/.env
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> apps/agent/.env; fi
timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-g -c apps/agent/langgraph.json
- name: Build and test prerelease reqs service
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graph_prerelease_reqs
run: |
langgraph build -t langgraph-test-h
cp ../.env.example .env
if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi
timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-h
LANGGRAPH_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h python -c "import sys; from importlib.metadata import version; v = version('langgraph'); print(v);")
if [ "$LANGGRAPH_VERSION" != "1.0.0a2" ]; then
exit 1
fi
LANGCHAIN_OPENAI_VERSION=$(docker run --rm --entrypoint "" langgraph-test-h python -c "import sys; from importlib.metadata import version; v = version('langchain-openai'); print(v);")
if [ "$LANGCHAIN_OPENAI_VERSION" != "0.3.0" ]; then
exit 1
fi
- name: Build and test prerelease reqs fail service
if: steps.changed-files.outputs.all
working-directory: libs/cli/examples/graph_prerelease_reqs_fail
run: |
langgraph build -t langgraph-test-i || [ $? -eq 1 ]
+4 -4
View File
@@ -31,7 +31,7 @@ jobs:
- "3.12"
name: "lint #${{ matrix.python-version }}"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Get changed files
id: changed-files
uses: Ana06/get-changed-files@v2.3.0
@@ -39,7 +39,7 @@ jobs:
filter: "${{ inputs.working-directory }}/**"
- name: Set up Python ${{ matrix.python-version }}
if: steps.changed-files.outputs.all
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
@@ -48,7 +48,7 @@ jobs:
- name: Install dependencies
if: steps.changed-files.outputs.all
working-directory: ${{ inputs.working-directory }}
run: uv sync --frozen --group dev
run: uv sync --frozen --group lint
- name: Get .mypy_cache to speed up mypy
if: steps.changed-files.outputs.all
@@ -74,7 +74,7 @@ jobs:
- name: Install test dependencies
if: steps.changed-files.outputs.all
working-directory: ${{ inputs.working-directory }}
run: uv sync --group dev
run: uv sync --group lint
- name: Get .mypy_cache_test to speed up mypy
if: steps.changed-files.outputs.all
+4 -4
View File
@@ -17,17 +17,17 @@ jobs:
strategy:
matrix:
python-version:
- "3.9"
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
name: "test #${{ matrix.python-version }}"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
@@ -42,7 +42,7 @@ jobs:
- name: Install dependencies
shell: bash
working-directory: ${{ inputs.working-directory }}
run: uv sync --frozen --group dev
run: uv sync --frozen --group test --no-dev
- name: Run tests
shell: bash
+4 -4
View File
@@ -12,20 +12,20 @@ jobs:
strategy:
matrix:
python-version:
- "3.9"
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
defaults:
run:
working-directory: libs/langgraph
name: "test #${{ matrix.python-version }}"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
@@ -39,7 +39,7 @@ jobs:
- name: Install dependencies
shell: bash
run: uv sync --frozen --group dev
run: uv sync --frozen --group test --no-dev
- name: Run tests
shell: bash
+4 -5
View File
@@ -16,7 +16,6 @@ permissions:
jobs:
build:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
outputs:
@@ -24,10 +23,10 @@ jobs:
version: ${{ steps.check-version.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up Python $${ env.PYTHON_VERSION }}
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: ${{ env.PYTHON_VERSION }}
enable-cache: true
@@ -75,9 +74,9 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
with:
name: test-dist
path: ${{ inputs.working-directory }}/dist/
+3 -3
View File
@@ -17,16 +17,16 @@ jobs:
run:
working-directory: libs/langgraph
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- run: SHA=$(git rev-parse HEAD) && echo "SHA=$SHA" >> $GITHUB_ENV
- name: Set up Python 3.11
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: "3.11"
enable-cache: true
cache-suffix: "bench"
- name: Install dependencies
run: uv sync --group dev
run: uv sync --group test
- name: Run benchmarks
run: OUTPUT=out/benchmark-baseline.json make -s benchmark
- name: Save outputs
+4 -4
View File
@@ -15,20 +15,20 @@ jobs:
run:
working-directory: libs/langgraph
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- id: files
name: Get changed files
uses: Ana06/get-changed-files@v2.3.0
with:
format: json
- name: Set up Python 3.11
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: "3.11"
enable-cache: true
cache-suffix: "bench"
- name: Install dependencies
run: uv sync --group dev
run: uv sync --group test
- name: Download baseline
uses: actions/cache/restore@v4
with:
@@ -57,7 +57,7 @@ jobs:
echo EOF
} >> "$GITHUB_OUTPUT"
- name: Annotation
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const file = JSON.parse(`${{ steps.files.outputs.added_modified_renamed }}`)[0]
+5 -5
View File
@@ -27,7 +27,7 @@ jobs:
python: ${{ steps.filter.outputs.python }}
deps: ${{ steps.filter.outputs.deps }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: dorny/paths-filter@v3
id: filter
with:
@@ -100,9 +100,9 @@ jobs:
name: "Check SDK methods matching"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.11"
- name: Run check_sdk_methods script
@@ -118,9 +118,9 @@ jobs:
python-version:
- "3.11"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: "3.11"
enable-cache: true
+1 -1
View File
@@ -21,7 +21,7 @@
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install Dependencies
run: |
+4 -4
View File
@@ -28,7 +28,7 @@ jobs:
outputs:
changed-files: ${{ steps.changed-files.outputs.added_modified }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Get changed files
id: changed-files
uses: Ana06/get-changed-files@v2.3.0
@@ -41,12 +41,12 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.MKDOCS_GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Python
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: "3.12"
enable-cache: true
@@ -140,7 +140,7 @@ jobs:
- name: Upload Pages Artifact
# if: github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@v4
with:
path: ./docs/site/
+2 -2
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
@@ -36,7 +36,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 1
+2 -1
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Validate PR Title
uses: amannn/action-semantic-pull-request@v5
uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -40,6 +40,7 @@ jobs:
sdk-py
docs
ci
deps
requireScope: false
ignoreLabels: |
ignore-lint-pr-title
+12 -13
View File
@@ -16,7 +16,6 @@ env:
jobs:
build:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
outputs:
@@ -26,10 +25,10 @@ jobs:
tag: ${{ steps.check-version.outputs.tag }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up Python
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: ${{ env.PYTHON_VERSION }}
enable-cache: true
@@ -87,7 +86,7 @@ jobs:
outputs:
release-body: ${{ steps.generate-release-body.outputs.release-body }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
repository: langchain-ai/langgraph
path: langgraph
@@ -158,7 +157,7 @@ jobs:
- test-pypi-publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
# We explicitly *don't* set up caching here. This ensures our tests are
# maximally sensitive to catching breakage.
@@ -174,7 +173,7 @@ jobs:
# used in the real world.
- name: Set up Python
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: ${{ env.PYTHON_VERSION }}
enable-cache: true
@@ -222,7 +221,7 @@ jobs:
uv run python -c "import $IMPORT_NAME; print(dir($IMPORT_NAME))"
- name: Import test dependencies
run: uv sync --group dev
run: uv sync --group test
working-directory: ${{ inputs.working-directory }}
# Overwrite the local version of the package with the test PyPI version.
@@ -261,16 +260,16 @@ jobs:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up Python
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: ${{ env.PYTHON_VERSION }}
enable-cache: true
cache-suffix: "release"
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
with:
name: dist
path: ${{ inputs.working-directory }}/dist/
@@ -302,16 +301,16 @@ jobs:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up Python
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: ${{ env.PYTHON_VERSION }}
enable-cache: true
cache-suffix: "release"
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
with:
name: dist
path: ${{ inputs.working-directory }}/dist/
+2 -2
View File
@@ -28,9 +28,9 @@ jobs:
- "latest"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up Python + Poetry
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: "3.11"
enable-cache: true
+5 -5
View File
@@ -16,13 +16,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
# use minimum supported Python version
python-version: "3.9"
python-version: "3.10"
enable-cache: true
cache-suffix: "uv-lock-upgrade"
@@ -33,8 +33,8 @@ jobs:
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore[deps]: upgrade dependencies with `uv lock --upgrade`"
title: "chore[deps]: upgrade dependencies with `uv lock --upgrade`"
commit-message: "chore(deps): upgrade dependencies with `uv lock --upgrade`"
title: "chore(deps): upgrade dependencies with `uv lock --upgrade`"
body: |
This PR updates the dependencies in all Python packages using `uv lock --upgrade`.
+1 -1
View File
@@ -28,7 +28,7 @@ Below is a high-level overview:
- **langgraph** core framework for building stateful, multi-actor agents.
- **prebuilt** high-level APIs for creating and running agents and tools.
- **sdk-js** JS/TS SDK for interacting with the LangGraph REST API.
- **sdk-py** Python SDK for the LangGraph Platform API.
- **sdk-py** Python SDK for the LangGraph Server API.
### Dependency map
+3 -3
View File
@@ -277,9 +277,9 @@ def my_function(arg1: int, arg2: str) -> float:
Examples:
This is a section for examples of how to use the function.
.. code-block:: python
my_function(1, "hello")
```python
my_function(1, "hello")
\```
Args:
arg1: This is a description of arg1. We do not need to specify the type since
+2 -2
View File
@@ -63,7 +63,7 @@ LangGraph provides low-level supporting infrastructure for *any* long-running, s
While LangGraph can be used standalone, it also integrates seamlessly with any LangChain product, giving developers a full suite of tools for building agents. To improve your LLM application development, pair LangGraph with:
- [LangSmith](http://www.langchain.com/langsmith) — Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
- [LangGraph Platform](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
- [LangSmith Deployment](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) — Deploy and scale agents effortlessly with a purpose-built deployment platform for long running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangGraph Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/).
- [LangChain](https://python.langchain.com/docs/introduction/) Provides integrations and composable components to streamline LLM application development.
> [!NOTE]
@@ -71,7 +71,7 @@ While LangGraph can be used standalone, it also integrates seamlessly with any L
## Additional resources
- [Guides](https://langchain-ai.github.io/langgraph/how-tos/): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.).
- [Guides](https://langchain-ai.github.io/langgraph/guides/): Quick, actionable code snippets for topics such as streaming, adding memory & persistence, and design patterns (e.g. branching, subgraphs, etc.).
- [Reference](https://langchain-ai.github.io/langgraph/reference/graphs/): Detailed reference on core classes, methods, how to use the graph and checkpointing APIs, and higher-level prebuilt components.
- [Examples](https://langchain-ai.github.io/langgraph/examples/): Guided examples on getting started with LangGraph.
- [LangChain Forum](https://forum.langchain.com/): Connect with the community and share all of your technical questions, ideas, and feedback.
+113 -10
View File
@@ -1,24 +1,126 @@
# Setup
# LangGraph Documentation
To setup requirements for building docs you can run:
For more information on contributing to our documentation, see the [Contributing Guide](../CONTRIBUTING.md).
```bash
uv sync --group test
## Structure
The primary documentation is located in the `docs/` directory. This directory contains both the source files for the main documentation as well as the API reference doc build process.
### Main Documentation
Main documentation files are located in `docs/docs/` and are written in Markdown format. The site uses [**MkDocs**](https://www.mkdocs.org/) with the [Material theme](https://squidfunk.github.io/mkdocs-material/) and includes:
- **Concepts**: Core LangGraph concepts and explanations
- **Tutorials**: Step-by-step learning guides
- **How-tos**: Task-focused guides for specific use cases
- **Examples**: Real-world applications and use cases
- **Jupyter Notebooks**: Interactive tutorials that are automatically converted to markdown
### API Reference
API reference documentation is defined in `docs/docs/reference/`. Each `.md` file outlines the "template" that each page is built from. Reference content is automatically generated from docstrings in the codebase using the **mkdocstrings** plugin. Once generated, the content is plugged into the corresponding markdown file where it is referenced by using manual directives to specify which classes and/or functions are documented:
```markdown
::: langgraph.graph.state.StateGraph
options:
show_if_no_docstring: true
show_root_heading: true
show_root_full_path: false
members:
- add_node
- add_edge
- add_conditional_edges
- add_sequence
- compile
```
## Serving documentation locally
## Build Process
To run the documentation server locally you can run:
Docs are built following these steps:
1. **Content Processing:**
- `_scripts/notebook_hooks.py` - Main processing pipeline that:
- Converts how-tos/tutorial Jupyter notebooks to markdown using `notebook_convert.py`
- Adds automatic API reference links to code blocks using `generate_api_reference_links.py`
- Handles conditional rendering for Python/JS versions
- Processes highlight comments and custom syntax
2. **API Reference Generation:**
- **mkdocstrings** plugin extracts docstrings from Python source code
- Manual `::: module.Class` directives in reference pages (`/docs/docs/*`) specify what to document
- Cross-references are automatically generated between docs and API
3. **Site Generation:**
- **MkDocs** processes all markdown files and generates static HTML
- Custom hooks handle redirects and inject additional functionality
4. **Deployment:**
- Site is deployed with Vercel
- `make build-docs` generates production build (also usable for local testing)
- Automatic redirects handle URL changes between versions
### Local Development
For local development, use the Makefile targets:
```bash
# Serve docs locally with hot reloading
make serve-docs
# Clean build for production testing
make build-docs
# Serve with clean build
make serve-clean-docs
```
This will start the documentation server on [http://127.0.0.1:8000/langgraph/](http://127.0.0.1:8000/langgraph/).
The `serve-docs` command:
- Watches source files for changes
- Includes dirty builds for faster iteration
- Serves on [http://127.0.0.1:8000/langgraph/](http://127.0.0.1:8000/langgraph/)
## Standards
**Docstring Format:**
The API reference uses **Google-style docstrings** with Markdown markup. The `mkdocstrings` plugin processes these to generate documentation.
**Required format:**
```python
def example_function(param1: str, param2: int = 5) -> bool:
"""Brief description of the function.
Longer description can go here. Use Markdown syntax for
rich formatting like **bold** and *italic*.
Args:
param1: Description of the first parameter.
param2: Description of the second parameter with default value.
Returns:
Description of the return value.
Raises:
ValueError: When param1 is empty.
TypeError: When param2 is not an integer.
!!! warning
This function is experimental and may change.
!!! version-added "Added in version 0.2.0"
"""
```
**Special Markers:**
- **MkDocs admonitions**: `!!! warning`, `!!! note`, `!!! version-added`
- **Code blocks**: Standard markdown ``` syntax
- **Cross-references**: Automatic linking via `generate_api_reference_links.py`
## Execute notebooks
If you would like to automatically execute all of the notebooks, to mimic the "Run notebooks" GHA, you can run:
If you would like to automatically execute all of the notebooks, to mimic the "Run notebooks" GitHub action, you can run:
```bash
python _scripts/prepare_notebooks_for_ci.py
@@ -33,8 +135,9 @@ python _scripts/prepare_notebooks_for_ci.py --comment-install-cells
```
`prepare_notebooks_for_ci.py` script will add VCR cassette context manager for each cell in the notebook, so that:
* when the notebook is run for the first time, cells with network requests will be recorded to a VCR cassette file
* when the notebook is run subsequently, the cells with network requests will be replayed from the cassettes
- when the notebook is run for the first time, cells with network requests will be recorded to a VCR cassette file
- when the notebook is run subsequently, the cells with network requests will be replayed from the cassettes
## Adding new notebooks
+14 -2
View File
@@ -1,3 +1,5 @@
"""Generate API reference links for imports in Python code blocks within markdown files."""
import ast
import importlib
import logging
@@ -70,8 +72,18 @@ MANUAL_API_REFERENCES_LANGGRAPH = [
([], "langgraph.checkpoint.postgres.aio", "AsyncPostgresSaver", "checkpoints"),
([], "langgraph.checkpoint.postgres", "PostgresSaver", "checkpoints"),
# other prebuilts
(["langgraph_supervisor"], "langgraph_supervisor.supervisor", "create_supervisor", "supervisor"),
(["langgraph_supervisor"], "langgraph_supervisor.handoff", "create_handoff_tool", "supervisor"),
(
["langgraph_supervisor"],
"langgraph_supervisor.supervisor",
"create_supervisor",
"supervisor",
),
(
["langgraph_supervisor"],
"langgraph_supervisor.handoff",
"create_handoff_tool",
"supervisor",
),
([], "langgraph_supervisor.handoff", "create_forward_message_tool", "supervisor"),
(["langgraph_swarm"], "langgraph_swarm.swarm", "create_swarm", "swarm"),
(["langgraph_swarm"], "langgraph_swarm.swarm", "add_active_agent_router", "swarm"),
+10 -4
View File
@@ -29,7 +29,11 @@ logger = logging.getLogger(__name__)
def _transform_link(
link_name: str, scope: str, file_path: str, line_number: int, custom_title: Optional[str] = None
link_name: str,
scope: str,
file_path: str,
line_number: int,
custom_title: Optional[str] = None,
) -> Optional[str]:
"""Transform a cross-reference link based on the current scope.
@@ -38,7 +42,7 @@ def _transform_link(
scope: The current scope context ("global", "python", "js", etc.).
file_path: The file path for error reporting.
line_number: The line number for error reporting.
custom_title: Optional custom title for the link. If None, uses link_name.
custom_title: Optional custom title for the link. If `None`, uses link_name.
Returns:
A formatted markdown link if the link is found in the scope mapping,
@@ -117,7 +121,9 @@ CROSS_REFERENCE_PATTERN = re.compile(
)
def _replace_autolinks(markdown: str, file_path: str, *, default_scope: str = "python") -> str:
def _replace_autolinks(
markdown: str, file_path: str, *, default_scope: str = "python"
) -> str:
"""Preprocess markdown lines to handle @[links] with conditional fence scopes.
This function processes markdown content to transform @[link_name] references
@@ -169,7 +175,7 @@ def _replace_autolinks(markdown: str, file_path: str, *, default_scope: str = "p
# This is @[ref] format
link_name = match.group("link_name")
custom_title = None
transformed = _transform_link(
link_name, current_scope, file_path, line_number, custom_title
)
@@ -2108,9 +2108,9 @@ __metadata:
linkType: hard
"hono@npm:^4.5.4":
version: 4.8.9
resolution: "hono@npm:4.8.9"
checksum: 10c0/385539d1787fdc747bc869ef0e5ccc9f39cbe40289b94f23eecfc82c6ca440f059704647cd6381a5066d2cf7baa43ab25184c78d44af4c5c98a5c5b07670059e
version: 4.9.7
resolution: "hono@npm:4.9.7"
checksum: 10c0/089184660a9211ea216ab95bafa45260e371651cb019db49828064b7982b0ae61cc3c4715324bfeb9037aa2460c39ffa2c91d84ad0c8d500fa77cbcc7fc07a8f
languageName: node
linkType: hard
+2
View File
@@ -1,3 +1,5 @@
"""Convert Jupyter notebooks to markdown with custom processing."""
import ast
import os
import re
+83 -83
View File
@@ -88,12 +88,12 @@ REDIRECT_MAP = {
"cloud/how-tos/human_in_the_loop_user_input.md": "cloud/how-tos/add-human-in-the-loop.md",
"concepts/platform_architecture.md": "concepts/langgraph_cloud#architecture",
# cloud streaming redirects
"cloud/how-tos/stream_values.md": "https://docs.langchain.com/langgraph-platform/streaming",
"cloud/how-tos/stream_updates.md": "https://docs.langchain.com/langgraph-platform/streaming",
"cloud/how-tos/stream_messages.md": "https://docs.langchain.com/langgraph-platform/streaming",
"cloud/how-tos/stream_events.md": "https://docs.langchain.com/langgraph-platform/streaming",
"cloud/how-tos/stream_debug.md": "https://docs.langchain.com/langgraph-platform/streaming",
"cloud/how-tos/stream_multiple.md": "https://docs.langchain.com/langgraph-platform/streaming",
"cloud/how-tos/stream_values.md": "https://docs.langchain.com/langsmith/streaming",
"cloud/how-tos/stream_updates.md": "https://docs.langchain.com/langsmith/streaming",
"cloud/how-tos/stream_messages.md": "https://docs.langchain.com/langsmith/streaming",
"cloud/how-tos/stream_events.md": "https://docs.langchain.com/langsmith/streaming",
"cloud/how-tos/stream_debug.md": "https://docs.langchain.com/langsmith/streaming",
"cloud/how-tos/stream_multiple.md": "https://docs.langchain.com/langsmith/streaming",
"cloud/concepts/streaming.md": "concepts/streaming.md",
"agents/streaming.md": "how-tos/streaming.md",
# prebuilt redirects
@@ -129,83 +129,83 @@ REDIRECT_MAP = {
"how-tos/human_in_the_loop/edit-graph-state.ipynb": "how-tos/human_in_the_loop/time-travel.md",
# LGP mintlify migration redirects
"tutorials/auth/getting_started.md": "https://docs.langchain.com/langgraph-platform/auth",
"tutorials/auth/resource_auth.md": "https://docs.langchain.com/langgraph-platform/resource-auth",
"tutorials/auth/add_auth_server.md": "https://docs.langchain.com/langgraph-platform/add-auth-server",
"how-tos/use-remote-graph.md": "https://docs.langchain.com/langgraph-platform/use-remote-graph",
"how-tos/autogen-integration.md": "https://docs.langchain.com/langgraph-platform/autogen-integration",
"cloud/how-tos/use_stream_react.md": "https://docs.langchain.com/langgraph-platform/use-stream-react",
"cloud/how-tos/generative_ui_react.md": "https://docs.langchain.com/langgraph-platform/generative-ui-react",
"concepts/langgraph_platform.md": "https://docs.langchain.com/langgraph-platform/index",
"concepts/langgraph_components.md": "https://docs.langchain.com/langgraph-platform/components",
"concepts/langgraph_server.md": "https://docs.langchain.com/langgraph-platform/langgraph-server",
"concepts/langgraph_data_plane.md": "https://docs.langchain.com/langgraph-platform/data-plane",
"concepts/langgraph_control_plane.md": "https://docs.langchain.com/langgraph-platform/control-plane",
"concepts/langgraph_cli.md": "https://docs.langchain.com/langgraph-platform/langgraph-cli",
"concepts/langgraph_studio.md": "https://docs.langchain.com/langgraph-platform/langgraph-studio",
"cloud/how-tos/studio/quick_start.md": "https://docs.langchain.com/langgraph-platform/quick-start-studio",
"cloud/how-tos/invoke_studio.md": "https://docs.langchain.com/langgraph-platform/invoke-studio",
"cloud/how-tos/studio/manage_assistants.md": "https://docs.langchain.com/langgraph-platform/manage-assistants-studio",
"cloud/how-tos/threads_studio.md": "https://docs.langchain.com/langgraph-platform/threads-studio",
"cloud/how-tos/iterate_graph_studio.md": "https://docs.langchain.com/langgraph-platform/iterate-graph-studio",
"cloud/how-tos/studio/run_evals.md": "https://docs.langchain.com/langgraph-platform/run-evals-studio",
"cloud/how-tos/clone_traces_studio.md": "https://docs.langchain.com/langgraph-platform/clone-traces-studio",
"cloud/how-tos/datasets_studio.md": "https://docs.langchain.com/langgraph-platform/datasets-studio",
"concepts/sdk.md": "https://docs.langchain.com/langgraph-platform/sdk",
"concepts/plans.md": "https://docs.langchain.com/langgraph-platform/plans",
"concepts/application_structure.md": "https://docs.langchain.com/langgraph-platform/application-structure",
"concepts/scalability_and_resilience.md": "https://docs.langchain.com/langgraph-platform/scalability-and-resilience",
"concepts/auth.md": "https://docs.langchain.com/langgraph-platform/auth",
"how-tos/auth/custom_auth.md": "https://docs.langchain.com/langgraph-platform/custom-auth",
"how-tos/auth/openapi_security.md": "https://docs.langchain.com/langgraph-platform/openapi-security",
"concepts/assistants.md": "https://docs.langchain.com/langgraph-platform/assistants",
"cloud/how-tos/configuration_cloud.md": "https://docs.langchain.com/langgraph-platform/configuration-cloud",
"cloud/how-tos/use_threads.md": "https://docs.langchain.com/langgraph-platform/use-threads",
"cloud/how-tos/background_run.md": "https://docs.langchain.com/langgraph-platform/background-run",
"cloud/how-tos/same-thread.md": "https://docs.langchain.com/langgraph-platform/same-thread",
"cloud/how-tos/stateless_runs.md": "https://docs.langchain.com/langgraph-platform/stateless-runs",
"cloud/how-tos/configurable_headers.md": "https://docs.langchain.com/langgraph-platform/configurable-headers",
"concepts/double_texting.md": "https://docs.langchain.com/langgraph-platform/double-texting",
"cloud/how-tos/interrupt_concurrent.md": "https://docs.langchain.com/langgraph-platform/interrupt-concurrent",
"cloud/how-tos/rollback_concurrent.md": "https://docs.langchain.com/langgraph-platform/rollback-concurrent",
"cloud/how-tos/reject_concurrent.md": "https://docs.langchain.com/langgraph-platform/reject-concurrent",
"cloud/how-tos/enqueue_concurrent.md": "https://docs.langchain.com/langgraph-platform/enqueue-concurrent",
"cloud/concepts/webhooks.md": "https://docs.langchain.com/langgraph-platform/use-webhooks",
"cloud/how-tos/webhooks.md": "https://docs.langchain.com/langgraph-platform/use-webhooks",
"cloud/concepts/cron_jobs.md": "https://docs.langchain.com/langgraph-platform/cron-jobs",
"cloud/how-tos/cron_jobs.md": "https://docs.langchain.com/langgraph-platform/cron-jobs",
"how-tos/http/custom_lifespan.md": "https://docs.langchain.com/langgraph-platform/custom-lifespan",
"how-tos/http/custom_middleware.md": "https://docs.langchain.com/langgraph-platform/custom-middleware",
"how-tos/http/custom_routes.md": "https://docs.langchain.com/langgraph-platform/custom-routes",
"cloud/concepts/data_storage_and_privacy.md": "https://docs.langchain.com/langgraph-platform/data-storage-and-privacy",
"cloud/deployment/semantic_search.md": "https://docs.langchain.com/langgraph-platform/semantic-search",
"how-tos/ttl/configure_ttl.md": "https://docs.langchain.com/langgraph-platform/configure-ttl",
"concepts/deployment_options.md": "https://docs.langchain.com/langgraph-platform/deployment-options",
"cloud/quick_start.md": "https://docs.langchain.com/langgraph-platform/deployment-quickstart",
"cloud/deployment/setup.md": "https://docs.langchain.com/langgraph-platform/setup-app-requirements-txt",
"cloud/deployment/setup_pyproject.md": "https://docs.langchain.com/langgraph-platform/setup-pyproject",
"cloud/deployment/setup_javascript.md": "https://docs.langchain.com/langgraph-platform/setup-javascript",
"cloud/deployment/custom_docker.md": "https://docs.langchain.com/langgraph-platform/custom-docker",
"cloud/deployment/graph_rebuild.md": "https://docs.langchain.com/langgraph-platform/graph-rebuild",
"concepts/langgraph_cloud.md": "https://docs.langchain.com/langgraph-platform/cloud",
"concepts/langgraph_self_hosted_data_plane.md": "https://docs.langchain.com/langgraph-platform/hybrid",
"concepts/langgraph_self_hosted_control_plane.md": "https://docs.langchain.com/langgraph-platform/self-hosted",
"concepts/langgraph_standalone_container.md": "https://docs.langchain.com/langgraph-platform/self-hosted#standalone-server",
"cloud/deployment/cloud.md": "https://docs.langchain.com/langgraph-platform/cloud",
"cloud/deployment/self_hosted_data_plane.md": "https://docs.langchain.com/langgraph-platform/deploy-hybrid",
"cloud/deployment/self_hosted_control_plane.md": "https://docs.langchain.com/langgraph-platform/deploy-self-hosted-full-platform",
"cloud/deployment/standalone_container.md": "https://docs.langchain.com/langgraph-platform/deploy-standalone-server",
"concepts/server-mcp.md": "https://docs.langchain.com/langgraph-platform/server-mcp",
"cloud/how-tos/human_in_the_loop_time_travel.md": "https://docs.langchain.com/langgraph-platform/human-in-the-loop-time-travel",
"cloud/how-tos/add-human-in-the-loop.md": "https://docs.langchain.com/langgraph-platform/add-human-in-the-loop",
"cloud/deployment/egress.md": "https://docs.langchain.com/langgraph-platform/env-var",
"cloud/how-tos/streaming.md": "https://docs.langchain.com/langgraph-platform/streaming",
"cloud/reference/api/api_ref.md": "https://docs.langchain.com/langgraph-platform/server-api-ref",
"cloud/reference/langgraph_server_changelog.md": "https://docs.langchain.com/langgraph-platform/langgraph-server-changelog",
"cloud/reference/api/api_ref_control_plane.md": "https://docs.langchain.com/langgraph-platform/api-ref-control-plane",
"cloud/reference/cli.md": "https://docs.langchain.com/langgraph-platform/cli",
"cloud/reference/env_var.md": "https://docs.langchain.com/langgraph-platform/env-var",
"troubleshooting/studio.md": "https://docs.langchain.com/langgraph-platform/troubleshooting-studio",
"tutorials/auth/getting_started.md": "https://docs.langchain.com/langsmith/auth",
"tutorials/auth/resource_auth.md": "https://docs.langchain.com/langsmith/resource-auth",
"tutorials/auth/add_auth_server.md": "https://docs.langchain.com/langsmith/add-auth-server",
"how-tos/use-remote-graph.md": "https://docs.langchain.com/langsmith/use-remote-graph",
"how-tos/autogen-integration.md": "https://docs.langchain.com/langsmith/autogen-integration",
"cloud/how-tos/use_stream_react.md": "https://docs.langchain.com/langsmith/use-stream-react",
"cloud/how-tos/generative_ui_react.md": "https://docs.langchain.com/langsmith/generative-ui-react",
"concepts/langgraph_platform.md": "https://docs.langchain.com/langsmith/deployments",
"concepts/langgraph_components.md": "https://docs.langchain.com/langsmith/components",
"concepts/langgraph_server.md": "https://docs.langchain.com/langsmith/langgraph-server",
"concepts/langgraph_data_plane.md": "https://docs.langchain.com/langsmith/data-plane",
"concepts/langgraph_control_plane.md": "https://docs.langchain.com/langsmith/control-plane",
"concepts/langgraph_cli.md": "https://docs.langchain.com/langsmith/langgraph-cli",
"concepts/langgraph_studio.md": "https://docs.langchain.com/langsmith/studio",
"cloud/how-tos/studio/quick_start.md": "https://docs.langchain.com/langsmith/quick-start-studio",
"cloud/how-tos/invoke_studio.md": "https://docs.langchain.com/langsmith/use-studio#run-application",
"cloud/how-tos/studio/manage_assistants.md": "https://docs.langchain.com/langsmith/use-studio#manage-assistants",
"cloud/how-tos/threads_studio.md": "https://docs.langchain.com/langsmith/use-studio#manage-threads",
"cloud/how-tos/iterate_graph_studio.md": "https://docs.langchain.com/langsmith/observability-studio#iterate-on-prompts",
"cloud/how-tos/studio/run_evals.md": "https://docs.langchain.com/langsmith/observability-studio#run-experiments-over-a-dataset",
"cloud/how-tos/clone_traces_studio.md": "https://docs.langchain.com/langsmith/observability-studio#debug-langsmith-traces",
"cloud/how-tos/datasets_studio.md": "https://docs.langchain.com/langsmith/observability-studio#add-node-to-dataset",
"concepts/sdk.md": "https://docs.langchain.com/langsmith/sdk",
"concepts/plans.md": "https://langchain.com/pricing",
"concepts/application_structure.md": "https://docs.langchain.com/langsmith/application-structure",
"concepts/scalability_and_resilience.md": "https://docs.langchain.com/langsmith/scalability-and-resilience",
"concepts/auth.md": "https://docs.langchain.com/langsmith/authentication-methods",
"how-tos/auth/custom_auth.md": "https://docs.langchain.com/langsmith/custom-auth",
"how-tos/auth/openapi_security.md": "https://docs.langchain.com/langsmith/openapi-security",
"concepts/assistants.md": "https://docs.langchain.com/langsmith/assistants",
"cloud/how-tos/configuration_cloud.md": "https://docs.langchain.com/langsmith/cloud",
"cloud/how-tos/use_threads.md": "https://docs.langchain.com/langsmith/use-threads",
"cloud/how-tos/background_run.md": "https://docs.langchain.com/langsmith/background-run",
"cloud/how-tos/same-thread.md": "https://docs.langchain.com/langsmith/same-thread",
"cloud/how-tos/stateless_runs.md": "https://docs.langchain.com/langsmith/stateless-runs",
"cloud/how-tos/configurable_headers.md": "https://docs.langchain.com/langsmith/configurable-headers",
"concepts/double_texting.md": "https://docs.langchain.com/langsmith/double-texting",
"cloud/how-tos/interrupt_concurrent.md": "https://docs.langchain.com/langsmith/interrupt-concurrent",
"cloud/how-tos/rollback_concurrent.md": "https://docs.langchain.com/langsmith/rollback-concurrent",
"cloud/how-tos/reject_concurrent.md": "https://docs.langchain.com/langsmith/reject-concurrent",
"cloud/how-tos/enqueue_concurrent.md": "https://docs.langchain.com/langsmith/enqueue-concurrent",
"cloud/concepts/webhooks.md": "https://docs.langchain.com/langsmith/use-webhooks",
"cloud/how-tos/webhooks.md": "https://docs.langchain.com/langsmith/use-webhooks",
"cloud/concepts/cron_jobs.md": "https://docs.langchain.com/langsmith/cron-jobs",
"cloud/how-tos/cron_jobs.md": "https://docs.langchain.com/langsmith/cron-jobs",
"how-tos/http/custom_lifespan.md": "https://docs.langchain.com/langsmith/custom-lifespan",
"how-tos/http/custom_middleware.md": "https://docs.langchain.com/langsmith/custom-middleware",
"how-tos/http/custom_routes.md": "https://docs.langchain.com/langsmith/custom-routes",
"cloud/concepts/data_storage_and_privacy.md": "https://docs.langchain.com/langsmith/data-storage-and-privacy",
"cloud/deployment/semantic_search.md": "https://docs.langchain.com/langsmith/semantic-search",
"how-tos/ttl/configure_ttl.md": "https://docs.langchain.com/langsmith/configure-ttl",
"concepts/deployment_options.md": "https://docs.langchain.com/langsmith/hosting",
"cloud/quick_start.md": "https://docs.langchain.com/langsmith/deployment-quickstart",
"cloud/deployment/setup.md": "https://docs.langchain.com/langsmith/setup-app-requirements-txt",
"cloud/deployment/setup_pyproject.md": "https://docs.langchain.com/langsmith/setup-pyproject",
"cloud/deployment/setup_javascript.md": "https://docs.langchain.com/langsmith/setup-javascript",
"cloud/deployment/custom_docker.md": "https://docs.langchain.com/langsmith/custom-docker",
"cloud/deployment/graph_rebuild.md": "https://docs.langchain.com/langsmith/graph-rebuild",
"concepts/langgraph_cloud.md": "https://docs.langchain.com/langsmith/cloud",
"concepts/langgraph_self_hosted_data_plane.md": "https://docs.langchain.com/langsmith/hybrid",
"concepts/langgraph_self_hosted_control_plane.md": "https://docs.langchain.com/langsmith/self-hosted",
"concepts/langgraph_standalone_container.md": "https://docs.langchain.com/langsmith/self-hosted#standalone-server",
"cloud/deployment/cloud.md": "https://docs.langchain.com/langsmith/cloud",
"cloud/deployment/self_hosted_data_plane.md": "https://docs.langchain.com/langsmith/deploy-hybrid",
"cloud/deployment/self_hosted_control_plane.md": "https://docs.langchain.com/langsmith/deploy-self-hosted-full-platform",
"cloud/deployment/standalone_container.md": "https://docs.langchain.com/langsmith/deploy-standalone-server",
"concepts/server-mcp.md": "https://docs.langchain.com/langsmith/server-mcp",
"cloud/how-tos/human_in_the_loop_time_travel.md": "https://docs.langchain.com/langsmith/human-in-the-loop-time-travel",
"cloud/how-tos/add-human-in-the-loop.md": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
"cloud/deployment/egress.md": "https://docs.langchain.com/langsmith/env-var",
"cloud/how-tos/streaming.md": "https://docs.langchain.com/langsmith/streaming",
"cloud/reference/api/api_ref.md": "https://docs.langchain.com/langsmith/server-api-ref",
"cloud/reference/langgraph_server_changelog.md": "https://docs.langchain.com/langsmith/langgraph-server-changelog",
"cloud/reference/api/api_ref_control_plane.md": "https://docs.langchain.com/langsmith/api-ref-control-plane",
"cloud/reference/cli.md": "https://docs.langchain.com/langsmith/cli",
"cloud/reference/env_var.md": "https://docs.langchain.com/langsmith/env-var",
"troubleshooting/studio.md": "https://docs.langchain.com/langsmith/troubleshooting-studio",
}
@@ -20,16 +20,19 @@ class Package(TypedDict):
description: str
"""A brief description of what the package does."""
class ResolvedPackage(Package):
weekly_downloads: int | None
"""The weekly download count of the package."""
language: str
"""The language of the package. (either 'python' or 'js')"""
HERE = pathlib.Path(__file__).parent
PACKAGES_FILE = HERE / "packages.yml"
PACKAGES = yaml.safe_load(PACKAGES_FILE.read_text())["packages"]
def _get_pypi_downloads(package: Package) -> int:
"""Retrieve the weekly download count for a package from PyPIStats."""
@@ -72,7 +75,8 @@ def _get_pypi_downloads(package: Package) -> int:
return sum(entry["downloads"] for entry in sorted_data[:7])
else:
return None
def _get_npm_downloads(package: Package) -> int:
"""Retrieve the weekly download count for a package on the npm registry."""
@@ -82,14 +86,18 @@ def _get_npm_downloads(package: Package) -> int:
npm_response = requests.get(npm_url)
npm_response.raise_for_status()
except requests.exceptions.HTTPError:
raise AssertionError(f"Package {package['name']} does not exist on npm registry")
raise AssertionError(
f"Package {package['name']} does not exist on npm registry"
)
npm_data = npm_response.json()
# Retrieve the first publish date using the 'created' timestamp from the 'time' field.
created_str = npm_data.get("time", {}).get("created")
if created_str is None:
raise AssertionError(f"Package {package['name']} has no creation time in registry data")
raise AssertionError(
f"Package {package['name']} has no creation time in registry data"
)
# Remove the trailing 'Z' if present and parse the ISO format timestamp
first_publish_date = datetime.fromisoformat(created_str.rstrip("Z"))
@@ -103,7 +111,10 @@ def _get_npm_downloads(package: Package) -> int:
else:
return None
def _get_weekly_downloads(packages: dict[str, list[Package]], fake: bool) -> list[ResolvedPackage]:
def _get_weekly_downloads(
packages: dict[str, list[Package]], fake: bool
) -> list[ResolvedPackage]:
"""Retrieve the weekly download count for a dictionary of python or js packages."""
resolved_packages: list[ResolvedPackage] = []
@@ -131,7 +142,7 @@ def _get_weekly_downloads(packages: dict[str, list[Package]], fake: bool) -> lis
num_downloads = _get_npm_downloads(package)
else:
num_downloads = None
resolved_packages.append(
{
"name": package["name"],
@@ -145,12 +156,13 @@ def _get_weekly_downloads(packages: dict[str, list[Package]], fake: bool) -> lis
return resolved_packages
def main(output_file: str, fake: bool) -> None:
"""Main function to generate package download information.
Args:
output_file: Path to the output YAML file.
fake: If True, use fake download counts for testing purposes.
fake: If `True`, use fake download counts for testing purposes.
"""
resolved_packages: list[ResolvedPackage] = _get_weekly_downloads(PACKAGES, fake)
+2 -2
View File
@@ -33,7 +33,7 @@ LangGraph provides three ways to manage context, which combines the mutability a
**Static runtime context** represents immutable data like user metadata, tools, and database connections that are passed to an application at the start of a run via the `context` argument to `invoke`/`stream`. This data does not change during execution.
!!! version-added "New in LangGraph v0.6: `context` replaces `config['configurable']`"
!!! version-added "Added in version 0.6.0: `context` replaces `config['configurable']`"
Runtime context is now passed to the `context` argument of `invoke`/`stream`,
which replaces the previous pattern of passing application configuration to `config['configurable']`.
@@ -90,7 +90,7 @@ graph.invoke( # (1)!
from langgraph.runtime import Runtime
# highlight-next-line
def node(state: State, config: Runtime[ContextSchema]):
def node(state: State, runtime: Runtime[ContextSchema]):
user_name = runtime.context.user_name
...
```
+5 -1
View File
@@ -211,7 +211,7 @@ output = agent.invoke(
print(output["messages"][-1].text())
```
!!! version-added "New in LangGraph v0.6"
!!! version-added "Added in version 0.6.0"
:::
@@ -351,11 +351,13 @@ If your desired LLM isn't officially supported by LangChain, consider these opti
:::python
1. **Implement a custom LangChain chat model**: Create a model conforming to the [LangChain chat model interface](https://python.langchain.com/docs/how_to/custom_chat_model/). This enables full compatibility with LangGraph's agents and workflows but requires understanding of the LangChain framework.
:::
:::js
1. **Implement a custom LangChain chat model**: Create a model conforming to the [LangChain chat model interface](https://js.langchain.com/docs/how_to/custom_chat/). This enables full compatibility with LangGraph's agents and workflows but requires understanding of the LangChain framework.
:::
2. **Direct invocation with custom streaming**: Use your model directly by [adding custom streaming logic](../how-tos/streaming.md#use-with-any-llm) with `StreamWriter`.
@@ -371,6 +373,7 @@ If your desired LLM isn't officially supported by LangChain, consider these opti
- [Force model to call a specific tool](https://python.langchain.com/docs/how_to/tool_choice/)
- [All chat model how-to guides](https://python.langchain.com/docs/how_to/#chat-models)
- [Chat model integrations](https://python.langchain.com/docs/integrations/chat/)
:::
:::js
@@ -381,4 +384,5 @@ If your desired LLM isn't officially supported by LangChain, consider these opti
- [Force model to call a specific tool](https://js.langchain.com/docs/how_to/tool_choice/)
- [All chat model how-to guides](https://js.langchain.com/docs/how_to/#chat-models)
- [Chat model integrations](https://js.langchain.com/docs/integrations/chat/)
:::
+265 -2
View File
@@ -1,7 +1,7 @@
{
"openapi": "3.1.0",
"info": {
"title": "LangGraph Platform",
"title": "LangSmith Deployment",
"version": "0.1.0"
},
"tags": [
@@ -29,6 +29,10 @@
"name": "Store",
"description": "Store is an API for managing persistent key-value store (long-term memory) that is available from any thread."
},
{
"name": "A2A",
"description": "Agent-to-Agent Protocol related endpoints for exposing assistants as A2A-compliant agents."
},
{
"name": "MCP",
"description": "Model Context Protocol related endpoints for exposing an agent as an MCP server."
@@ -1550,6 +1554,29 @@
},
"name": "Last-Event-ID",
"in": "header"
},
{
"required": false,
"schema": {
"anyOf": [
{
"type": "string",
"enum": ["lifecycle", "run_modes", "state_update"]
},
{
"type": "array",
"items": {
"type": "string",
"enum": ["lifecycle", "run_modes", "state_update"]
}
}
],
"default": ["run_modes"],
"title": "Stream Modes",
"description": "Stream modes to control which events are returned. 'lifecycle' returns only run start/end events, 'run_modes' returns all run events (default behavior), 'state_update' returns only state update events."
},
"name": "stream_modes",
"in": "query"
}
],
"responses": {
@@ -3159,6 +3186,195 @@
}
}
},
"/a2a/{assistant_id}": {
"post": {
"operationId": "post_a2a",
"summary": "A2A Post",
"description": "Communicate with an assistant using the Agent-to-Agent Protocol.\nSends a JSON-RPC 2.0 message to the assistant.\n\n- **Request**: Provide an object with `jsonrpc`, `id`, `method`, and optional `params`.\n- **Response**: Returns a JSON-RPC response with task information or error.\n\n**Supported Methods:**\n- `message/send`: Send a message to the assistant\n- `tasks/get`: Get the status and result of a task\n\n**Notes:**\n- Supports threaded conversations via thread context\n- Messages can contain text and data parts\n- Tasks run asynchronously and return completion status\n",
"parameters": [
{
"name": "assistant_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
},
"description": "The ID of the assistant to communicate with"
},
{
"name": "Accept",
"in": "header",
"required": true,
"schema": {
"type": "string",
"enum": ["application/json"]
},
"description": "Must be application/json"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"jsonrpc": {
"type": "string",
"enum": ["2.0"],
"description": "JSON-RPC version"
},
"id": {
"type": "string",
"description": "Request identifier"
},
"method": {
"type": "string",
"enum": ["message/send", "tasks/get"],
"description": "The method to invoke"
},
"params": {
"type": "object",
"description": "Method parameters",
"oneOf": [
{
"title": "Message Send Parameters",
"properties": {
"message": {
"type": "object",
"properties": {
"role": {
"type": "string",
"enum": ["user", "assistant"],
"description": "Message role"
},
"parts": {
"type": "array",
"items": {
"oneOf": [
{
"title": "Text Part",
"type": "object",
"properties": {
"kind": {
"type": "string",
"enum": ["text"]
},
"text": {
"type": "string"
}
},
"required": ["kind", "text"]
},
{
"title": "Data Part",
"type": "object",
"properties": {
"kind": {
"type": "string",
"enum": ["data"]
},
"data": {
"type": "object"
}
},
"required": ["kind", "data"]
}
]
},
"description": "Message parts"
},
"messageId": {
"type": "string",
"description": "Unique message identifier"
}
},
"required": ["role", "parts", "messageId"]
},
"thread": {
"type": "object",
"properties": {
"threadId": {
"type": "string",
"description": "Thread identifier for conversation context"
}
},
"description": "Optional thread context"
}
},
"required": ["message"]
},
{
"title": "Task Get Parameters",
"properties": {
"taskId": {
"type": "string",
"description": "Task identifier to retrieve"
}
},
"required": ["taskId"]
}
]
}
},
"required": ["jsonrpc", "id", "method"]
}
}
}
},
"responses": {
"200": {
"description": "JSON-RPC response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"jsonrpc": {
"type": "string",
"enum": ["2.0"]
},
"id": {
"type": "string"
},
"result": {
"type": "object",
"description": "Success result containing task information or task details"
},
"error": {
"type": "object",
"properties": {
"code": {
"type": "integer"
},
"message": {
"type": "string"
}
},
"description": "Error information if request failed"
}
},
"required": ["jsonrpc", "id"]
}
}
}
},
"400": {
"description": "Bad request - invalid JSON-RPC or missing Accept header"
},
"404": {
"description": "Assistant not found"
},
"500": {
"description": "Internal server error"
}
},
"tags": [
"A2A"
]
}
},
"/mcp/": {
"post": {
"operationId": "post_mcp",
@@ -4413,6 +4629,17 @@
"title": "Checkpoint During",
"description": "Whether to checkpoint during the run.",
"default": false
},
"durability": {
"type": "string",
"enum": [
"sync",
"async",
"exit"
],
"title": "Durability",
"description": "Durability level for the run. Must be one of 'sync', 'async', or 'exit'.",
"default": "async"
}
},
"type": "object",
@@ -4649,6 +4876,17 @@
"title": "Checkpoint During",
"description": "Whether to checkpoint during the run.",
"default": false
},
"durability": {
"type": "string",
"enum": [
"sync",
"async",
"exit"
],
"title": "Durability",
"description": "Durability level for the run. Must be one of 'sync', 'async', or 'exit'.",
"default": "async"
}
},
"type": "object",
@@ -4777,6 +5015,12 @@
},
"ThreadSearchRequest": {
"properties": {
"ids": {
"type": "array",
"items": {"type": "string", "format": "uuid"},
"title": "Ids",
"description": "List of thread IDs to include. Others are excluded."
},
"metadata": {
"type": "object",
"title": "Metadata",
@@ -5017,11 +5261,30 @@
"type": "object",
"title": "Metadata",
"description": "Metadata to merge with existing thread metadata."
},
"ttl": {
"type": "object",
"title": "TTL",
"description": "The time-to-live for the thread.",
"properties": {
"strategy": {
"type": "string",
"enum": [
"delete"
],
"description": "The TTL strategy. 'delete' removes the entire thread.",
"default": "delete"
},
"ttl": {
"type": "number",
"description": "The time-to-live in minutes from now until thread should be swept."
}
}
}
},
"type": "object",
"title": "ThreadPatch",
"description": "Payload for creating a thread."
"description": "Payload for updating a thread."
},
"ThreadStateCheckpointRequest": {
"properties": {
+3 -3
View File
@@ -483,19 +483,19 @@ The LangGraph CLI requires a JSON configuration file that follows this [schema](
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt langchain_community langchain_anthropic langchain_openai wikipedia scikit-learn
ADD ./graphs /deps/__outer_graphs/src
ADD ./graphs /deps/outer-graphs/src
RUN set -ex && \
for line in '[project]' \
'name = "graphs"' \
'version = "0.1"' \
'[tool.setuptools.package-data]' \
'"*" = ["**/*"]'; do \
echo "$line" >> /deps/__outer_graphs/pyproject.toml; \
echo "$line" >> /deps/outer-graphs/pyproject.toml; \
done
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"}'
ENV LANGSERVE_GRAPHS='{"agent": "/deps/outer-graphs/src/agent.py:graph", "storm": "/deps/outer-graphs/src/storm.py:graph"}'
```
???+ note "Updating your langgraph.json file"
+14 -4
View File
@@ -21,12 +21,16 @@ To leverage durable execution in LangGraph, you need to:
1. Enable [persistence](./persistence.md) in your workflow by specifying a [checkpointer](./persistence.md#checkpointer-libraries) that will save workflow progress.
2. Specify a [thread identifier](./persistence.md#threads) when executing a workflow. This will track the execution history for a particular instance of the workflow.
:::python
:::python
3. Wrap any non-deterministic operations (e.g., random number generation) or operations with side effects (e.g., file writes, API calls) inside @[tasks][task] to ensure that when a workflow is resumed, these operations are not repeated for the particular run, and instead their results are retrieved from the persistence layer. For more information, see [Determinism and Consistent Replay](#determinism-and-consistent-replay).
:::
:::js
:::js
3. Wrap any non-deterministic operations (e.g., random number generation) or operations with side effects (e.g., file writes, API calls) inside @[tasks][task] to ensure that when a workflow is resumed, these operations are not repeated for the particular run, and instead their results are retrieved from the persistence layer. For more information, see [Determinism and Consistent Replay](#determinism-and-consistent-replay).
:::
## Determinism and Consistent Replay
@@ -61,7 +65,7 @@ LangGraph supports three durability modes that allow you to balance performance
A higher durability mode add more overhead to the workflow execution.
!!! version-added "Added in v0.6.0"
!!! version-added "Added in version 0.6.0"
Use the `durability` parameter instead of `checkpoint_during` (deprecated in v0.6.0) for persistence policy management:
@@ -73,14 +77,16 @@ A higher durability mode add more overhead to the workflow execution.
* `checkpoint_during=True` -> `durability="async"`
* `checkpoint_during=False` -> `durability="exit"`
### `"exit"`
Changes are persisted only when graph execution completes (either successfully or with an error). This provides the best performance for long-running graphs but means intermediate state is not saved, so you cannot recover from mid-execution failures or interrupt the graph execution.
### `"async"`
Changes are persisted asynchronously while the next step executes. This provides good performance and durability, but there's a small risk that checkpoints might not be written if the process crashes during execution.
### `"sync"`
Changes are persisted synchronously before the next step starts. This ensures that every checkpoint is written before continuing execution, providing high durability at the cost of some performance overhead.
You can specify the durability mode when calling any graph execution method:
@@ -310,12 +316,14 @@ Once you have enabled durable execution in your workflow, you can resume executi
- **Pausing and Resuming Workflows:** Use the @[interrupt][interrupt] function to pause a workflow at specific points and the @[Command] primitive to resume it with updated state. See [**Human-in-the-Loop**](./human_in_the_loop.md) for more details.
- **Recovering from Failures:** Automatically resume workflows from the last successful checkpoint after an exception (e.g., LLM provider outage). This involves executing the workflow with the same thread identifier by providing it with a `None` as the input value (see this [example](../how-tos/use-functional-api.md#resuming-after-an-error) with the functional API).
:::
:::js
- **Pausing and Resuming Workflows:** Use the @[interrupt][interrupt] function to pause a workflow at specific points and the @[Command] primitive to resume it with updated state. See [**Human-in-the-Loop**](./human_in_the_loop.md) for more details.
- **Recovering from Failures:** Automatically resume workflows from the last successful checkpoint after an exception (e.g., LLM provider outage). This involves executing the workflow with the same thread identifier by providing it with a `null` as the input value (see this [example](../how-tos/use-functional-api.md#resuming-after-an-error) with the functional API).
:::
## Starting Points for Resuming Workflows
@@ -326,6 +334,7 @@ Once you have enabled durable execution in your workflow, you can resume executi
- If you're making a subgraph call inside a node, the starting point will be the **parent** node that called the subgraph that was halted.
Inside the subgraph, the starting point will be the specific [**node**](./low_level.md#nodes) where execution stopped.
- If you're using the Functional API, the starting point is the beginning of the [**entrypoint**](./functional_api.md#entrypoint) where execution stopped.
:::
:::js
@@ -334,4 +343,5 @@ Once you have enabled durable execution in your workflow, you can resume executi
- If you're making a subgraph call inside a node, the starting point will be the **parent** node that called the subgraph that was halted.
Inside the subgraph, the starting point will be the specific [**node**](./low_level.md#nodes) where execution stopped.
- If you're using the Functional API, the starting point is the beginning of the [**entrypoint**](./functional_api.md#entrypoint) where execution stopped.
:::
+2 -2
View File
@@ -134,7 +134,7 @@ def update_instructions(state: State, store: BaseStore):
namespace = ("instructions",)
current_instructions = store.search(namespace)[0]
# Memory logic
prompt = prompt_template.format(instructions=instructions.value["instructions"], conversation=state["messages"])
prompt = prompt_template.format(instructions=current_instructions.value["instructions"], conversation=state["messages"])
output = llm.invoke(prompt)
new_instructions = output['new_instructions']
store.put(("agent_instructions",), "agent_a", {"instructions": new_instructions})
@@ -278,4 +278,4 @@ const items = await store.search(
```
:::
For more information about the memory store, see the [Persistence](persistence.md#memory-store) guide.
For more information about the memory store, see the [Persistence](persistence.md#memory-store) guide.
+2 -2
View File
@@ -897,5 +897,5 @@ There are two high-level approaches to achieve that:
An agent might need to have a different state schema from the rest of the agents. For example, a search agent might only need to keep track of queries and retrieved documents. There are two ways to achieve this in LangGraph:
- Define [subgraph](./subgraphs.md) agents with a separate state schema. If there are no shared state keys (channels) between the subgraph and the parent graph, it's important to [add input / output transformations](../how-tos/subgraph.ipynb#different-state-schemas) so that the parent graph knows how to communicate with the subgraphs.
- Define agent node functions with a [private input state schema](../how-tos/graph-api.ipynb#pass-private-state-between-nodes) that is distinct from the overall graph state schema. This allows passing information that is only needed for executing that particular agent.
- Define [subgraph](./subgraphs.md) agents with a separate state schema. If there are no shared state keys (channels) between the subgraph and the parent graph, it's important to [add input / output transformations](../how-tos/subgraph.md#different-state-schemas) so that the parent graph knows how to communicate with the subgraphs.
- Define agent node functions with a [private input state schema](../how-tos/graph-api.md#pass-private-state-between-nodes) that is distinct from the overall graph state schema. This allows passing information that is only needed for executing that particular agent.
+13 -14
View File
@@ -1019,7 +1019,7 @@ console.log(await graph.invoke({}, { configurable: { myRuntimeValue: "b" } }));
# Usage
input_message = {"role": "user", "content": "hi"}
# With no configuration, uses default (Anthropic)
response_1 = graph.invoke({"messages": [input_message]})["messages"][-1]
response_1 = graph.invoke({"messages": [input_message]}, context=ContextSchema())["messages"][-1]
# Or, can set OpenAI
response_2 = graph.invoke({"messages": [input_message]}, context={"model_provider": "openai"})["messages"][-1]
@@ -1205,7 +1205,7 @@ There are many use cases where you may wish for your node to have a custom retry
To configure a retry policy, pass the `retry_policy` parameter to the [add_node](../reference/graphs.md#langgraph.graph.state.StateGraph.add_node). The `retry_policy` parameter takes in a `RetryPolicy` named tuple object. Below we instantiate a `RetryPolicy` object with the default parameters and associate it with a node:
```python
from langgraph.pregel import RetryPolicy
from langgraph.types import RetryPolicy
builder.add_node(
"node_name",
@@ -1260,7 +1260,7 @@ By default, the retry policy retries on any exception except for the following:
from typing_extensions import TypedDict
from langchain.chat_models import init_chat_model
from langgraph.graph import END, MessagesState, StateGraph, START
from langgraph.pregel import RetryPolicy
from langgraph.types import RetryPolicy
from langchain_community.utilities import SQLDatabase
from langchain_core.messages import AIMessage
@@ -1422,15 +1422,15 @@ const builder = new StateGraph(State)
:::
??? info "Why split application steps into a sequence with LangGraph?"
LangGraph makes it easy to add an underlying persistence layer to your application.
This allows state to be checkpointed in between the execution of nodes, so your LangGraph nodes govern:
LangGraph makes it easy to add an underlying persistence layer to your application.
This allows state to be checkpointed in between the execution of nodes, so your LangGraph nodes govern:
- How state updates are [checkpointed](../concepts/persistence.md)
- How interruptions are resumed in [human-in-the-loop](../concepts/human_in_the_loop.md) workflows
- How we can "rewind" and branch-off executions using LangGraph's [time travel](../concepts/time-travel.md) features
- How state updates are [checkpointed](../concepts/persistence.md)
- How interruptions are resumed in [human-in-the-loop](../concepts/human_in_the_loop.md) workflows
- How we can "rewind" and branch-off executions using LangGraph's [time travel](../concepts/time-travel.md) features
They also determine how execution steps are [streamed](../concepts/streaming.md), and how your application is visualized
and debugged using [LangGraph Studio](../concepts/langgraph_studio.md).
They also determine how execution steps are [streamed](../concepts/streaming.md), and how your application is visualized
and debugged using [LangGraph Studio](../concepts/langgraph_studio.md).
Let's demonstrate an end-to-end example. We will create a sequence of three steps:
@@ -2110,7 +2110,6 @@ builder.add_edge(START, "generate_topics")
builder.add_conditional_edges("generate_topics", continue_to_jokes, ["generate_joke"])
builder.add_edge("generate_joke", "best_joke")
builder.add_edge("best_joke", END)
builder.add_edge("generate_topics", END)
graph = builder.compile()
```
@@ -2333,7 +2332,7 @@ from IPython.display import Image, display
display(Image(graph.get_graph().draw_mermaid_png()))
```
![Simple loop graph](assets/graph_api_image_3.png)
![Simple loop graph](assets/graph_api_image_7.png)
:::
:::js
@@ -3272,7 +3271,7 @@ from langchain_core.runnables.graph import CurveStyle, MermaidDrawMethod, NodeSt
display(Image(app.get_graph().draw_mermaid_png()))
```
![Fractal graph visualization](assets/graph_api_image_5.png)
![Fractal graph visualization](assets/graph_api_image_10.png)
**Using Mermaid + Pyppeteer**
@@ -3320,4 +3319,4 @@ const imageBuffer = new Uint8Array(await image.arrayBuffer());
await fs.writeFile("graph.png", imageBuffer);
```
:::
:::
@@ -366,8 +366,8 @@ result = graph.invoke(
# Resume with mapping of interrupt IDs to values
resume_map = {
i.interrupt_id: f"human input for prompt {i.value}"
for i in parent.get_state(thread_config).interrupts
i.id: f"edited text for {i.value['text_to_revise']}"
for i in graph.get_state(config).interrupts
}
print(graph.invoke(Command(resume=resume_map), config=config))
# > {'text_1': 'edited text for original text 1', 'text_2': 'edited text for original text 2'}
+1 -1
View File
@@ -244,7 +244,7 @@ output = agent.invoke(
print(output["messages"][-1].text())
```
!!! version-added "New in langgraph>=0.6"
!!! version-added "Added in version 0.6.0"
:::
+1 -1
View File
@@ -2,4 +2,4 @@
::: langgraph.cache.base
::: langgraph.cache.memory
::: langgraph.cache.sqlite
::: langgraph.cache.sqlite
+1 -1
View File
@@ -68,7 +68,7 @@ The server will start and open the studio in your browser:
> - 📚 API Docs: http://127.0.0.1:2024/docs
>
> This in-memory server is designed for development and testing.
> For production use, please use LangGraph Platform.
> For production use, please use LangSmith Deployment.
```
If you were to self-host this on the public internet, anyone could access it!
+1 -1
View File
@@ -1948,7 +1948,7 @@ const llmWithTools = llm.bindTools(tools);
# Conditional edge function to route to the tool node or end based upon whether the LLM made a tool call
def should_continue(state: MessagesState) -> Literal["environment", END]:
def should_continue(state: MessagesState) -> Literal["Action", END]:
"""Decide if we should continue the loop or stop based upon whether the LLM made a tool call"""
messages = state["messages"]
+2 -2
View File
@@ -291,7 +291,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
}
.md-banner {
background-color: #CFC9FA;
background-color: #FFAE42;
color: #000000;
}
@@ -360,5 +360,5 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
{% endblock %}
{% block announce %}
Our new LangChain Academy Course Deep Research with LangGraph is now live! <a href="https://academy.langchain.com/courses/deep-research-with-langgraph/?utm_medium=internal&utm_source=docs&utm_campaign=q3-2025_deep-research-course_co" target="_blank">Enroll for free</a>.
These docs will be deprecated and removed with the release of LangGraph v1.0 in October 2025. <a href="https://docs.langchain.com/oss/python/langgraph/overview" target="_blank">Visit the v1.0 alpha docs</a>
{% endblock %}
+4 -4
View File
@@ -7,14 +7,14 @@ name = "langgraph-docs"
version = "0.0.1"
description = "LangGraph docs"
authors = []
requires-python = "~=3.11"
requires-python = ">=3.11.0,<4.0.0"
readme = "README.md"
license = "MIT"
dependencies = [
"aiohappyeyeballs==2.4.3",
"hub>=3.0.1,<4",
"xxhash>=3.5.0,<4",
"black>=25.1.0,<26",
"hub>=3.0.1,<4.0.0",
"xxhash>=3.5.0,<4.0.0",
"black>=25.1.0,<26.0.0",
]
[dependency-groups]
Generated
+5 -4
View File
@@ -1,5 +1,5 @@
version = 1
revision = 2
revision = 3
requires-python = ">=3.11, <4"
resolution-markers = [
"python_full_version >= '3.13' and platform_python_implementation != 'PyPy'",
@@ -2337,7 +2337,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "0.6.2"
version = "0.6.7"
source = { editable = "../libs/langgraph" }
dependencies = [
{ name = "langchain-core" },
@@ -2380,6 +2380,7 @@ dev = [
{ name = "pytest-repeat" },
{ name = "pytest-watcher" },
{ name = "pytest-xdist", extras = ["psutil"] },
{ name = "redis" },
{ name = "ruff" },
{ name = "syrupy" },
{ name = "types-requests" },
@@ -2413,6 +2414,7 @@ dev = [
{ name = "pytest-asyncio" },
{ name = "pytest-mock" },
{ name = "pytest-watcher" },
{ name = "redis" },
{ name = "ruff" },
]
@@ -2643,7 +2645,7 @@ test = [
[[package]]
name = "langgraph-prebuilt"
version = "0.6.2"
version = "0.6.4"
source = { editable = "../libs/prebuilt" }
dependencies = [
{ name = "langchain-core" },
@@ -2674,7 +2676,6 @@ dev = [
[[package]]
name = "langgraph-sdk"
version = "0.2.0"
source = { editable = "../libs/sdk-py" }
dependencies = [
{ name = "httpx" },
+1 -1
View File
@@ -5,7 +5,7 @@
"id": "18526f23",
"metadata": {},
"source": [
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/persistence_postgres.ipynb"
"This file has been moved to https://github.com/langchain-ai/langgraph/blob/main/docs/docs/how-tos/memory/add-memory.md"
]
}
],
+3 -1
View File
@@ -707,7 +707,9 @@
" \"\"\"\n",
" Find all tool calls in the messages returned\n",
" \"\"\"\n",
" tool_calls = [tc['name'] for m in messages['messages'] for tc in getattr(m, 'tool_calls', [])]\n",
" tool_calls = [\n",
" tc[\"name\"] for m in messages[\"messages\"] for tc in getattr(m, \"tool_calls\", [])\n",
" ]\n",
" return tool_calls\n",
"\n",
"\n",
@@ -7,11 +7,6 @@ from contextlib import contextmanager
from typing import Any
from langchain_core.runnables import RunnableConfig
from psycopg import Capabilities, Connection, Cursor, Pipeline
from psycopg.rows import DictRow, dict_row
from psycopg.types.json import Jsonb
from psycopg_pool import ConnectionPool
from langgraph.checkpoint.base import (
WRITES_IDX_MAP,
ChannelVersions,
@@ -19,12 +14,17 @@ from langgraph.checkpoint.base import (
CheckpointMetadata,
CheckpointTuple,
get_checkpoint_id,
get_checkpoint_metadata,
get_serializable_checkpoint_metadata,
)
from langgraph.checkpoint.serde.base import SerializerProtocol
from psycopg import Capabilities, Connection, Cursor, Pipeline
from psycopg.rows import DictRow, dict_row
from psycopg.types.json import Jsonb
from psycopg_pool import ConnectionPool
from langgraph.checkpoint.postgres import _internal
from langgraph.checkpoint.postgres.base import BasePostgresSaver
from langgraph.checkpoint.postgres.shallow import ShallowPostgresSaver
from langgraph.checkpoint.serde.base import SerializerProtocol
Conn = _internal.Conn # For backward compatibility
@@ -94,6 +94,7 @@ class PostgresSaver(BasePostgresSaver):
for v, migration in zip(
range(version + 1, len(self.MIGRATIONS)),
self.MIGRATIONS[version + 1 :],
strict=False,
):
cur.execute(migration)
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
@@ -115,12 +116,12 @@ class PostgresSaver(BasePostgresSaver):
Args:
config: The config to use for listing the checkpoints.
filter: Additional filtering criteria for metadata. Defaults to None.
before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
limit: The maximum number of checkpoints to return. Defaults to None.
filter: Additional filtering criteria for metadata.
before: If provided, only checkpoints before the specified checkpoint ID are returned.
limit: The maximum number of checkpoints to return.
Yields:
Iterator[CheckpointTuple]: An iterator of checkpoint tuples.
An iterator of checkpoint tuples.
Examples:
>>> from langgraph.checkpoint.postgres import PostgresSaver
@@ -182,7 +183,7 @@ class PostgresSaver(BasePostgresSaver):
"""Get a checkpoint tuple from the database.
This method retrieves a checkpoint tuple from the Postgres database based on the
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
for the given thread ID is retrieved.
@@ -190,7 +191,7 @@ class PostgresSaver(BasePostgresSaver):
config: The config to use for retrieving the checkpoint.
Returns:
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
Examples:
@@ -325,7 +326,7 @@ class PostgresSaver(BasePostgresSaver):
checkpoint["id"],
checkpoint_id,
Jsonb(copy),
Jsonb(get_checkpoint_metadata(config, metadata)),
Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
),
)
return next_config
@@ -450,7 +451,7 @@ class PostgresSaver(BasePostgresSaver):
{
**value["checkpoint"],
"channel_values": {
**value["checkpoint"].get("channel_values"),
**(value["checkpoint"].get("channel_values") or {}),
**self._load_blobs(value["channel_values"]),
},
},
@@ -2,13 +2,12 @@
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import Union
from psycopg import AsyncConnection
from psycopg.rows import DictRow
from psycopg_pool import AsyncConnectionPool
Conn = Union[AsyncConnection[DictRow], AsyncConnectionPool[AsyncConnection[DictRow]]]
Conn = AsyncConnection[DictRow] | AsyncConnectionPool[AsyncConnection[DictRow]]
@asynccontextmanager
@@ -2,13 +2,12 @@
from collections.abc import Iterator
from contextlib import contextmanager
from typing import Union
from psycopg import Connection
from psycopg.rows import DictRow
from psycopg_pool import ConnectionPool
Conn = Union[Connection[DictRow], ConnectionPool[Connection[DictRow]]]
Conn = Connection[DictRow] | ConnectionPool[Connection[DictRow]]
@contextmanager
@@ -7,11 +7,6 @@ from contextlib import asynccontextmanager
from typing import Any
from langchain_core.runnables import RunnableConfig
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
from psycopg.rows import DictRow, dict_row
from psycopg.types.json import Jsonb
from psycopg_pool import AsyncConnectionPool
from langgraph.checkpoint.base import (
WRITES_IDX_MAP,
ChannelVersions,
@@ -19,12 +14,17 @@ from langgraph.checkpoint.base import (
CheckpointMetadata,
CheckpointTuple,
get_checkpoint_id,
get_checkpoint_metadata,
get_serializable_checkpoint_metadata,
)
from langgraph.checkpoint.serde.base import SerializerProtocol
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
from psycopg.rows import DictRow, dict_row
from psycopg.types.json import Jsonb
from psycopg_pool import AsyncConnectionPool
from langgraph.checkpoint.postgres import _ainternal
from langgraph.checkpoint.postgres.base import BasePostgresSaver
from langgraph.checkpoint.postgres.shallow import AsyncShallowPostgresSaver
from langgraph.checkpoint.serde.base import SerializerProtocol
Conn = _ainternal.Conn # For backward compatibility
@@ -99,6 +99,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
for v, migration in zip(
range(version + 1, len(self.MIGRATIONS)),
self.MIGRATIONS[version + 1 :],
strict=False,
):
await cur.execute(migration)
await cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
@@ -121,11 +122,11 @@ class AsyncPostgresSaver(BasePostgresSaver):
Args:
config: Base configuration for filtering checkpoints.
filter: Additional filtering criteria for metadata.
before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
before: If provided, only checkpoints before the specified checkpoint ID are returned.
limit: Maximum number of checkpoints to return.
Yields:
AsyncIterator[CheckpointTuple]: An asynchronous iterator of matching checkpoint tuples.
An asynchronous iterator of matching checkpoint tuples.
"""
where, args = self._search_where(config, filter, before)
query = self.SELECT_SQL + where + " ORDER BY checkpoint_id DESC"
@@ -169,7 +170,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
"""Get a checkpoint tuple from the database asynchronously.
This method retrieves a checkpoint tuple from the Postgres database based on the
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint
for the given thread ID is retrieved.
@@ -177,7 +178,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
config: The config to use for retrieving the checkpoint.
Returns:
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
"""
thread_id = config["configurable"]["thread_id"]
checkpoint_id = get_checkpoint_id(config)
@@ -283,7 +284,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
checkpoint["id"],
checkpoint_id,
Jsonb(copy),
Jsonb(get_checkpoint_metadata(config, metadata)),
Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
),
)
return next_config
@@ -409,7 +410,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
{
**value["checkpoint"],
"channel_values": {
**value["checkpoint"].get("channel_values"),
**(value["checkpoint"].get("channel_values") or {}),
**self._load_blobs(value["channel_values"]),
},
},
@@ -444,11 +445,11 @@ class AsyncPostgresSaver(BasePostgresSaver):
Args:
config: Base configuration for filtering checkpoints.
filter: Additional filtering criteria for metadata.
before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
before: If provided, only checkpoints before the specified checkpoint ID are returned.
limit: Maximum number of checkpoints to return.
Yields:
Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
An iterator of matching checkpoint tuples.
"""
try:
# check if we are in the main thread, only bg threads can block
@@ -476,7 +477,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
"""Get a checkpoint tuple from the database.
This method retrieves a checkpoint tuple from the Postgres database based on the
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint
for the given thread ID is retrieved.
@@ -484,7 +485,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
config: The config to use for retrieving the checkpoint.
Returns:
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
"""
try:
# check if we are in the main thread, only bg threads can block
@@ -1,12 +1,12 @@
from __future__ import annotations
import random
import warnings
from collections.abc import Sequence
from typing import Any, Optional, cast
from importlib.metadata import version as get_version
from typing import Any, cast
from langchain_core.runnables import RunnableConfig
from psycopg.types.json import Jsonb
from langgraph.checkpoint.base import (
WRITES_IDX_MAP,
BaseCheckpointSaver,
@@ -14,8 +14,21 @@ from langgraph.checkpoint.base import (
get_checkpoint_id,
)
from langgraph.checkpoint.serde.types import TASKS
from psycopg.types.json import Jsonb
MetadataInput = Optional[dict[str, Any]]
MetadataInput = dict[str, Any] | None
try:
major, minor = get_version("langgraph").split(".")[:2]
if int(major) == 0 and int(minor) < 5:
warnings.warn(
"You're using incompatible versions of langgraph and checkpoint-postgres. Please upgrade langgraph to avoid unexpected behavior.",
DeprecationWarning,
stacklevel=2,
)
except Exception:
# skip version check if running from source
pass
"""
To add a new migration, add a new string to the MIGRATIONS list.
@@ -3,9 +3,19 @@ import threading
import warnings
from collections.abc import AsyncIterator, Iterator, Sequence
from contextlib import asynccontextmanager, contextmanager
from typing import Any, Optional
from typing import Any
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import (
WRITES_IDX_MAP,
ChannelVersions,
Checkpoint,
CheckpointMetadata,
CheckpointTuple,
get_serializable_checkpoint_metadata,
)
from langgraph.checkpoint.serde.base import SerializerProtocol
from langgraph.checkpoint.serde.types import TASKS
from psycopg import (
AsyncConnection,
AsyncCursor,
@@ -19,18 +29,8 @@ from psycopg.rows import DictRow, dict_row
from psycopg.types.json import Jsonb
from psycopg_pool import AsyncConnectionPool, ConnectionPool
from langgraph.checkpoint.base import (
WRITES_IDX_MAP,
ChannelVersions,
Checkpoint,
CheckpointMetadata,
CheckpointTuple,
get_checkpoint_metadata,
)
from langgraph.checkpoint.postgres import _ainternal, _internal
from langgraph.checkpoint.postgres.base import BasePostgresSaver
from langgraph.checkpoint.serde.base import SerializerProtocol
from langgraph.checkpoint.serde.types import TASKS
"""
To add a new migration, add a new string to the MIGRATIONS list.
@@ -151,7 +151,7 @@ def _dump_blobs(
checkpoint_ns: str,
values: dict[str, Any],
versions: ChannelVersions,
) -> list[tuple[str, str, str, str, Optional[bytes]]]:
) -> list[tuple[str, str, str, str, bytes | None]]:
if not versions:
return []
@@ -186,8 +186,8 @@ class ShallowPostgresSaver(BasePostgresSaver):
def __init__(
self,
conn: _internal.Conn,
pipe: Optional[Pipeline] = None,
serde: Optional[SerializerProtocol] = None,
pipe: Pipeline | None = None,
serde: SerializerProtocol | None = None,
) -> None:
warnings.warn(
"ShallowPostgresSaver is deprecated as of version 2.0.20 and will be removed in 3.0.0. "
@@ -249,6 +249,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
for v, migration in zip(
range(version + 1, len(self.MIGRATIONS)),
self.MIGRATIONS[version + 1 :],
strict=False,
):
cur.execute(migration)
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
@@ -257,11 +258,11 @@ class ShallowPostgresSaver(BasePostgresSaver):
def list(
self,
config: Optional[RunnableConfig],
config: RunnableConfig | None,
*,
filter: Optional[dict[str, Any]] = None,
before: Optional[RunnableConfig] = None,
limit: Optional[int] = None,
filter: dict[str, Any] | None = None,
before: RunnableConfig | None = None,
limit: int | None = None,
) -> Iterator[CheckpointTuple]:
"""List checkpoints from the database.
@@ -299,7 +300,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
pending_writes=self._load_writes(value["pending_writes"]),
)
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
"""Get a checkpoint tuple from the database.
This method retrieves a checkpoint tuple from the Postgres database based on the
@@ -309,7 +310,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
config: The config to use for retrieving the checkpoint.
Returns:
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
Examples:
@@ -441,7 +442,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
thread_id,
checkpoint_ns,
Jsonb(copy),
Jsonb(get_checkpoint_metadata(config, metadata)),
Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
),
)
return next_config
@@ -542,8 +543,8 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
def __init__(
self,
conn: _ainternal.Conn,
pipe: Optional[AsyncPipeline] = None,
serde: Optional[SerializerProtocol] = None,
pipe: AsyncPipeline | None = None,
serde: SerializerProtocol | None = None,
) -> None:
warnings.warn(
"AsyncShallowPostgresSaver is deprecated as of version 2.0.20 and will be removed in 3.0.0. "
@@ -570,7 +571,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
conn_string: str,
*,
pipeline: bool = False,
serde: Optional[SerializerProtocol] = None,
serde: SerializerProtocol | None = None,
) -> AsyncIterator["AsyncShallowPostgresSaver"]:
"""Create a new AsyncShallowPostgresSaver instance from a connection string.
@@ -610,6 +611,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
for v, migration in zip(
range(version + 1, len(self.MIGRATIONS)),
self.MIGRATIONS[version + 1 :],
strict=False,
):
await cur.execute(migration)
await cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
@@ -618,11 +620,11 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
async def alist(
self,
config: Optional[RunnableConfig],
config: RunnableConfig | None,
*,
filter: Optional[dict[str, Any]] = None,
before: Optional[RunnableConfig] = None,
limit: Optional[int] = None,
filter: dict[str, Any] | None = None,
before: RunnableConfig | None = None,
limit: int | None = None,
) -> AsyncIterator[CheckpointTuple]:
"""List checkpoints from the database asynchronously.
@@ -662,7 +664,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
),
)
async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
"""Get a checkpoint tuple from the database asynchronously.
This method retrieves a checkpoint tuple from the Postgres database based on the
@@ -672,7 +674,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
config: The config to use for retrieving the checkpoint.
Returns:
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
"""
thread_id = config["configurable"]["thread_id"]
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
@@ -774,7 +776,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
thread_id,
checkpoint_ns,
Jsonb(copy),
Jsonb(get_checkpoint_metadata(config, metadata)),
Jsonb(get_serializable_checkpoint_metadata(config, metadata)),
),
)
return next_config
@@ -861,11 +863,11 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
def list(
self,
config: Optional[RunnableConfig],
config: RunnableConfig | None,
*,
filter: Optional[dict[str, Any]] = None,
before: Optional[RunnableConfig] = None,
limit: Optional[int] = None,
filter: dict[str, Any] | None = None,
before: RunnableConfig | None = None,
limit: int | None = None,
) -> Iterator[CheckpointTuple]:
"""List checkpoints from the database.
@@ -883,7 +885,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
except StopAsyncIteration:
break
def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
"""Get a checkpoint tuple from the database.
This method retrieves a checkpoint tuple from the Postgres database based on the
@@ -893,7 +895,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
config: The config to use for retrieving the checkpoint.
Returns:
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
"""
try:
# check if we are in the main thread, only bg threads can block
@@ -1,4 +1,4 @@
from langgraph.store.postgres.aio import AsyncPostgresStore
from langgraph.store.postgres.base import PostgresStore
from langgraph.store.postgres.base import PoolConfig, PostgresStore
__all__ = ["AsyncPostgresStore", "PostgresStore"]
__all__ = ["AsyncPostgresStore", "PoolConfig", "PostgresStore"]
@@ -2,17 +2,12 @@ from __future__ import annotations
import asyncio
import logging
from collections.abc import AsyncIterator, Iterable, Sequence
from collections.abc import AsyncIterator, Callable, Iterable, Sequence
from contextlib import asynccontextmanager
from types import TracebackType
from typing import Any, Callable, cast
from typing import Any, cast
import orjson
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
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,
@@ -22,6 +17,11 @@ from langgraph.store.base import (
SearchOp,
)
from langgraph.store.base.batch import AsyncBatchedBaseStore
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
from psycopg.rows import DictRow, dict_row
from psycopg_pool import AsyncConnectionPool
from langgraph.checkpoint.postgres import _ainternal
from langgraph.store.postgres.base import (
PLACEHOLDER,
BasePostgresStore,
@@ -339,7 +339,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
Args:
timeout: Maximum time to wait for the task to stop, in seconds.
If None, wait indefinitely.
If `None`, wait indefinitely.
Returns:
bool: True if the task was successfully stopped or wasn't running,
@@ -465,7 +465,9 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
query,
[
p
for (ns, k, pathname, _), vector in zip(txt_params, vectors)
for (ns, k, pathname, _), vector in zip(
txt_params, vectors, strict=False
)
for p in (ns, k, pathname, vector)
],
)
@@ -486,13 +488,13 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
vectors = await self.embeddings.aembed_documents(
[query for _, query in embedding_requests]
)
for (idx, _), vector in zip(embedding_requests, vectors):
for (idx, _), vector in zip(embedding_requests, vectors, strict=False):
_paramslist = queries[idx][1]
for i in range(len(_paramslist)):
if _paramslist[i] is PLACEHOLDER:
_paramslist[i] = vector
for (idx, _), (query, params) in zip(search_ops, queries):
for (idx, _), (query, params) in zip(search_ops, queries, strict=False):
await cur.execute(query, params)
rows = cast(list[Row], await cur.fetchall())
items = [
@@ -510,7 +512,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
cur: AsyncCursor[DictRow],
) -> None:
queries = self._get_batch_list_namespaces_queries(list_ops)
for (query, params), (idx, _) in zip(queries, list_ops):
for (query, params), (idx, _) in zip(queries, list_ops, strict=False):
await cur.execute(query, params)
rows = cast(list[dict], await cur.fetchall())
namespaces = [_decode_ns_bytes(row["truncated_prefix"]) for row in rows]
@@ -6,30 +6,20 @@ import json
import logging
import threading
from collections import defaultdict
from collections.abc import Iterable, Iterator, Sequence
from collections.abc import Callable, Iterable, Iterator, Sequence
from contextlib import contextmanager
from datetime import datetime
from typing import (
TYPE_CHECKING,
Any,
Callable,
Generic,
Literal,
NamedTuple,
TypeVar,
Union,
cast,
)
import orjson
from psycopg import Capabilities, Connection, Cursor, Pipeline
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,
@@ -46,6 +36,14 @@ from langgraph.store.base import (
get_text_at_path,
tokenize_path,
)
from psycopg import Capabilities, Connection, Cursor, Pipeline
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
if TYPE_CHECKING:
from langchain_core.embeddings import Embeddings
@@ -141,7 +139,7 @@ CREATE INDEX CONCURRENTLY IF NOT EXISTS store_vectors_embedding_idx ON store_vec
]
C = TypeVar("C", bound=Union[_pg_internal.Conn, _ainternal.Conn])
C = TypeVar("C", bound=_pg_internal.Conn | _ainternal.Conn)
class PoolConfig(TypedDict, total=False):
@@ -255,7 +253,7 @@ class BasePostgresStore(Generic[C]):
results = []
for namespace, items in namespace_groups.items():
_, keys = zip(*items)
_, keys = zip(*items, strict=False)
this_refresh_ttls = refresh_ttls[namespace]
query = """
@@ -868,7 +866,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
Args:
timeout: Maximum time to wait for the thread to stop, in seconds.
If None, wait indefinitely.
If `None`, wait indefinitely.
Returns:
bool: True if the thread was successfully stopped or wasn't running,
@@ -1014,7 +1012,9 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
query,
[
p
for (ns, k, pathname, _), vector in zip(txt_params, vectors)
for (ns, k, pathname, _), vector in zip(
txt_params, vectors, strict=False
)
for p in (ns, k, pathname, vector)
],
)
@@ -1035,13 +1035,15 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
embeddings = self.embeddings.embed_documents(
[query for _, query in embedding_requests]
)
for (idx, _), embedding in zip(embedding_requests, embeddings):
for (idx, _), embedding in zip(
embedding_requests, embeddings, strict=False
):
_paramslist = queries[idx][1]
for i in range(len(_paramslist)):
if _paramslist[i] is PLACEHOLDER:
_paramslist[i] = embedding
for (idx, _), (query, params) in zip(search_ops, queries):
for (idx, _), (query, params) in zip(search_ops, queries, strict=False):
cur.execute(query, params)
rows = cast(list[Row], cur.fetchall())
results[idx] = [
@@ -1058,7 +1060,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
cur: Cursor[DictRow],
) -> None:
for (query, params), (idx, _) in zip(
self._get_batch_list_namespaces_queries(list_ops), list_ops
self._get_batch_list_namespaces_queries(list_ops), list_ops, strict=False
):
cur.execute(query, params)
results[idx] = [_decode_ns_bytes(row["truncated_prefix"]) for row in cur]
+15 -7
View File
@@ -4,15 +4,15 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-checkpoint-postgres"
version = "2.0.23"
version = "3.0.0"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
authors = []
requires-python = ">=3.9"
requires-python = ">=3.10"
readme = "README.md"
license = "MIT"
license-files = ['LICENSE']
dependencies = [
"langgraph-checkpoint>=2.0.21,<3.0.0",
"langgraph-checkpoint>=2.1.2,<4.0.0",
"orjson>=3.10.1",
"psycopg>=3.2.0",
"psycopg-pool>=3.2.0",
@@ -22,18 +22,24 @@ dependencies = [
Repository = "https://www.github.com/langchain-ai/langgraph"
[dependency-groups]
dev = [
"ruff",
"codespell",
test = [
"pytest",
"anyio",
"pytest-asyncio",
"pytest-mock",
"mypy",
"psycopg[binary]",
"langgraph-checkpoint",
"pytest-watcher",
]
lint = [
"ruff",
"codespell",
"mypy",
]
dev = [
{include-group = "test"},
{include-group = "lint"},
]
[tool.uv]
default-groups = ['dev']
@@ -55,8 +61,10 @@ lint.select = [
"UP", # pyupgrade
"B", # flake8-bugbear
"I", # isort
"UP", # pyupgrade
]
lint.ignore = ["E501", "B008"]
target-version = "py310"
[tool.mypy]
# https://mypy.readthedocs.io/en/stable/config_file.html
+36 -9
View File
@@ -6,10 +6,6 @@ from uuid import uuid4
import pytest
from langchain_core.runnables import RunnableConfig
from psycopg import AsyncConnection
from psycopg.rows import dict_row
from psycopg_pool import AsyncConnectionPool
from langgraph.checkpoint.base import (
EXCLUDED_METADATA_KEYS,
Checkpoint,
@@ -17,11 +13,15 @@ from langgraph.checkpoint.base import (
create_checkpoint,
empty_checkpoint,
)
from langgraph.checkpoint.serde.types import TASKS
from psycopg import AsyncConnection
from psycopg.rows import dict_row
from psycopg_pool import AsyncConnectionPool
from langgraph.checkpoint.postgres.aio import (
AsyncPostgresSaver,
AsyncShallowPostgresSaver,
)
from langgraph.checkpoint.serde.types import TASKS
from tests.conftest import DEFAULT_POSTGRES_URI
@@ -187,13 +187,11 @@ def test_data():
metadata_1: CheckpointMetadata = {
"source": "input",
"step": 2,
"writes": {},
"score": 1,
}
metadata_2: CheckpointMetadata = {
"source": "loop",
"step": 1,
"writes": {"foo": "bar"},
"score": None,
}
metadata_3: CheckpointMetadata = {}
@@ -220,7 +218,6 @@ async def test_combined_metadata(saver_name: str, test_data) -> None:
metadata: CheckpointMetadata = {
"source": "loop",
"step": 1,
"writes": {"foo": "bar"},
"score": None,
}
await saver.aput(config, chkpnt, metadata, {})
@@ -246,7 +243,6 @@ async def test_asearch(saver_name: str, test_data) -> None:
query_1 = {"source": "input"} # search by 1 key
query_2 = {
"step": 1,
"writes": {"foo": "bar"},
} # search by multiple keys
query_3: dict[str, Any] = {} # search by no keys, return all checkpoints
query_4 = {"source": "update", "step": 1} # no match
@@ -344,3 +340,34 @@ async def test_pending_sends_migration(saver_name: str) -> None:
TASKS: ["send-1", "send-2", "send-3"]
}
assert TASKS in search_results[0].checkpoint["channel_versions"]
@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe"])
async def test_get_checkpoint_no_channel_values(
monkeypatch, saver_name: str, test_data
) -> None:
"""Backwards compatibility test that verifies a checkpoint with no channel_values key can be retrieved without throwing an error."""
async with _saver(saver_name) as saver:
config = {
"configurable": {
"thread_id": "thread-2",
"checkpoint_ns": "",
"__super_private_key": "super_private_value",
},
"metadata": {"run_id": "my_run_id"},
}
chkpnt: Checkpoint = create_checkpoint(empty_checkpoint(), {}, 1)
await saver.aput(config, chkpnt, {}, {})
load_checkpoint_tuple = saver._load_checkpoint_tuple
def patched_load_checkpoint_tuple(value):
value["checkpoint"].pop("channel_values", None)
return load_checkpoint_tuple(value)
monkeypatch.setattr(
saver, "_load_checkpoint_tuple", patched_load_checkpoint_tuple
)
checkpoint = await saver.aget_tuple(config)
assert checkpoint.checkpoint["channel_values"] == {}
@@ -3,7 +3,6 @@ from __future__ import annotations
import asyncio
import itertools
import sys
import uuid
from collections.abc import AsyncIterator
from concurrent.futures import ThreadPoolExecutor
@@ -12,8 +11,6 @@ from typing import Any
import pytest
from langchain_core.embeddings import Embeddings
from psycopg import AsyncConnection
from langgraph.store.base import (
GetOp,
Item,
@@ -21,6 +18,8 @@ from langgraph.store.base import (
PutOp,
SearchOp,
)
from psycopg import AsyncConnection
from langgraph.store.postgres import AsyncPostgresStore
from tests.conftest import (
DEFAULT_URI,
@@ -34,9 +33,6 @@ TTL_MINUTES = TTL_SECONDS / 60
@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])
@@ -358,8 +354,6 @@ async def _create_vector_store(
text_fields: list[str] | None = None,
) -> AsyncIterator[AsyncPostgresStore]:
"""Create a store with vector search enabled."""
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("/")
+43 -5
View File
@@ -9,8 +9,6 @@ from uuid import uuid4
import pytest
from langchain_core.embeddings import Embeddings
from psycopg import Connection
from langgraph.store.base import (
GetOp,
Item,
@@ -19,6 +17,8 @@ from langgraph.store.base import (
PutOp,
SearchOp,
)
from psycopg import Connection
from langgraph.store.postgres import PostgresStore
from tests.conftest import (
DEFAULT_URI,
@@ -754,7 +754,7 @@ def _cosine_similarity(X: list[float], Y: list[list[float]]) -> list[float]:
similarities = []
for y in Y:
dot_product = sum(a * b for a, b in zip(X, y))
dot_product = sum(a * b for a, b in zip(X, y, strict=False))
norm1 = sum(a * a for a in X) ** 0.5
norm2 = sum(a * a for a in y) ** 0.5
similarity = dot_product / (norm1 * norm2) if norm1 > 0 and norm2 > 0 else 0.0
@@ -771,7 +771,7 @@ def _inner_product(X: list[float], Y: list[list[float]]) -> list[float]:
similarities = []
for y in Y:
similarity = sum(a * b for a, b in zip(X, y))
similarity = sum(a * b for a, b in zip(X, y, strict=False))
similarities.append(similarity)
return similarities
@@ -785,7 +785,7 @@ def _neg_l2_distance(X: list[float], Y: list[list[float]]) -> list[float]:
similarities = []
for y in Y:
similarity = sum((a - b) ** 2 for a, b in zip(X, y)) ** 0.5
similarity = sum((a - b) ** 2 for a, b in zip(X, y, strict=False)) ** 0.5
similarities.append(-similarity)
return similarities
@@ -861,3 +861,41 @@ def test_store_ttl(store):
# Now has been (TTL_SECONDS-2)*2 > TTL_SECONDS + TTL_SECONDS/2
res = store.search(ns, query="bar", refresh_ttl=False)
assert len(res) == 0
@pytest.mark.parametrize(
"vector_type,distance_type",
[
("vector", "cosine"),
("vector", "inner_product"),
("halfvec", "cosine"),
("halfvec", "inner_product"),
],
)
def test_non_ascii(
request: Any,
fake_embeddings: CharacterEmbeddings,
vector_type: str,
distance_type: str,
) -> None:
"""Test support for non-ascii characters"""
with _create_vector_store(vector_type, distance_type, fake_embeddings) as store:
store.put(("user_123", "memories"), "1", {"text": "这是中文"}) # Chinese
store.put(
("user_123", "memories"), "2", {"text": "これは日本語です"}
) # Japanese
store.put(("user_123", "memories"), "3", {"text": "이건 한국어야"}) # Korean
store.put(("user_123", "memories"), "4", {"text": "Это русский"}) # Russian
store.put(("user_123", "memories"), "5", {"text": "यह रूसी है"}) # Hindi
result1 = store.search(("user_123", "memories"), query="这是中文")
result2 = store.search(("user_123", "memories"), query="これは日本語です")
result3 = store.search(("user_123", "memories"), query="이건 한국어야")
result4 = store.search(("user_123", "memories"), query="Это русский")
result5 = store.search(("user_123", "memories"), query="यह रूसी है")
assert result1[0].key == "1"
assert result2[0].key == "2"
assert result3[0].key == "3"
assert result4[0].key == "4"
assert result5[0].key == "5"
+35 -9
View File
@@ -7,10 +7,6 @@ from uuid import uuid4
import pytest
from langchain_core.runnables import RunnableConfig
from psycopg import Connection
from psycopg.rows import dict_row
from psycopg_pool import ConnectionPool
from langgraph.checkpoint.base import (
EXCLUDED_METADATA_KEYS,
Checkpoint,
@@ -18,8 +14,12 @@ from langgraph.checkpoint.base import (
create_checkpoint,
empty_checkpoint,
)
from langgraph.checkpoint.postgres import PostgresSaver, ShallowPostgresSaver
from langgraph.checkpoint.serde.types import TASKS
from psycopg import Connection
from psycopg.rows import dict_row
from psycopg_pool import ConnectionPool
from langgraph.checkpoint.postgres import PostgresSaver, ShallowPostgresSaver
from tests.conftest import DEFAULT_POSTGRES_URI
@@ -169,13 +169,11 @@ def test_data():
metadata_1: CheckpointMetadata = {
"source": "input",
"step": 2,
"writes": {},
"score": 1,
}
metadata_2: CheckpointMetadata = {
"source": "loop",
"step": 1,
"writes": {"foo": "bar"},
"score": None,
}
metadata_3: CheckpointMetadata = {}
@@ -202,7 +200,6 @@ def test_combined_metadata(saver_name: str, test_data) -> None:
metadata: CheckpointMetadata = {
"source": "loop",
"step": 1,
"writes": {"foo": "bar"},
"score": None,
}
saver.put(config, chkpnt, metadata, {})
@@ -228,7 +225,6 @@ def test_search(saver_name: str, test_data) -> None:
query_1 = {"source": "input"} # search by 1 key
query_2 = {
"step": 1,
"writes": {"foo": "bar"},
} # search by multiple keys
query_3: dict[str, Any] = {} # search by no keys, return all checkpoints
query_4 = {"source": "update", "step": 1} # no match
@@ -332,3 +328,33 @@ def test_pending_sends_migration(saver_name: str) -> None:
TASKS: ["send-1", "send-2", "send-3"]
}
assert TASKS in search_results[0].checkpoint["channel_versions"]
@pytest.mark.parametrize("saver_name", ["base", "pool", "pipe"])
def test_get_checkpoint_no_channel_values(
monkeypatch, saver_name: str, test_data
) -> None:
"""Backwards compatibility test that verifies a checkpoint with no channel_values key can be retrieved without throwing an error."""
with _saver(saver_name) as saver:
config = {
"configurable": {
"thread_id": "thread-2",
"checkpoint_ns": "",
"__super_private_key": "super_private_value",
},
}
chkpnt: Checkpoint = create_checkpoint(empty_checkpoint(), {}, 1)
saver.put(config, chkpnt, {}, {})
load_checkpoint_tuple = saver._load_checkpoint_tuple
def patched_load_checkpoint_tuple(value):
value["checkpoint"].pop("channel_values", None)
return load_checkpoint_tuple(value)
monkeypatch.setattr(
saver, "_load_checkpoint_tuple", patched_load_checkpoint_tuple
)
checkpoint = saver.get_tuple(config)
assert checkpoint.checkpoint["channel_values"] == {}
+622 -650
View File
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,6 @@
from __future__ import annotations
import json
import random
import sqlite3
import threading
@@ -8,7 +9,6 @@ from contextlib import closing, contextmanager
from typing import Any, cast
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import (
WRITES_IDX_MAP,
BaseCheckpointSaver,
@@ -21,6 +21,7 @@ from langgraph.checkpoint.base import (
get_checkpoint_metadata,
)
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
from langgraph.checkpoint.sqlite.utils import search_where
_AIO_ERROR_MSG = (
@@ -184,7 +185,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
"""Get a checkpoint tuple from the database.
This method retrieves a checkpoint tuple from the SQLite database based on the
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
for the given thread ID is retrieved.
@@ -192,7 +193,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
config: The config to use for retrieving the checkpoint.
Returns:
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
Examples:
@@ -265,9 +266,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
self.serde.loads_typed((type, checkpoint)),
cast(
CheckpointMetadata,
self.jsonplus_serde.loads(metadata)
if metadata is not None
else {},
json.loads(metadata) if metadata is not None else {},
),
(
{
@@ -301,12 +300,12 @@ class SqliteSaver(BaseCheckpointSaver[str]):
Args:
config: The config to use for listing the checkpoints.
filter: Additional filtering criteria for metadata. Defaults to None.
before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
limit: The maximum number of checkpoints to return. Defaults to None.
filter: Additional filtering criteria for metadata.
before: If provided, only checkpoints before the specified checkpoint ID are returned.
limit: The maximum number of checkpoints to return.
Yields:
Iterator[CheckpointTuple]: An iterator of checkpoint tuples.
An iterator of checkpoint tuples.
Examples:
>>> from langgraph.checkpoint.sqlite import SqliteSaver
@@ -358,9 +357,7 @@ class SqliteSaver(BaseCheckpointSaver[str]):
self.serde.loads_typed((type, checkpoint)),
cast(
CheckpointMetadata,
self.jsonplus_serde.loads(metadata)
if metadata is not None
else {},
json.loads(metadata) if metadata is not None else {},
),
(
{
@@ -413,9 +410,9 @@ class SqliteSaver(BaseCheckpointSaver[str]):
thread_id = config["configurable"]["thread_id"]
checkpoint_ns = config["configurable"]["checkpoint_ns"]
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
serialized_metadata = self.jsonplus_serde.dumps(
get_checkpoint_metadata(config, metadata)
)
serialized_metadata = json.dumps(
get_checkpoint_metadata(config, metadata), ensure_ascii=False
).encode("utf-8", "ignore")
with self.cursor() as cur:
cur.execute(
"INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
@@ -1,14 +1,14 @@
from __future__ import annotations
import asyncio
import json
import random
from collections.abc import AsyncIterator, Iterator, Sequence
from collections.abc import AsyncIterator, Callable, Iterator, Sequence
from contextlib import asynccontextmanager
from typing import Any, Callable, TypeVar, cast
from typing import Any, TypeVar, cast
import aiosqlite
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import (
WRITES_IDX_MAP,
BaseCheckpointSaver,
@@ -21,6 +21,7 @@ from langgraph.checkpoint.base import (
get_checkpoint_metadata,
)
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
from langgraph.checkpoint.sqlite.utils import search_where
T = TypeVar("T", bound=Callable)
@@ -139,7 +140,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
"""Get a checkpoint tuple from the database.
This method retrieves a checkpoint tuple from the SQLite database based on the
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
for the given thread ID is retrieved.
@@ -147,7 +148,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
config: The config to use for retrieving the checkpoint.
Returns:
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
"""
try:
# check if we are in the main thread, only bg threads can block
@@ -181,11 +182,11 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
Args:
config: Base configuration for filtering checkpoints.
filter: Additional filtering criteria for metadata.
before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
before: If provided, only checkpoints before the specified checkpoint ID are returned.
limit: Maximum number of checkpoints to return.
Yields:
Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
An iterator of matching checkpoint tuples.
"""
try:
# check if we are in the main thread, only bg threads can block
@@ -316,7 +317,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
"""Get a checkpoint tuple from the database asynchronously.
This method retrieves a checkpoint tuple from the SQLite database based on the
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint
for the given thread ID is retrieved.
@@ -324,7 +325,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
config: The config to use for retrieving the checkpoint.
Returns:
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
"""
await self.setup()
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
@@ -377,9 +378,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
self.serde.loads_typed((type, checkpoint)),
cast(
CheckpointMetadata,
self.jsonplus_serde.loads(metadata)
if metadata is not None
else {},
(json.loads(metadata) if metadata is not None else {}),
),
(
{
@@ -414,11 +413,11 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
Args:
config: Base configuration for filtering checkpoints.
filter: Additional filtering criteria for metadata.
before: If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None.
before: If provided, only checkpoints before the specified checkpoint ID are returned.
limit: Maximum number of checkpoints to return.
Yields:
AsyncIterator[CheckpointTuple]: An asynchronous iterator of matching checkpoint tuples.
An asynchronous iterator of matching checkpoint tuples.
"""
await self.setup()
where, params = search_where(config, filter, before)
@@ -457,9 +456,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
self.serde.loads_typed((type, checkpoint)),
cast(
CheckpointMetadata,
self.jsonplus_serde.loads(metadata)
if metadata is not None
else {},
(json.loads(metadata) if metadata is not None else {}),
),
(
{
@@ -503,9 +500,9 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
thread_id = config["configurable"]["thread_id"]
checkpoint_ns = config["configurable"]["checkpoint_ns"]
type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
serialized_metadata = self.jsonplus_serde.dumps(
get_checkpoint_metadata(config, metadata)
)
serialized_metadata = json.dumps(
get_checkpoint_metadata(config, metadata), ensure_ascii=False
).encode("utf-8", "ignore")
async with (
self.lock,
self.conn.execute(
@@ -5,7 +5,6 @@ from collections.abc import Sequence
from typing import Any
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import get_checkpoint_id
@@ -3,15 +3,14 @@ from __future__ import annotations
import asyncio
import logging
from collections import defaultdict
from collections.abc import AsyncIterator, Iterable, Sequence
from collections.abc import AsyncIterator, Callable, Iterable, Sequence
from contextlib import asynccontextmanager
from types import TracebackType
from typing import Any, Callable, cast
from typing import Any, cast
import aiosqlite
import orjson
import sqlite_vec # type: ignore[import-untyped]
from langgraph.store.base import (
GetOp,
ListNamespacesOp,
@@ -22,6 +21,7 @@ from langgraph.store.base import (
TTLConfig,
)
from langgraph.store.base.batch import AsyncBatchedBaseStore
from langgraph.store.sqlite.base import (
_PLACEHOLDER,
BaseSqliteStore,
@@ -303,7 +303,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
Args:
timeout: Maximum time to wait for the task to stop, in seconds.
If None, wait indefinitely.
If `None`, wait indefinitely.
Returns:
bool: True if the task was successfully stopped or wasn't running,
@@ -484,7 +484,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
# Convert vectors to SQLite-friendly format
vector_params = []
for (ns, k, pathname, _), vector in zip(txt_params, vectors):
for (ns, k, pathname, _), vector in zip(txt_params, vectors, strict=False):
vector_params.extend(
[ns, k, pathname, sqlite_vec.serialize_float32(vector)]
)
@@ -507,7 +507,9 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
results: List to store results in.
cur: Database cursor.
"""
queries, embedding_requests = self._prepare_batch_search_queries(search_ops)
prepared_queries, embedding_requests = self._prepare_batch_search_queries(
search_ops
)
# Setup dot_product function if it doesn't exist
if embedding_requests and self.embeddings:
@@ -515,23 +517,62 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
[query for _, query in embedding_requests]
)
for (idx, _), embedding in zip(embedding_requests, vectors):
_params_list: list = queries[idx][1]
for i, param in enumerate(_params_list):
if param is _PLACEHOLDER:
_params_list[i] = sqlite_vec.serialize_float32(embedding)
for (embed_req_idx, _), embedding in zip(
embedding_requests, vectors, strict=False
):
# Find the corresponding query in prepared_queries
# The embed_req_idx is the original index in search_ops, which should map to prepared_queries
if embed_req_idx < len(prepared_queries):
_params_list: list = prepared_queries[embed_req_idx][1]
for i, param in enumerate(_params_list):
if param is _PLACEHOLDER:
_params_list[i] = sqlite_vec.serialize_float32(embedding)
else:
logger.warning(
f"Embedding request index {embed_req_idx} out of bounds for prepared_queries."
)
for (idx, _), (query, params) in zip(search_ops, queries):
for (original_op_idx, _), (query, params, needs_refresh) in zip(
search_ops, prepared_queries, strict=False
):
await cur.execute(query, params)
rows = await cur.fetchall()
if "score" in query:
if needs_refresh and rows and self.ttl_config:
keys_to_refresh = []
for row_data in rows:
# Assuming row_data[0] is prefix (text), row_data[1] is key (text)
# These are raw text values directly from the DB.
keys_to_refresh.append((row_data[0], row_data[1]))
if keys_to_refresh:
updates_by_prefix = defaultdict(list)
for prefix_text, key_text in keys_to_refresh:
updates_by_prefix[prefix_text].append(key_text)
for prefix_text, key_list in updates_by_prefix.items():
placeholders = ",".join(["?"] * len(key_list))
update_query = f"""
UPDATE store
SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
WHERE prefix = ? AND key IN ({placeholders}) AND ttl_minutes IS NOT NULL
"""
update_params = (prefix_text, *key_list)
try:
await cur.execute(update_query, update_params)
except Exception as e:
logger.error(
f"Error during TTL refresh update for search: {e}"
)
# Process rows into items
if "score" in query: # Vector search query
items = [
_row_to_search_item(
_decode_ns_text(row[0]),
_decode_ns_text(row[0]), # prefix
{
"key": row[1],
"value": row[2],
"key": row[1], # key
"value": row[2], # value
"created_at": row[3],
"updated_at": row[4],
"expires_at": row[5] if len(row) > 5 else None,
@@ -545,10 +586,10 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
else: # Regular search query
items = [
_row_to_search_item(
_decode_ns_text(row[0]),
_decode_ns_text(row[0]), # prefix
{
"key": row[1],
"value": row[2],
"key": row[1], # key
"value": row[2], # value
"created_at": row[3],
"updated_at": row[4],
"expires_at": row[5] if len(row) > 5 else None,
@@ -559,7 +600,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
for row in rows
]
results[idx] = items
results[original_op_idx] = items
async def _batch_list_namespaces_ops(
self,
@@ -575,7 +616,7 @@ class AsyncSqliteStore(AsyncBatchedBaseStore, BaseSqliteStore):
cur: Database cursor.
"""
queries = self._get_batch_list_namespaces_queries(list_ops)
for (query, params), (idx, _) in zip(queries, list_ops):
for (query, params), (idx, _) in zip(queries, list_ops, strict=False):
await cur.execute(query, params)
rows = await cur.fetchall()
@@ -7,13 +7,12 @@ import re
import sqlite3
import threading
from collections import defaultdict
from collections.abc import Iterable, Iterator, Sequence
from collections.abc import Callable, Iterable, Iterator, Sequence
from contextlib import contextmanager
from typing import Any, Callable, Literal, NamedTuple, cast
from typing import Any, Literal, NamedTuple, cast
import orjson
import sqlite_vec # type: ignore[import-untyped]
from langgraph.store.base import (
BaseStore,
GetOp,
@@ -233,7 +232,7 @@ class BaseSqliteStore:
results = []
for namespace, items in namespace_groups.items():
_, keys = zip(*items)
_, keys = zip(*items, strict=False)
this_refresh_ttls = refresh_ttls[namespace]
refresh_ttl_any = any(this_refresh_ttls)
@@ -372,13 +371,15 @@ class BaseSqliteStore:
def _prepare_batch_search_queries(
self, search_ops: Sequence[tuple[int, SearchOp]]
) -> tuple[
list[tuple[str, list[None | str | list[float]]]], # queries, params
list[
tuple[str, list[None | str | list[float]], bool]
], # queries, params, needs_refresh
list[tuple[int, str]], # idx, query_text pairs to embed
]:
"""
Build per-SearchOp SQL queries (with optional TTL refresh) plus embedding requests.
Build per-SearchOp SQL queries (with optional TTL refresh flag) plus embedding requests.
Returns:
- queries: list of (SQL, param_list)
- queries: list of (SQL, param_list, needs_ttl_refresh_flag)
- embedding_requests: list of (original_index_in_search_ops, text_query)
"""
queries = []
@@ -519,30 +520,18 @@ class BaseSqliteStore:
logger.debug(f"Search query: {base_query}")
logger.debug(f"Search params: {params}")
# Handle TTL refresh if requested
if (
# Determine if TTL refresh is needed
needs_ttl_refresh = bool(
op.refresh_ttl
and self.ttl_config
and self.ttl_config.get("refresh_on_read", False)
):
final_sql = f"""
WITH search_results AS (
{base_query}
),
updated AS (
UPDATE store
SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
WHERE (prefix, key) IN (SELECT prefix, key FROM search_results)
AND ttl_minutes IS NOT NULL
)
SELECT * FROM search_results
"""
final_params = params[:] # copy params
else:
final_sql = base_query
final_params = params
)
queries.append((final_sql, final_params))
# The base_query is now the final_sql, and we pass the refresh flag
final_sql = base_query
final_params = params
queries.append((final_sql, final_params, needs_ttl_refresh))
return queries, embedding_requests
@@ -840,7 +829,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
results = []
for namespace, items in namespace_groups.items():
_, keys = zip(*items)
_, keys = zip(*items, strict=False)
this_refresh_ttls = refresh_ttls[namespace]
refresh_ttl_any = any(this_refresh_ttls)
@@ -1167,7 +1156,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
Args:
timeout: Maximum time to wait for the thread to stop, in seconds.
If None, wait indefinitely.
If `None`, wait indefinitely.
Returns:
bool: True if the thread was successfully stopped or wasn't running,
@@ -1315,7 +1304,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
# Convert vectors to SQLite-friendly format
vector_params = []
for (ns, k, pathname, _), vector in zip(txt_params, vectors):
for (ns, k, pathname, _), vector in zip(txt_params, vectors, strict=False):
vector_params.extend(
[ns, k, pathname, sqlite_vec.serialize_float32(vector)]
)
@@ -1331,7 +1320,9 @@ class SqliteStore(BaseSqliteStore, BaseStore):
results: list[Result],
cur: sqlite3.Cursor,
) -> None:
queries, embedding_requests = self._prepare_batch_search_queries(search_ops)
prepared_queries, embedding_requests = self._prepare_batch_search_queries(
search_ops
)
# Setup similarity functions if they don't exist
if embedding_requests and self.embeddings:
@@ -1341,16 +1332,50 @@ class SqliteStore(BaseSqliteStore, BaseStore):
)
# Replace placeholders with actual embeddings
for (idx, _), embedding in zip(embedding_requests, embeddings):
_params_list: list = queries[idx][1]
for i, param in enumerate(_params_list):
if param is _PLACEHOLDER:
_params_list[i] = sqlite_vec.serialize_float32(embedding)
for (embed_req_idx, _), embedding in zip(
embedding_requests, embeddings, strict=False
):
if embed_req_idx < len(prepared_queries):
_params_list: list = prepared_queries[embed_req_idx][1]
for i, param in enumerate(_params_list):
if param is _PLACEHOLDER:
_params_list[i] = sqlite_vec.serialize_float32(embedding)
else:
logger.warning(
f"Embedding request index {embed_req_idx} out of bounds for prepared_queries."
)
for (idx, _), (query, params) in zip(search_ops, queries):
for (original_op_idx, _), (query, params, needs_refresh) in zip(
search_ops, prepared_queries, strict=False
):
cur.execute(query, params)
rows = cur.fetchall()
if needs_refresh and rows and self.ttl_config:
keys_to_refresh = []
for row_data in rows:
keys_to_refresh.append((row_data[0], row_data[1]))
if keys_to_refresh:
updates_by_prefix = defaultdict(list)
for prefix_text, key_text in keys_to_refresh:
updates_by_prefix[prefix_text].append(key_text)
for prefix_text, key_list in updates_by_prefix.items():
placeholders = ",".join(["?"] * len(key_list))
update_query = f"""
UPDATE store
SET expires_at = DATETIME(CURRENT_TIMESTAMP, '+' || ttl_minutes || ' minutes')
WHERE prefix = ? AND key IN ({placeholders}) AND ttl_minutes IS NOT NULL
"""
update_params = (prefix_text, *key_list)
try:
cur.execute(update_query, update_params)
except Exception as e:
logger.error(
f"Error during TTL refresh update for search: {e}"
)
if "score" in query: # Vector search query
items = [
_row_to_search_item(
@@ -1385,7 +1410,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
for row in rows
]
results[idx] = items
results[original_op_idx] = items
def _batch_list_namespaces_ops(
self,
@@ -1394,7 +1419,7 @@ class SqliteStore(BaseSqliteStore, BaseStore):
cur: sqlite3.Cursor,
) -> None:
queries = self._get_batch_list_namespaces_queries(list_ops)
for (query, params), (idx, _) in zip(queries, list_ops):
for (query, params), (idx, _) in zip(queries, list_ops, strict=False):
cur.execute(query, params)
results[idx] = [_decode_ns_text(row[0]) for row in cur.fetchall()]
+15 -7
View File
@@ -4,15 +4,15 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-checkpoint-sqlite"
version = "2.0.11"
version = "3.0.0"
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
authors = []
requires-python = ">=3.9"
requires-python = ">=3.10"
readme = "README.md"
license = "MIT"
license-files = ['LICENSE']
dependencies = [
"langgraph-checkpoint>=2.0.21,<3.0.0",
"langgraph-checkpoint>=3,<4.0.0",
"aiosqlite>=0.20",
"sqlite-vec>=0.1.6",
]
@@ -21,17 +21,23 @@ dependencies = [
Repository = "https://www.github.com/langchain-ai/langgraph"
[dependency-groups]
dev = [
"ruff",
"codespell",
test = [
"pytest",
"pytest-asyncio",
"pytest-mock",
"pytest-watcher",
"mypy",
"langgraph-checkpoint",
"pytest-retry>=1.7.0",
]
lint = [
"ruff",
"codespell",
"mypy",
]
dev = [
{include-group = "test"},
{include-group = "lint"},
]
[tool.uv]
default-groups = ['dev']
@@ -53,8 +59,10 @@ lint.select = [
"UP", # pyupgrade
"B", # flake8-bugbear
"I", # isort
"UP", # pyupgrade
]
lint.ignore = ["E501", "B008"]
target-version = "py310"
[tool.pytest-watcher]
now = true
@@ -2,13 +2,13 @@ from typing import Any
import pytest
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import (
Checkpoint,
CheckpointMetadata,
create_checkpoint,
empty_checkpoint,
)
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
@@ -5,10 +5,9 @@ import tempfile
import uuid
from collections.abc import AsyncIterator, Generator, Iterable
from contextlib import asynccontextmanager
from typing import Optional, Union, cast
from typing import cast
import pytest
from langgraph.store.base import (
GetOp,
Item,
@@ -16,6 +15,7 @@ from langgraph.store.base import (
PutOp,
SearchOp,
)
from langgraph.store.sqlite import AsyncSqliteStore
from langgraph.store.sqlite.base import SqliteIndexConfig
from tests.test_store import CharacterEmbeddings
@@ -51,7 +51,7 @@ def fake_embeddings() -> CharacterEmbeddings:
async def create_vector_store(
fake_embeddings: CharacterEmbeddings,
conn_string: str = ":memory:",
text_fields: Optional[list[str]] = None,
text_fields: list[str] | None = None,
) -> AsyncIterator[AsyncSqliteStore]:
"""Create an AsyncSqliteStore with vector search capabilities."""
index_config: SqliteIndexConfig = {
@@ -168,7 +168,7 @@ async def test_abatch_order(store: AsyncSqliteStore) -> None:
]
results = await store.abatch(
cast(Iterable[Union[GetOp, PutOp, SearchOp, ListNamespacesOp]], ops)
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops)
)
assert len(results) == 5
assert isinstance(results[0], Item)
@@ -193,7 +193,7 @@ async def test_abatch_order(store: AsyncSqliteStore) -> None:
]
results_reordered = await store.abatch(
cast(Iterable[Union[GetOp, PutOp, SearchOp, ListNamespacesOp]], ops_reordered)
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops_reordered)
)
assert len(results_reordered) == 5
assert isinstance(results_reordered[0], list)
@@ -681,7 +681,7 @@ async def test_search_items(
fake_embeddings, text_fields=["key0", "key1", "key3"]
) as store:
# Insert test data
for ns, item in zip(test_namespaces, test_items):
for ns, item in zip(test_namespaces, test_items, strict=False):
key = f"item_{ns[-1]}"
await store.aput(ns, key, item)
+12 -2
View File
@@ -2,13 +2,13 @@ from typing import Any, cast
import pytest
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import (
Checkpoint,
CheckpointMetadata,
create_checkpoint,
empty_checkpoint,
)
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.checkpoint.sqlite.utils import _metadata_predicate, search_where
@@ -116,7 +116,17 @@ class TestSqliteSaver:
search_results_5[1].config["configurable"]["checkpoint_ns"],
} == {"", "inner"}
# TODO: test before and limit params
# search with before param
search_results_6 = list(saver.list(None, before=search_results_5[1].config))
assert len(search_results_6) == 1
assert search_results_6[0].config["configurable"]["thread_id"] == "thread-1"
# search with limit param
search_results_7 = list(
saver.list({"configurable": {"thread_id": "thread-2"}}, limit=1)
)
assert len(search_results_7) == 1
assert search_results_7[0].config["configurable"]["thread_id"] == "thread-2"
def test_search_where(self) -> None:
# call method / assertions
+36 -8
View File
@@ -5,11 +5,10 @@ import tempfile
import uuid
from collections.abc import Generator, Iterable
from contextlib import contextmanager
from typing import Any, Literal, Optional, Union, cast
from typing import Any, Literal, cast
import pytest
from langchain_core.embeddings import Embeddings
from langgraph.store.base import (
GetOp,
Item,
@@ -18,6 +17,7 @@ from langgraph.store.base import (
PutOp,
SearchOp,
)
from langgraph.store.sqlite import SqliteStore
from langgraph.store.sqlite.base import SqliteIndexConfig
@@ -110,7 +110,7 @@ VECTOR_TYPES = ["cosine"] # SQLite only supports cosine similarity
@contextmanager
def create_vector_store(
fake_embeddings: CharacterEmbeddings,
text_fields: Optional[list[str]] = None,
text_fields: list[str] | None = None,
distance_type: str = "cosine",
conn_type: Literal["memory", "file"] = "memory",
) -> Generator[SqliteStore, None, None]:
@@ -153,7 +153,7 @@ def test_batch_order(store: SqliteStore) -> None:
]
results = store.batch(
cast(Iterable[Union[GetOp, PutOp, SearchOp, ListNamespacesOp]], ops)
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops)
)
assert len(results) == 5
assert isinstance(results[0], Item)
@@ -182,7 +182,7 @@ def test_batch_order(store: SqliteStore) -> None:
]
results_reordered = store.batch(
cast(Iterable[Union[GetOp, PutOp, SearchOp, ListNamespacesOp]], ops_reordered)
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops_reordered)
)
assert len(results_reordered) == 5
assert isinstance(results_reordered[0], list)
@@ -301,7 +301,7 @@ def test_batch_list_namespaces_ops(store: SqliteStore) -> None:
]
results = store.batch(
cast(Iterable[Union[GetOp, PutOp, SearchOp, ListNamespacesOp]], ops)
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops)
)
assert len(results) == 3
@@ -778,7 +778,7 @@ def _cosine_similarity(X: list[float], Y: list[list[float]]) -> list[float]:
similarities = []
for y in Y:
dot_product = sum(a * b for a, b in zip(X, y))
dot_product = sum(a * b for a, b in zip(X, y, strict=False))
norm1 = sum(a * a for a in X) ** 0.5
norm2 = sum(a * a for a in y) ** 0.5
similarity = dot_product / (norm1 * norm2) if norm1 > 0 and norm2 > 0 else 0.0
@@ -1011,7 +1011,7 @@ def test_search_items(
fake_embeddings, text_fields=["key0", "key1", "key3"]
) as store:
# Insert test data
for ns, item in zip(test_namespaces, test_items):
for ns, item in zip(test_namespaces, test_items, strict=False):
key = f"item_{ns[-1]}"
store.put(ns, key, item)
@@ -1067,3 +1067,31 @@ def test_sql_injection_vulnerability(store: SqliteStore) -> None:
with pytest.raises(ValueError, match="Invalid filter key"):
store.search(("docs",), filter={malicious_key: "dummy"})
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
def test_non_ascii(
fake_embeddings: CharacterEmbeddings,
distance_type: str,
) -> None:
"""Test support for non-ascii characters"""
with create_vector_store(fake_embeddings, distance_type=distance_type) as store:
store.put(("user_123", "memories"), "1", {"text": "这是中文"}) # Chinese
store.put(
("user_123", "memories"), "2", {"text": "これは日本語です"}
) # Japanese
store.put(("user_123", "memories"), "3", {"text": "이건 한국어야"}) # Korean
store.put(("user_123", "memories"), "4", {"text": "Это русский"}) # Russian
store.put(("user_123", "memories"), "5", {"text": "यह रूसी है"}) # Hindi
result1 = store.search(("user_123", "memories"), query="这是中文")
result2 = store.search(("user_123", "memories"), query="これは日本語です")
result3 = store.search(("user_123", "memories"), query="이건 한국어야")
result4 = store.search(("user_123", "memories"), query="Это русский")
result5 = store.search(("user_123", "memories"), query="यह रूसी है")
assert result1[0].key == "1"
assert result2[0].key == "2"
assert result3[0].key == "3"
assert result4[0].key == "4"
assert result5[0].key == "5"
+76 -2
View File
@@ -7,6 +7,7 @@ import time
from collections.abc import Generator
import pytest
from langgraph.store.base import TTLConfig
from langgraph.store.sqlite import SqliteStore
from langgraph.store.sqlite.aio import AsyncSqliteStore
@@ -93,9 +94,13 @@ def test_ttl_sweeper(temp_db_file: str) -> None:
ttl_seconds = 2
ttl_minutes = ttl_seconds / 60
ttl_config: TTLConfig = {
"default_ttl": ttl_minutes,
"sweep_interval_minutes": ttl_minutes / 2,
}
with SqliteStore.from_conn_string(
temp_db_file,
ttl={"default_ttl": ttl_minutes, "sweep_interval_minutes": ttl_minutes / 2},
ttl=ttl_config,
) as store:
store.setup()
@@ -298,9 +303,14 @@ async def test_async_ttl_sweeper(temp_db_file: str) -> None:
ttl_seconds = 2
ttl_minutes = ttl_seconds / 60
ttl_config: TTLConfig = {
"default_ttl": ttl_minutes,
"sweep_interval_minutes": ttl_minutes / 2,
}
async with AsyncSqliteStore.from_conn_string(
temp_db_file,
ttl={"default_ttl": ttl_minutes, "sweep_interval_minutes": ttl_minutes / 2},
ttl=ttl_config,
) as store:
await store.setup()
@@ -353,3 +363,67 @@ async def test_async_search_with_ttl(temp_db_file: str) -> None:
# Search after expiration
results = await store.asearch(("test",), filter={"value": "apple"})
assert len(results) == 0
@pytest.mark.asyncio
@pytest.mark.flaky(retries=3)
async def test_async_asearch_refresh_ttl(temp_db_file: str) -> None:
"""Test TTL refresh on asearch with async API."""
ttl_seconds = 4.0 # Increased TTL for less sensitivity to timing
ttl_minutes = ttl_seconds / 60.0
async with AsyncSqliteStore.from_conn_string(
temp_db_file, ttl={"default_ttl": ttl_minutes, "refresh_on_read": True}
) as store:
await store.setup()
namespace = ("docs", "user1")
# t=0: items put, expire at t=4.0s
await store.aput(namespace, "item1", {"text": "content1", "id": 1})
await store.aput(namespace, "item2", {"text": "content2", "id": 2})
# t=3.0s: (after sleep ttl_seconds * 0.75 = 3s)
await asyncio.sleep(ttl_seconds * 0.75)
# Perform asearch with refresh_ttl=True for item1.
# item1's TTL should be refreshed. New expiry: t=3.0s + 4.0s = t=7.0s.
# item2's TTL is not affected. Expires at t=4.0s.
searched_items = await store.asearch(
namespace, filter={"id": 1}, refresh_ttl=True
)
assert len(searched_items) == 1
assert searched_items[0].key == "item1"
# t=5.0s: (after sleep ttl_seconds * 0.5 = 2s more. Total elapsed: 3s + 2s = 5s)
await asyncio.sleep(ttl_seconds * 0.5)
# At this point:
# - item1 (refreshed by asearch) should expire at t=7.0s. Should be ALIVE.
# - item2 (original TTL) should have expired at t=4.0s. Should be GONE after sweep.
await store.sweep_ttl()
# Check item1 (should exist due to asearch refresh)
item1_check1 = await store.aget(namespace, "item1", refresh_ttl=False)
assert item1_check1 is not None, (
"Item1 should exist after asearch refresh and first sweep"
)
assert item1_check1.value["text"] == "content1"
# Check item2 (should be gone)
item2_check1 = await store.aget(namespace, "item2", refresh_ttl=False)
assert item2_check1 is None, (
"Item2 should be gone after its original TTL expired"
)
# t=7.5s: (after sleep ttl_seconds * 0.625 = 2.5s more. Total elapsed: 5s + 2.5s = 7.5s)
await asyncio.sleep(ttl_seconds * 0.625)
# At this point:
# - item1 (refreshed by asearch, expired at t=7.0s) should be GONE after sweep.
await store.sweep_ttl()
# Check item1 again (should be gone now)
item1_final_check = await store.aget(namespace, "item1", refresh_ttl=False)
assert item1_final_check is None, (
"Item1 should be gone after its refreshed TTL expired"
)
+571 -591
View File
File diff suppressed because it is too large Load Diff
@@ -8,7 +8,6 @@ from typing import ( # noqa: UP035
NamedTuple,
TypedDict,
TypeVar,
Union,
)
from langchain_core.runnables import RunnableConfig
@@ -35,17 +34,17 @@ class CheckpointMetadata(TypedDict, total=False):
source: Literal["input", "loop", "update", "fork"]
"""The source of the checkpoint.
- "input": The checkpoint was created from an input to invoke/stream/batch.
- "loop": The checkpoint was created from inside the pregel loop.
- "update": The checkpoint was created from a manual state update.
- "fork": The checkpoint was created as a copy of another checkpoint.
- `"input"`: The checkpoint was created from an input to invoke/stream/batch.
- `"loop"`: The checkpoint was created from inside the pregel loop.
- `"update"`: The checkpoint was created from a manual state update.
- `"fork"`: The checkpoint was created as a copy of another checkpoint.
"""
step: int
"""The step number of the checkpoint.
-1 for the first "input" checkpoint.
0 for the first "loop" checkpoint.
... for the nth checkpoint afterwards.
`-1` for the first `"input"` checkpoint.
`0` for the first `"loop"` checkpoint.
`...` for the `nth` checkpoint afterwards.
"""
parents: dict[str, str]
"""The IDs of the parent checkpoints.
@@ -54,7 +53,7 @@ class CheckpointMetadata(TypedDict, total=False):
"""
ChannelVersions = dict[str, Union[str, int, float]]
ChannelVersions = dict[str, str | int | float]
class Checkpoint(TypedDict):
@@ -148,7 +147,7 @@ class BaseCheckpointSaver(Generic[V]):
config: Configuration specifying which checkpoint to retrieve.
Returns:
Optional[Checkpoint]: The requested checkpoint, or None if not found.
The requested checkpoint, or `None` if not found.
"""
if value := self.get_tuple(config):
return value.checkpoint
@@ -160,7 +159,7 @@ class BaseCheckpointSaver(Generic[V]):
config: Configuration specifying which checkpoint to retrieve.
Returns:
Optional[CheckpointTuple]: The requested checkpoint tuple, or None if not found.
The requested checkpoint tuple, or `None` if not found.
Raises:
NotImplementedError: Implement this method in your custom checkpoint saver.
@@ -184,7 +183,7 @@ class BaseCheckpointSaver(Generic[V]):
limit: Maximum number of checkpoints to return.
Returns:
Iterator[CheckpointTuple]: Iterator of matching checkpoint tuples.
Iterator of matching checkpoint tuples.
Raises:
NotImplementedError: Implement this method in your custom checkpoint saver.
@@ -252,7 +251,7 @@ class BaseCheckpointSaver(Generic[V]):
config: Configuration specifying which checkpoint to retrieve.
Returns:
Optional[Checkpoint]: The requested checkpoint, or None if not found.
The requested checkpoint, or `None` if not found.
"""
if value := await self.aget_tuple(config):
return value.checkpoint
@@ -264,7 +263,7 @@ class BaseCheckpointSaver(Generic[V]):
config: Configuration specifying which checkpoint to retrieve.
Returns:
Optional[CheckpointTuple]: The requested checkpoint tuple, or None if not found.
The requested checkpoint tuple, or `None` if not found.
Raises:
NotImplementedError: Implement this method in your custom checkpoint saver.
@@ -288,7 +287,7 @@ class BaseCheckpointSaver(Generic[V]):
limit: Maximum number of checkpoints to return.
Returns:
AsyncIterator[CheckpointTuple]: Async iterator of matching checkpoint tuples.
Async iterator of matching checkpoint tuples.
Raises:
NotImplementedError: Implement this method in your custom checkpoint saver.
@@ -353,11 +352,11 @@ class BaseCheckpointSaver(Generic[V]):
def get_next_version(self, current: V | None, channel: None) -> V:
"""Generate the next version ID for a channel.
Default is to use integer versions, incrementing by 1. If you override, you can use str/int/float versions,
as long as they are monotonically increasing.
Default is to use integer versions, incrementing by `1`. If you override, you can use `str`/`int`/`float`
versions, as long as they are monotonically increasing.
Args:
current: The current version identifier (int, float, or str).
current: The current version identifier (`int`, `float`, or `str`).
channel: Deprecated argument, kept for backwards compatibility.
Returns:
@@ -404,6 +403,16 @@ def get_checkpoint_metadata(
return metadata
def get_serializable_checkpoint_metadata(
config: RunnableConfig, metadata: CheckpointMetadata
) -> CheckpointMetadata:
"""Get checkpoint metadata in a backwards-compatible manner."""
checkpoint_metadata = get_checkpoint_metadata(config, metadata)
if "writes" in checkpoint_metadata:
checkpoint_metadata.pop("writes")
return checkpoint_metadata
"""
Mapping from error type to error index.
Regular writes just map to their index in the list of writes being saved.
@@ -39,10 +39,10 @@ class InMemorySaver(
Only use `InMemorySaver` for debugging or testing purposes.
For production use cases we recommend installing [langgraph-checkpoint-postgres](https://pypi.org/project/langgraph-checkpoint-postgres/) and using `PostgresSaver` / `AsyncPostgresSaver`.
If you are using the LangGraph Platform, no checkpointer needs to be specified. The correct managed checkpointer will be used automatically.
If you are using LangSmith Deployment, no checkpointer needs to be specified. The correct managed checkpointer will be used automatically.
Args:
serde: The serializer to use for serializing and deserializing checkpoints. Defaults to None.
serde: The serializer to use for serializing and deserializing checkpoints.
Examples:
@@ -133,7 +133,7 @@ class InMemorySaver(
"""Get a checkpoint tuple from the in-memory storage.
This method retrieves a checkpoint tuple from the in-memory storage based on the
provided config. If the config contains a "checkpoint_id" key, the checkpoint with
provided config. If the config contains a `checkpoint_id` key, the checkpoint with
the matching thread ID and timestamp is retrieved. Otherwise, the latest checkpoint
for the given thread ID is retrieved.
@@ -141,7 +141,7 @@ class InMemorySaver(
config: The config to use for retrieving the checkpoint.
Returns:
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
"""
thread_id: str = config["configurable"]["thread_id"]
checkpoint_ns: str = config["configurable"].get("checkpoint_ns", "")
@@ -231,7 +231,7 @@ class InMemorySaver(
limit: Maximum number of checkpoints to return.
Yields:
Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples.
An iterator of matching checkpoint tuples.
"""
thread_ids = (config["configurable"]["thread_id"],) if config else self.storage
config_checkpoint_ns = (
@@ -423,16 +423,16 @@ class InMemorySaver(
del self.blobs[k]
async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
"""Asynchronous version of get_tuple.
"""Asynchronous version of `get_tuple`.
This method is an asynchronous wrapper around get_tuple that runs the synchronous
This method is an asynchronous wrapper around `get_tuple` that runs the synchronous
method in a separate thread using asyncio.
Args:
config: The config to use for retrieving the checkpoint.
Returns:
Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
The retrieved checkpoint tuple, or None if no matching checkpoint was found.
"""
return self.get_tuple(config)
@@ -444,16 +444,16 @@ class InMemorySaver(
before: RunnableConfig | None = None,
limit: int | None = None,
) -> AsyncIterator[CheckpointTuple]:
"""Asynchronous version of list.
"""Asynchronous version of `list`.
This method is an asynchronous wrapper around list that runs the synchronous
This method is an asynchronous wrapper around `list` that runs the synchronous
method in a separate thread using asyncio.
Args:
config: The config to use for listing the checkpoints.
Yields:
AsyncIterator[CheckpointTuple]: An asynchronous iterator of checkpoint tuples.
An asynchronous iterator of checkpoint tuples.
"""
for item in self.list(config, filter=filter, before=before, limit=limit):
yield item
@@ -465,7 +465,7 @@ class InMemorySaver(
metadata: CheckpointMetadata,
new_versions: ChannelVersions,
) -> RunnableConfig:
"""Asynchronous version of put.
"""Asynchronous version of `put`.
Args:
config: The config to associate with the checkpoint.
@@ -485,9 +485,9 @@ class InMemorySaver(
task_id: str,
task_path: str = "",
) -> None:
"""Asynchronous version of put_writes.
"""Asynchronous version of `put_writes`.
This method is an asynchronous wrapper around put_writes that runs the synchronous
This method is an asynchronous wrapper around `put_writes` that runs the synchronous
method in a separate thread using asyncio.
Args:
@@ -1,6 +1,6 @@
from __future__ import annotations
from typing import Any, Protocol
from typing import Any, Protocol, runtime_checkable
class UntypedSerializerProtocol(Protocol):
@@ -11,13 +11,14 @@ class UntypedSerializerProtocol(Protocol):
def loads(self, data: bytes) -> Any: ...
class SerializerProtocol(UntypedSerializerProtocol, Protocol):
@runtime_checkable
class SerializerProtocol(Protocol):
"""Protocol for serialization and deserialization of objects.
- `dumps`: Serialize an object to bytes.
- `dumps_typed`: Serialize an object to a tuple (type, bytes).
- `dumps_typed`: Serialize an object to a tuple `(type, bytes)`.
- `loads`: Deserialize an object from bytes.
- `loads_typed`: Deserialize an object from a tuple (type, bytes).
- `loads_typed`: Deserialize an object from a tuple `(type, bytes)`.
Valid implementations include the `pickle`, `json` and `orjson` modules.
"""
@@ -31,12 +32,6 @@ class SerializerCompat(SerializerProtocol):
def __init__(self, serde: UntypedSerializerProtocol) -> None:
self.serde = serde
def dumps(self, obj: Any) -> bytes:
return self.serde.dumps(obj)
def loads(self, data: bytes) -> Any:
return self.serde.loads(data)
def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
return type(obj).__name__, self.serde.dumps(obj)
@@ -49,7 +44,7 @@ def maybe_add_typed_methods(
) -> SerializerProtocol:
"""Wrap serde old serde implementations in a class with loads_typed and dumps_typed for backwards compatibility."""
if not hasattr(serde, "loads_typed") or not hasattr(serde, "dumps_typed"):
if not isinstance(serde, SerializerProtocol):
return SerializerCompat(serde)
return serde
@@ -14,14 +14,8 @@ class EncryptedSerializer(SerializerProtocol):
self.cipher = cipher
self.serde = serde
def dumps(self, obj: Any) -> bytes:
return self.serde.dumps(obj)
def loads(self, data: bytes) -> Any:
return self.serde.loads(data)
def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
"""Serialize an object to a tuple (type, bytes) and encrypt the bytes."""
"""Serialize an object to a tuple `(type, bytes)` and encrypt the bytes."""
# serialize data
typ, data = self.serde.dumps_typed(obj)
# encrypt data
@@ -45,7 +39,7 @@ class EncryptedSerializer(SerializerProtocol):
def from_pycryptodome_aes(
cls, serde: SerializerProtocol = JsonPlusSerializer(), **kwargs: Any
) -> "EncryptedSerializer":
"""Create an EncryptedSerializer using AES encryption."""
"""Create an `EncryptedSerializer` using AES encryption."""
try:
from Crypto.Cipher import AES # type: ignore
except ImportError:
@@ -4,12 +4,13 @@ import dataclasses
import decimal
import importlib
import json
import logging
import pathlib
import pickle
import re
import sys
from collections import deque
from collections.abc import Sequence
from collections.abc import Callable, Sequence
from datetime import date, datetime, time, timedelta, timezone
from enum import Enum
from inspect import isclass
@@ -21,13 +22,12 @@ from ipaddress import (
IPv6Interface,
IPv6Network,
)
from typing import Any, Callable, cast
from typing import Any, Literal
from uuid import UUID
from zoneinfo import ZoneInfo
import ormsgpack
from langchain_core.load.load import Reviver
from langchain_core.load.serializable import Serializable
from langgraph.checkpoint.serde.base import SerializerProtocol
from langgraph.checkpoint.serde.types import SendProtocol
@@ -35,18 +35,31 @@ from langgraph.store.base import Item
LC_REVIVER = Reviver()
EMPTY_BYTES = b""
logger = logging.getLogger(__name__)
class JsonPlusSerializer(SerializerProtocol):
"""Serializer that uses ormsgpack, with a fallback to extended JSON serializer."""
"""Serializer that uses ormsgpack, with optional fallbacks.
Security note: this serializer is intended for use within the BaseCheckpointSaver
class and called within the Pregel loop. It should not be used on untrusted
python objects. If an attacker can write directly to your checkpoint database,
they may be able to trigger code execution when data is deserialized.
"""
def __init__(
self,
*,
pickle_fallback: bool = False,
allowed_json_modules: Sequence[tuple[str, ...]] | Literal[True] | None = None,
__unpack_ext_hook__: Callable[[int, bytes], Any] | None = None,
) -> None:
self.pickle_fallback = pickle_fallback
self._allowed_modules = (
{mod_and_name for mod_and_name in allowed_json_modules}
if allowed_json_modules and allowed_json_modules is not True
else (allowed_json_modules if allowed_json_modules is True else None)
)
self._unpack_ext_hook = (
__unpack_ext_hook__
if __unpack_ext_hook__ is not None
@@ -74,134 +87,90 @@ class JsonPlusSerializer(SerializerProtocol):
out["kwargs"] = kwargs
return out
def _default(self, obj: Any) -> str | dict[str, Any]:
if isinstance(obj, Serializable):
return cast(dict[str, Any], obj.to_json())
elif hasattr(obj, "model_dump") and callable(obj.model_dump):
return self._encode_constructor_args(
obj.__class__, method=(None, "model_construct"), kwargs=obj.model_dump()
)
elif hasattr(obj, "dict") and callable(obj.dict):
return self._encode_constructor_args(
obj.__class__, method=(None, "construct"), kwargs=obj.dict()
)
elif hasattr(obj, "_asdict") and callable(obj._asdict):
return self._encode_constructor_args(obj.__class__, kwargs=obj._asdict())
elif isinstance(obj, pathlib.Path):
return self._encode_constructor_args(pathlib.Path, args=obj.parts)
elif isinstance(obj, re.Pattern):
return self._encode_constructor_args(
re.compile, args=(obj.pattern, obj.flags)
)
elif isinstance(obj, UUID):
return self._encode_constructor_args(UUID, args=(obj.hex,))
elif isinstance(obj, decimal.Decimal):
return self._encode_constructor_args(decimal.Decimal, args=(str(obj),))
elif isinstance(obj, (set, frozenset, deque)):
return self._encode_constructor_args(type(obj), args=(tuple(obj),))
elif isinstance(obj, (IPv4Address, IPv4Interface, IPv4Network)):
return self._encode_constructor_args(obj.__class__, args=(str(obj),))
elif isinstance(obj, (IPv6Address, IPv6Interface, IPv6Network)):
return self._encode_constructor_args(obj.__class__, args=(str(obj),))
elif isinstance(obj, datetime):
return self._encode_constructor_args(
datetime, method="fromisoformat", args=(obj.isoformat(),)
)
elif isinstance(obj, timezone):
return self._encode_constructor_args(
timezone,
args=obj.__getinitargs__(), # type: ignore[attr-defined]
)
elif isinstance(obj, ZoneInfo):
return self._encode_constructor_args(ZoneInfo, args=(obj.key,))
elif isinstance(obj, timedelta):
return self._encode_constructor_args(
timedelta, args=(obj.days, obj.seconds, obj.microseconds)
)
elif isinstance(obj, date):
return self._encode_constructor_args(
date, args=(obj.year, obj.month, obj.day)
)
elif isinstance(obj, time):
return self._encode_constructor_args(
time,
args=(obj.hour, obj.minute, obj.second, obj.microsecond, obj.tzinfo),
kwargs={"fold": obj.fold},
)
elif dataclasses.is_dataclass(obj):
return self._encode_constructor_args(
obj.__class__,
kwargs={
field.name: getattr(obj, field.name)
for field in dataclasses.fields(obj)
},
)
elif isinstance(obj, Enum):
return self._encode_constructor_args(obj.__class__, args=(obj.value,))
elif isinstance(obj, SendProtocol):
return self._encode_constructor_args(
obj.__class__, kwargs={"node": obj.node, "arg": obj.arg}
)
elif isinstance(obj, (bytes, bytearray)):
return self._encode_constructor_args(
obj.__class__, method="fromhex", args=(obj.hex(),)
)
elif isinstance(obj, BaseException):
return repr(obj)
else:
raise TypeError(
f"Object of type {obj.__class__.__name__} is not JSON serializable"
)
def _reviver(self, value: dict[str, Any]) -> Any:
if (
if self._allowed_modules and (
value.get("lc", None) == 2
and value.get("type", None) == "constructor"
and value.get("id", None) is not None
):
try:
# Get module and class name
[*module, name] = value["id"]
# Import module
mod = importlib.import_module(".".join(module))
# Import class
cls = getattr(mod, name)
# Instantiate class
method = value.get("method")
if isinstance(method, str):
methods = [getattr(cls, method)]
elif isinstance(method, list):
methods = [
cls if method is None else getattr(cls, method)
for method in method
]
else:
methods = [cls]
args = value.get("args")
kwargs = value.get("kwargs")
for method in methods:
try:
if isclass(method) and issubclass(method, BaseException):
return None
if args and kwargs:
return method(*args, **kwargs)
elif args:
return method(*args)
elif kwargs:
return method(**kwargs)
else:
return method()
except Exception:
continue
except Exception:
return None
return self._revive_lc2(value)
except InvalidModuleError as e:
logger.warning(
"Object %s is not in the deserialization allowlist.\n%s",
value["id"],
e.message,
)
return LC_REVIVER(value)
def dumps(self, obj: Any) -> bytes:
return json.dumps(obj, default=self._default, ensure_ascii=False).encode(
"utf-8", "ignore"
def _revive_lc2(self, value: dict[str, Any]) -> Any:
self._check_allowed_modules(value)
[*module, name] = value["id"]
try:
mod = importlib.import_module(".".join(module))
cls = getattr(mod, name)
method = value.get("method")
if isinstance(method, str):
methods = [getattr(cls, method)]
elif isinstance(method, list):
methods = [cls if m is None else getattr(cls, m) for m in method]
else:
methods = [cls]
args = value.get("args")
kwargs = value.get("kwargs")
for method in methods:
try:
if isclass(method) and issubclass(method, BaseException):
return None
if args and kwargs:
return method(*args, **kwargs)
elif args:
return method(*args)
elif kwargs:
return method(**kwargs)
else:
return method()
except Exception:
continue
except Exception:
return None
def _check_allowed_modules(self, value: dict[str, Any]) -> None:
needed = tuple(value["id"])
method = value.get("method")
if isinstance(method, list):
method_display = ",".join(m or "<init>" for m in method)
elif isinstance(method, str):
method_display = method
else:
method_display = "<init>"
dotted = ".".join(needed)
if not self._allowed_modules:
raise InvalidModuleError(
f"Refused to deserialize JSON constructor: {dotted} (method: {method_display}). "
"No allowed_json_modules configured.\n\n"
"Unblock with ONE of:\n"
f" • JsonPlusSerializer(allowed_json_modules=[{needed!r}, ...])\n"
" • (DANGEROUS) JsonPlusSerializer(allowed_json_modules=True)\n\n"
"Note: Prefix allowlists are intentionally unsupported; prefer exact symbols "
"or plain-JSON representations revived without import-time side effects."
)
if self._allowed_modules is True:
return
if needed in self._allowed_modules:
return
raise InvalidModuleError(
f"Refused to deserialize JSON constructor: {dotted} (method: {method_display}). "
"Symbol is not in the deserialization allowlist.\n\n"
"Add exactly this symbol to unblock:\n"
f" JsonPlusSerializer(allowed_json_modules=[{needed!r}, ...])\n"
"Or, as a last resort (DANGEROUS):\n"
" JsonPlusSerializer(allowed_json_modules=True)"
)
def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
@@ -215,15 +184,10 @@ class JsonPlusSerializer(SerializerProtocol):
try:
return "msgpack", _msgpack_enc(obj)
except ormsgpack.MsgpackEncodeError as exc:
if "valid UTF-8" in str(exc):
return "json", self.dumps(obj)
elif self.pickle_fallback:
if self.pickle_fallback:
return "pickle", pickle.dumps(obj)
raise exc
def loads(self, data: bytes) -> Any:
return json.loads(data, object_hook=self._reviver)
def loads_typed(self, data: tuple[str, bytes]) -> Any:
type_, data_ = data
if type_ == "null":
@@ -233,7 +197,7 @@ class JsonPlusSerializer(SerializerProtocol):
elif type_ == "bytearray":
return bytearray(data_)
elif type_ == "json":
return self.loads(data_)
return json.loads(data_, object_hook=self._reviver)
elif type_ == "msgpack":
return ormsgpack.unpackb(
data_, ext_hook=self._unpack_ext_hook, option=ormsgpack.OPT_NON_STR_KEYS
@@ -663,6 +627,13 @@ def _msgpack_ext_hook_to_json(code: int, data: bytes) -> Any:
return
class InvalidModuleError(Exception):
"""Exception raised when a module is not in the allowlist."""
def __init__(self, message: str):
self.message = message
_option = (
ormsgpack.OPT_NON_STR_KEYS
| ormsgpack.OPT_PASSTHROUGH_DATACLASS
@@ -1,7 +1,6 @@
from collections.abc import Sequence
from typing import (
Any,
Optional,
Protocol,
TypeVar,
runtime_checkable,
@@ -28,9 +27,9 @@ class ChannelProtocol(Protocol[Value, Update, C]):
@property
def UpdateType(self) -> Any: ...
def checkpoint(self) -> Optional[C]: ...
def checkpoint(self) -> C | None: ...
def from_checkpoint(self, checkpoint: Optional[C]) -> Self: ...
def from_checkpoint(self, checkpoint: C | None) -> Self: ...
def update(self, values: Sequence[Update]) -> bool: ...
+146 -97
View File
@@ -4,9 +4,9 @@ Stores provide long-term memory that persists across threads and conversations.
Supports hierarchical namespaces, key-value storage, and optional vector search.
Core types:
- BaseStore: Store interface with sync/async operations
- Item: Stored key-value pairs with metadata
- Op: Get/Put/Search/List operations
- `BaseStore`: Store interface with sync/async operations
- `Item`: Stored key-value pairs with metadata
- `Op`: Get/Put/Search/List operations
"""
from __future__ import annotations
@@ -19,7 +19,6 @@ from typing import (
Literal,
NamedTuple,
TypedDict,
Union,
cast,
)
@@ -57,7 +56,7 @@ class Item:
key: Unique identifier within the namespace.
namespace: Hierarchical path defining the collection in which this document resides.
Represented as a tuple of strings, allowing for nested categorization.
For example: ("documents", 'user123')
For example: `("documents", 'user123')`
created_at: Timestamp of item creation.
updated_at: Timestamp of last update.
"""
@@ -164,6 +163,7 @@ class GetOp(NamedTuple):
???+ example "Examples"
Basic item retrieval:
```python
GetOp(namespace=("users", "profiles"), key="user123")
GetOp(namespace=("cache", "embeddings"), key="doc456")
@@ -207,11 +207,14 @@ class SearchOp(NamedTuple):
within a given namespace prefix. It provides pagination through limit and offset
parameters.
Note:
!!! note
Natural language search support depends on your store implementation.
???+ example "Examples"
Search with filters and pagination:
```python
SearchOp(
namespace_prefix=("documents",),
@@ -222,6 +225,7 @@ class SearchOp(NamedTuple):
```
Natural language search:
```python
SearchOp(
namespace_prefix=("users", "content"),
@@ -249,14 +253,15 @@ class SearchOp(NamedTuple):
The filter supports both exact matches and operator-based comparisons.
Supported Operators:
- $eq: Equal to (same as direct value comparison)
- $ne: Not equal to
- $gt: Greater than
- $gte: Greater than or equal to
- $lt: Less than
- $lte: Less than or equal to
- `$eq`: Equal to (same as direct value comparison)
- `$ne`: Not equal to
- `$gt`: Greater than
- `$gte`: Greater than or equal to
- `$lt`: Less than
- `$lte`: Less than or equal to
???+ example "Examples"
Simple exact match:
```python
@@ -289,6 +294,7 @@ class SearchOp(NamedTuple):
"""Natural language search query for semantic search capabilities.
???+ example "Examples"
- "technical documentation about REST APIs"
- "machine learning papers from 2023"
"""
@@ -302,10 +308,11 @@ class SearchOp(NamedTuple):
# Type representing a namespace path that can include wildcards
NamespacePath = tuple[Union[str, Literal["*"]], ...]
NamespacePath = tuple[str | Literal["*"], ...]
"""A tuple representing a namespace path that can include wildcards.
???+ example "Examples"
```python
("users",) # Exact users namespace
("documents", "*") # Any sub-namespace under documents
@@ -331,17 +338,21 @@ class MatchCondition(NamedTuple):
hierarchies.
???+ example "Examples"
Prefix matching:
```python
MatchCondition(match_type="prefix", path=("users", "profiles"))
```
Suffix matching with wildcard:
```python
MatchCondition(match_type="suffix", path=("cache", "*"))
```
Simple suffix matching:
```python
MatchCondition(match_type="suffix", path=("v1",))
```
@@ -362,7 +373,8 @@ class ListNamespacesOp(NamedTuple):
???+ example "Examples"
List all namespaces under the "documents" path:
List all namespaces under the `"documents"` path:
```python
ListNamespacesOp(
match_conditions=(MatchCondition(match_type="prefix", path=("documents",)),),
@@ -370,7 +382,8 @@ class ListNamespacesOp(NamedTuple):
)
```
List all namespaces that end with "v1":
List all namespaces that end with `"v1"`:
```python
ListNamespacesOp(
match_conditions=(MatchCondition(match_type="suffix", path=("v1",)),),
@@ -384,12 +397,15 @@ class ListNamespacesOp(NamedTuple):
"""Optional conditions for filtering namespaces.
???+ example "Examples"
All user namespaces:
```python
(MatchCondition(match_type="prefix", path=("users",)),)
```
All namespaces that start with "docs" and end with "draft":
All namespaces that start with `"docs"` and end with `"draft"`:
```python
(
MatchCondition(match_type="prefix", path=("docs",)),
@@ -426,17 +442,21 @@ class PutOp(NamedTuple):
Each element in the tuple represents one level in the hierarchy.
???+ example "Examples"
Root level documents
Root level documents:
```python
("documents",)
```
User-specific documents
User-specific documents:
```python
("documents", "user123")
```
Nested cache structure
Nested cache structure:
```python
("cache", "embeddings", "v1")
```
@@ -449,15 +469,15 @@ class PutOp(NamedTuple):
Together with the namespace, it forms a complete path to the item.
Example:
If namespace is ("documents", "user123") and key is "report1",
the full path would effectively be "documents/user123/report1"
If namespace is `("documents", "user123")` and key is `"report1"`,
the full path would effectively be `"documents/user123/report1"`
"""
value: dict[str, Any] | None
"""The data to store, or None to mark the item for deletion.
"""The data to store, or `None` to mark the item for deletion.
The value must be a dictionary with string keys and JSON-serializable values.
Setting this to None signals that the item should be deleted.
Setting this to `None` signals that the item should be deleted.
Example:
{
@@ -471,25 +491,26 @@ class PutOp(NamedTuple):
"""Controls how the item's fields are indexed for search operations.
Indexing configuration determines how the item can be found through search:
- None (default): Uses the store's default indexing configuration (if provided)
- False: Disables indexing for this item
- list[str]: Specifies which json path fields to index for search
- `None` (default): Uses the store's default indexing configuration (if provided)
- `False`: Disables indexing for this item
- `list[str]`: Specifies which json path fields to index for search
The item remains accessible through direct get() operations regardless of indexing.
When indexed, fields can be searched using natural language queries through
vector similarity search (if supported by the store implementation).
Path Syntax:
- Simple field access: "field"
- Nested fields: "parent.child.grandchild"
- Simple field access: `"field"`
- Nested fields: `"parent.child.grandchild"`
- Array indexing:
- Specific index: "array[0]"
- Last element: "array[-1]"
- All elements (each individually): "array[*]"
- Specific index: `"array[0]"`
- Last element: `"array[-1]"`
- All elements (each individually): `"array[*]"`
???+ example "Examples"
- None - Use store defaults (whole item)
- list[str] - List of fields to index
- `None` - Use store defaults (whole item)
- `list[str]` - List of fields to index
```python
[
@@ -509,12 +530,12 @@ class PutOp(NamedTuple):
will expire this many minutes after it was last accessed. The expiration timer
refreshes on both read operations (get/search) and write operations (put/update).
When the TTL expires, the item will be scheduled for deletion on a best-effort basis.
Defaults to None (no expiration).
Defaults to `None` (no expiration).
"""
Op = Union[GetOp, SearchOp, PutOp, ListNamespacesOp]
Result = Union[Item, list[Item], list[SearchItem], list[tuple[str, ...]], None]
Op = GetOp | SearchOp | PutOp | ListNamespacesOp
Result = Item | list[Item] | list[SearchItem] | list[tuple[str, ...]] | None
class InvalidNamespaceError(ValueError):
@@ -525,18 +546,18 @@ class TTLConfig(TypedDict, total=False):
"""Configuration for TTL (time-to-live) behavior in the store."""
refresh_on_read: bool
"""Default behavior for refreshing TTLs on read operations (GET and SEARCH).
"""Default behavior for refreshing TTLs on read operations (`GET` and `SEARCH`).
If True, TTLs will be refreshed on read operations (get/search) by default.
This can be overridden per-operation by explicitly setting refresh_ttl.
Defaults to True if not configured.
If `True`, TTLs will be refreshed on read operations (get/search) by default.
This can be overridden per-operation by explicitly setting `refresh_ttl`.
Defaults to `True` if not configured.
"""
default_ttl: float | None
"""Default TTL (time-to-live) in minutes for new items.
If provided, new items will expire after this many minutes after their last access.
The expiration timer refreshes on both read and write operations.
Defaults to None (no expiration).
Defaults to `None` (no expiration).
"""
sweep_interval_minutes: int | None
"""Interval in minutes between TTL sweep operations.
@@ -550,33 +571,35 @@ class IndexConfig(TypedDict, total=False):
"""Configuration for indexing documents for semantic search in the store.
If not provided to the store, the store will not support vector search.
In that case, all `index` arguments to put() and `aput()` operations will be ignored.
In that case, all `index` arguments to `put()` and `aput()` operations will be ignored.
"""
dims: int
"""Number of dimensions in the embedding vectors.
Common embedding models have the following dimensions:
- openai:text-embedding-3-large: 3072
- openai:text-embedding-3-small: 1536
- openai:text-embedding-ada-002: 1536
- cohere:embed-english-v3.0: 1024
- cohere:embed-english-light-v3.0: 384
- cohere:embed-multilingual-v3.0: 1024
- cohere:embed-multilingual-light-v3.0: 384
- `openai:text-embedding-3-large`: `3072`
- `openai:text-embedding-3-small`: `1536`
- `openai:text-embedding-ada-002`: `1536`
- `cohere:embed-english-v3.0`: `1024`
- `cohere:embed-english-light-v3.0`: `384`
- `cohere:embed-multilingual-v3.0`: `1024`
- `cohere:embed-multilingual-light-v3.0`: `384`
"""
embed: Embeddings | EmbeddingsFunc | AEmbeddingsFunc | str
"""Optional function to generate embeddings from text.
Can be specified in three ways:
1. A LangChain Embeddings instance
2. A synchronous embedding function (EmbeddingsFunc)
3. An asynchronous embedding function (AEmbeddingsFunc)
4. A provider string (e.g., "openai:text-embedding-3-small")
1. A LangChain `Embeddings` instance
2. A synchronous embedding function (`EmbeddingsFunc`)
3. An asynchronous embedding function (`AEmbeddingsFunc`)
4. A provider string (e.g., `"openai:text-embedding-3-small"`)
???+ example "Examples"
Using LangChain's initialization with InMemoryStore:
Using LangChain's initialization with `InMemoryStore`:
```python
from langchain.embeddings import init_embeddings
from langgraph.store.memory import InMemoryStore
@@ -589,7 +612,8 @@ class IndexConfig(TypedDict, total=False):
)
```
Using a custom embedding function with InMemoryStore:
Using a custom embedding function with `InMemoryStore`:
```python
from openai import OpenAI
from langgraph.store.memory import InMemoryStore
@@ -611,7 +635,8 @@ class IndexConfig(TypedDict, total=False):
)
```
Using an asynchronous embedding function with InMemoryStore:
Using an asynchronous embedding function with `InMemoryStore`:
```python
from openai import AsyncOpenAI
from langgraph.store.memory import InMemoryStore
@@ -639,16 +664,17 @@ class IndexConfig(TypedDict, total=False):
Controls which parts of stored items are embedded for semantic search. Follows JSON path syntax:
- ["$"]: Embeds the entire JSON object as one vector (default)
- ["field1", "field2"]: Embeds specific top-level fields
- ["parent.child"]: Embeds nested fields using dot notation
- ["array[*].field"]: Embeds field from each array element separately
- `["$"]`: Embeds the entire JSON object as one vector (default)
- `["field1", "field2"]`: Embeds specific top-level fields
- `["parent.child"]`: Embeds nested fields using dot notation
- `["array[*].field"]`: Embeds field from each array element separately
Note:
You can always override this behavior when storing an item using the
`index` parameter in the `put` or `aput` operations.
???+ example "Examples"
```python
# Embed entire document (default)
fields=["$"]
@@ -667,7 +693,7 @@ class IndexConfig(TypedDict, total=False):
Note:
- Fields missing from a document are skipped
- Array notation creates separate embeddings for each element
- Complex nested paths are supported (e.g., "a.b[*].c.d")
- Complex nested paths are supported (e.g., `"a.b[*].c.d"`)
"""
@@ -732,11 +758,11 @@ class BaseStore(ABC):
namespace: Hierarchical path for the item.
key: Unique identifier within the namespace.
refresh_ttl: Whether to refresh TTLs for the returned item.
If None (default), uses the store's default refresh_ttl setting.
If `None`, uses the store's default `refresh_ttl` setting.
If no TTL is specified, this argument is ignored.
Returns:
The retrieved item or None if not found.
The retrieved item or `None` if not found.
"""
return self.batch(
[GetOp(namespace, str(key), _ensure_refresh(self.ttl_config, refresh_ttl))]
@@ -768,7 +794,9 @@ class BaseStore(ABC):
List of items matching the search criteria.
???+ example "Examples"
Basic filtering:
```python
# Search for documents with specific metadata
results = store.search(
@@ -778,6 +806,7 @@ class BaseStore(ABC):
```
Natural language search (requires vector store implementation):
```python
# Initialize store with embedding configuration
store = YourStore( # e.g., InMemoryStore, AsyncPostgresStore
@@ -789,6 +818,7 @@ class BaseStore(ABC):
)
# Search for semantically similar documents
results = store.search(
("docs",),
query="machine learning applications in healthcare",
@@ -797,8 +827,10 @@ class BaseStore(ABC):
)
```
Note: Natural language search support depends on your store implementation
and requires proper embedding configuration.
!!! note
Natural language search support depends on your store implementation
and requires proper embedding configuration.
"""
return self.batch(
[
@@ -826,7 +858,7 @@ class BaseStore(ABC):
Args:
namespace: Hierarchical path for the item, represented as a tuple of strings.
Example: ("documents", "user123")
Example: `("documents", "user123")`
key: Unique identifier within the namespace. Together with namespace forms
the complete path to the item.
value: Dictionary containing the item's data. Must contain string keys
@@ -837,10 +869,10 @@ class BaseStore(ABC):
If you do not initialize the store with indexing capabilities,
the `index` parameter will be ignored
- False: Disable indexing for this item
- list[str]: List of field paths to index, supporting:
- Nested fields: "metadata.title"
- Array access: "chapters[*].content" (each indexed separately)
- Specific indices: "authors[0].name"
- `list[str]`: List of field paths to index, supporting:
- Nested fields: `"metadata.title"`
- Array access: `"chapters[*].content"` (each indexed separately)
- Specific indices: `"authors[0].name"`
ttl: Time to live in minutes. Support for this argument depends on your store adapter.
If specified, the item will expire after this many minutes from when it was last accessed.
None means no expiration. Expired runs will be deleted opportunistically.
@@ -856,18 +888,22 @@ class BaseStore(ABC):
Some implementations may not support expiration of items.
???+ example "Examples"
Store item. Indexing depends on how you configure the store.
Store item. Indexing depends on how you configure the store:
```python
store.put(("docs",), "report", {"memory": "Will likes ai"})
```
Do not index item for semantic search. Still accessible through get()
and search() operations but won't have a vector representation.
Do not index item for semantic search. Still accessible through `get()`
and `search()` operations but won't have a vector representation.
```python
store.put(("docs",), "report", {"memory": "Will likes ai"}, index=False)
```
Index specific fields for search.
Index specific fields for search:
```python
store.put(("docs",), "report", {"memory": "Will likes ai"}, index=["memory"])
```
@@ -918,15 +954,17 @@ class BaseStore(ABC):
suffix: Filter namespaces that end with this path.
max_depth: Return namespaces up to this depth in the hierarchy.
Namespaces deeper than this level will be truncated.
limit: Maximum number of namespaces to return (default 100).
offset: Number of namespaces to skip for pagination (default 0).
limit: Maximum number of namespaces to return.
offset: Number of namespaces to skip for pagination.
Returns:
List[Tuple[str, ...]]: A list of namespace tuples that match the criteria.
Each tuple represents a full namespace path up to `max_depth`.
A list of namespace tuples that match the criteria. Each tuple represents a
full namespace path up to `max_depth`.
???+ example "Examples":
Setting max_depth=3. Given the namespaces:
Setting `max_depth=3`. Given the namespaces:
```python
# Example if you have the following namespaces:
# ("a", "b", "c")
@@ -966,7 +1004,7 @@ class BaseStore(ABC):
key: Unique identifier within the namespace.
Returns:
The retrieved item or None if not found.
The retrieved item or `None` if not found.
"""
return (
await self.abatch(
@@ -1000,14 +1038,16 @@ class BaseStore(ABC):
limit: Maximum number of items to return.
offset: Number of items to skip before returning results.
refresh_ttl: Whether to refresh TTLs for the returned items.
If None (default), uses the store's TTLConfig.refresh_default setting.
If TTLConfig is not provided or no TTL is specified, this argument is ignored.
If `None`, uses the store's `TTLConfig.refresh_default` setting.
If `TTLConfig` is not provided or no TTL is specified, this argument is ignored.
Returns:
List of items matching the search criteria.
???+ example "Examples"
Basic filtering:
```python
# Search for documents with specific metadata
results = await store.asearch(
@@ -1017,6 +1057,7 @@ class BaseStore(ABC):
```
Natural language search (requires vector store implementation):
```python
# Initialize store with embedding configuration
store = YourStore( # e.g., InMemoryStore, AsyncPostgresStore
@@ -1028,6 +1069,7 @@ class BaseStore(ABC):
)
# Search for semantically similar documents
results = await store.asearch(
("docs",),
query="machine learning applications in healthcare",
@@ -1036,8 +1078,10 @@ class BaseStore(ABC):
)
```
Note: Natural language search support depends on your store implementation
and requires proper embedding configuration.
!!! note
Natural language search support depends on your store implementation
and requires proper embedding configuration.
"""
return (
await self.abatch(
@@ -1067,7 +1111,7 @@ class BaseStore(ABC):
Args:
namespace: Hierarchical path for the item, represented as a tuple of strings.
Example: ("documents", "user123")
Example: `("documents", "user123")`
key: Unique identifier within the namespace. Together with namespace forms
the complete path to the item.
value: Dictionary containing the item's data. Must contain string keys
@@ -1078,10 +1122,10 @@ class BaseStore(ABC):
If you do not initialize the store with indexing capabilities,
the `index` parameter will be ignored
- False: Disable indexing for this item
- list[str]: List of field paths to index, supporting:
- Nested fields: "metadata.title"
- Array access: "chapters[*].content" (each indexed separately)
- Specific indices: "authors[0].name"
- `list[str]`: List of field paths to index, supporting:
- Nested fields: `"metadata.title"`
- Array access: `"chapters[*].content"` (each indexed separately)
- Specific indices: `"authors[0].name"`
ttl: Time to live in minutes. Support for this argument depends on your store adapter.
If specified, the item will expire after this many minutes from when it was last accessed.
None means no expiration. Expired runs will be deleted opportunistically.
@@ -1097,18 +1141,22 @@ class BaseStore(ABC):
Some implementations may not support expiration of items.
???+ example "Examples"
Store item. Indexing depends on how you configure the store.
Store item. Indexing depends on how you configure the store:
```python
await store.aput(("docs",), "report", {"memory": "Will likes ai"})
```
Do not index item for semantic search. Still accessible through get()
and search() operations but won't have a vector representation.
Do not index item for semantic search. Still accessible through `get()`
and `search()` operations but won't have a vector representation.
```python
await store.aput(("docs",), "report", {"memory": "Will likes ai"}, index=False)
```
Index specific fields for search (if store configured to index items):
```python
await store.aput(
("docs",),
@@ -1167,15 +1215,16 @@ class BaseStore(ABC):
suffix: Filter namespaces that end with this path.
max_depth: Return namespaces up to this depth in the hierarchy.
Namespaces deeper than this level will be truncated to this depth.
limit: Maximum number of namespaces to return (default 100).
offset: Number of namespaces to skip for pagination (default 0).
limit: Maximum number of namespaces to return.
offset: Number of namespaces to skip for pagination.
Returns:
List[Tuple[str, ...]]: A list of namespace tuples that match the criteria.
Each tuple represents a full namespace path up to `max_depth`.
A list of namespace tuples that match the criteria. Each tuple represents a
full namespace path up to `max_depth`.
???+ example "Examples"
Setting max_depth=3 with existing namespaces:
Setting `max_depth=3` with existing namespaces:
```python
# Given the following namespaces:
# ("a", "b", "c")
@@ -5,8 +5,8 @@ from __future__ import annotations
import asyncio
import functools
import weakref
from collections.abc import Iterable
from typing import Any, Callable, Literal, TypeVar
from collections.abc import Callable, Iterable
from typing import Any, Literal, TypeVar
from langgraph.store.base import (
NOT_PROVIDED,
@@ -349,7 +349,7 @@ async def _run(
results = [results[ix] for ix in listen]
# set the results of each operation
for fut, result in zip(futs, results):
for fut, result in zip(futs, results, strict=False):
# guard against future being done (e.g. cancelled)
if not fut.done():
fut.set_result(result)
+16 -5
View File
@@ -11,8 +11,8 @@ from __future__ import annotations
import asyncio
import functools
import json
from collections.abc import Awaitable, Sequence
from typing import Any, Callable
from collections.abc import Awaitable, Callable, Sequence
from typing import Any
from langchain_core.embeddings import Embeddings
@@ -49,7 +49,9 @@ def ensure_embeddings(
An Embeddings instance that wraps the provided function(s).
??? example "Examples"
Wrap a synchronous embedding function:
```python
def my_embed_fn(texts):
return [[0.1, 0.2] for _ in texts]
@@ -59,6 +61,7 @@ def ensure_embeddings(
```
Wrap an asynchronous embedding function:
```python
async def my_async_fn(texts):
return [[0.1, 0.2] for _ in texts]
@@ -68,6 +71,7 @@ def ensure_embeddings(
```
Initialize embeddings using a provider string:
```python
# Requires langchain>=0.3.9 and langgraph-checkpoint>=2.0.11
embeddings = ensure_embeddings("openai:text-embedding-3-small")
@@ -119,7 +123,9 @@ class EmbeddingsLambda(Embeddings):
will raise an error. If sync, it will be used for both sync and async operations.
??? example "Examples"
With a sync function:
```python
def my_embed_fn(texts):
# Return 2D embeddings for each text
@@ -131,6 +137,7 @@ class EmbeddingsLambda(Embeddings):
```
With an async function:
```python
async def my_async_fn(texts):
return [[0.1, 0.2] for _ in texts]
@@ -238,7 +245,7 @@ def get_text_at_path(obj: Any, path: str | list[str]) -> list[str]:
- Nested paths in multi-field: "{field1,nested.field2}"
"""
if not path or path == "$":
return [json.dumps(obj, sort_keys=True)]
return [json.dumps(obj, sort_keys=True, ensure_ascii=False)]
tokens = tokenize_path(path) if isinstance(path, str) else path
@@ -249,7 +256,7 @@ def get_text_at_path(obj: Any, path: str | list[str]) -> list[str]:
elif obj is None:
return []
elif isinstance(obj, (list, dict)):
return [json.dumps(obj, sort_keys=True)]
return [json.dumps(obj, sort_keys=True, ensure_ascii=False)]
return []
token = tokens[pos]
@@ -295,7 +302,11 @@ def get_text_at_path(obj: Any, path: str | list[str]) -> list[str]:
if isinstance(current_obj, (str, int, float, bool)):
results.append(str(current_obj))
elif isinstance(current_obj, (list, dict)):
results.append(json.dumps(current_obj, sort_keys=True))
results.append(
json.dumps(
current_obj, sort_keys=True, ensure_ascii=False
)
)
# Handle wildcard
elif token == "*":
@@ -295,7 +295,7 @@ class InMemoryStore(BaseStore):
if queries:
coros = [self.embeddings.aembed_query(q) for q in list(queries)]
results = await asyncio.gather(*coros)
queryinmem_store = dict(zip(queries, results))
queryinmem_store = dict(zip(queries, results, strict=False))
return queryinmem_store
@@ -323,7 +323,9 @@ class InMemoryStore(BaseStore):
scores = _cosine_similarity(query_embedding, flat_vectors)
sorted_results = sorted(
zip(scores, flat_items), key=lambda x: x[0], reverse=True
zip(scores, flat_items, strict=False),
key=lambda x: x[0],
reverse=True,
)
# max pooling
seen: set[tuple[tuple[str, ...], str]] = set()
@@ -452,7 +454,7 @@ class InMemoryStore(BaseStore):
f"Number of embeddings ({len(embeddings)}) does not"
f" match number of indices ({len(indices)})"
)
for embedding, (ns, key, path) in zip(embeddings, indices):
for embedding, (ns, key, path) in zip(embeddings, indices, strict=False):
self._vectors[ns][key][path] = embedding
def _handle_list_namespaces(self, op: ListNamespacesOp) -> list[tuple[str, ...]]:
@@ -511,7 +513,7 @@ def _cosine_similarity(X: list[float], Y: list[list[float]]) -> list[float]:
similarities = []
for y in Y:
dot_product = sum(a * b for a, b in zip(X, y))
dot_product = sum(a * b for a, b in zip(X, y, strict=False))
norm1 = sum(a * a for a in X) ** 0.5
norm2 = sum(a * a for a in y) ** 0.5
similarity = dot_product / (norm1 * norm2) if norm1 > 0 and norm2 > 0 else 0.0
@@ -529,14 +531,14 @@ def _does_match(match_condition: MatchCondition, key: tuple[str, ...]) -> bool:
return False
if match_type == "prefix":
for k_elem, p_elem in zip(key, path):
for k_elem, p_elem in zip(key, path, strict=False):
if p_elem == "*":
continue # Wildcard matches any element
if k_elem != p_elem:
return False
return True
elif match_type == "suffix":
for k_elem, p_elem in zip(reversed(key), reversed(path)):
for k_elem, p_elem in zip(reversed(key), reversed(path), strict=False):
if p_elem == "*":
continue # Wildcard matches any element
if k_elem != p_elem:
@@ -563,7 +565,10 @@ def _compare_values(item_value: Any, filter_value: Any) -> bool:
return (
isinstance(item_value, (list, tuple))
and len(item_value) == len(filter_value)
and all(_compare_values(iv, fv) for iv, fv in zip(item_value, filter_value))
and all(
_compare_values(iv, fv)
for iv, fv in zip(item_value, filter_value, strict=False)
)
)
else:
return item_value == filter_value
+14 -6
View File
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-checkpoint"
version = "2.1.1"
version = "3.0.0"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
requires-python = ">=3.9"
requires-python = ">=3.10"
readme = "README.md"
license = "MIT"
license-files = ['LICENSE']
@@ -20,20 +20,26 @@ dependencies = [
Repository = "https://www.github.com/langchain-ai/langgraph"
[dependency-groups]
dev = [
"ruff",
"codespell",
test = [
"pytest",
"pytest-asyncio",
"pytest-mock",
"pytest-watcher",
"mypy",
"dataclasses-json",
"numpy",
"pandas",
"pandas-stubs>=2.2.2.240807",
"redis",
]
lint = [
"ruff",
"codespell",
"mypy",
]
dev = [
{include-group = "test"},
{include-group = "lint"},
]
[tool.hatch.build.targets.wheel]
include = ["langgraph"]
@@ -49,8 +55,10 @@ lint.select = [
"UP", # pyupgrade
"B", # flake8-bugbear
"I", # isort
"UP", # pyupgrade
]
lint.ignore = ["E501", "B008"]
target-version = "py310"
[tool.pytest-watcher]
now = true
+44 -27
View File
@@ -1,4 +1,5 @@
import dataclasses
import json
import pathlib
import re
import sys
@@ -19,6 +20,7 @@ from pydantic.v1 import BaseModel as BaseModelV1
from pydantic.v1 import SecretStr as SecretStrV1
from langgraph.checkpoint.serde.jsonplus import (
InvalidModuleError,
JsonPlusSerializer,
_msgpack_ext_hook_to_json,
)
@@ -60,22 +62,15 @@ class MyDataclass:
pass
if sys.version_info < (3, 10):
@dataclasses.dataclass(slots=True)
class MyDataclassWSlots:
foo: str
bar: int
inner: InnerDataclass
class MyDataclassWSlots(MyDataclass):
def something(self) -> None:
pass
else:
@dataclasses.dataclass(slots=True)
class MyDataclassWSlots:
foo: str
bar: int
inner: InnerDataclass
def something(self) -> None:
pass
class MyEnum(Enum):
FOO = "foo"
@@ -115,11 +110,7 @@ def test_serde_jsonplus() -> None:
"my_dataclass": MyDataclass("foo", 1, InnerDataclass("hello")),
"my_enum": MyEnum.FOO,
"my_pydantic": MyPydantic(foo="foo", bar=1, inner=InnerPydantic(hello="hello")),
"my_pydantic_v1": MyPydanticV1(
foo="foo", bar=1, inner=InnerPydanticV1(hello="hello")
),
"my_secret_str": SecretStr("meow"),
"my_secret_str_v1": SecretStrV1("meow"),
"person": Person(name="foo"),
"a_bool": True,
"a_none": None,
@@ -141,6 +132,12 @@ def test_serde_jsonplus() -> None:
),
}
if sys.version_info < (3, 14):
to_serialize["my_pydantic_v1"] = MyPydanticV1(
foo="foo", bar=1, inner=InnerPydanticV1(hello="hello")
)
to_serialize["my_secret_str_v1"] = SecretStrV1("meow")
serde = JsonPlusSerializer()
dumped = serde.dumps_typed(to_serialize)
@@ -165,10 +162,9 @@ def test_serde_jsonplus() -> None:
"Text\ud83d\udcac",
"收花🙄·到",
]
serde = JsonPlusSerializer(pickle_fallback=True)
assert serde.loads_typed(serde.dumps_typed(surrogates)) == [
v.encode("utf-8", "ignore").decode() for v in surrogates
]
assert serde.loads_typed(serde.dumps_typed(surrogates)) == surrogates
def test_serde_jsonplus_json_mode() -> None:
@@ -197,11 +193,7 @@ def test_serde_jsonplus_json_mode() -> None:
"my_dataclass": MyDataclass("foo", 1, InnerDataclass("hello")),
"my_enum": MyEnum.FOO,
"my_pydantic": MyPydantic(foo="foo", bar=1, inner=InnerPydantic(hello="hello")),
"my_pydantic_v1": MyPydanticV1(
foo="foo", bar=1, inner=InnerPydanticV1(hello="hello")
),
"my_secret_str": SecretStr("meow"),
"my_secret_str_v1": SecretStrV1("meow"),
"person": Person(name="foo"),
"a_bool": True,
"a_none": None,
@@ -223,13 +215,20 @@ def test_serde_jsonplus_json_mode() -> None:
),
}
if sys.version_info < (3, 14):
to_serialize["my_pydantic_v1"] = MyPydanticV1(
foo="foo", bar=1, inner=InnerPydanticV1(hello="hello")
)
to_serialize["my_secret_str_v1"] = SecretStrV1("meow")
serde = JsonPlusSerializer(__unpack_ext_hook__=_msgpack_ext_hook_to_json)
dumped = serde.dumps_typed(to_serialize)
assert dumped[0] == "msgpack"
result = serde.loads_typed(dumped)
assert result == {
expected_result = {
"path": ["foo", "bar"],
"re": ["foo", 48],
"decimal": "1.10101",
@@ -253,9 +252,7 @@ def test_serde_jsonplus_json_mode() -> None:
"my_dataclass": {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}},
"my_enum": "foo",
"my_pydantic": {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}},
"my_pydantic_v1": {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}},
"my_secret_str": "meow",
"my_secret_str_v1": "meow",
"person": {"name": "foo"},
"a_bool": True,
"a_none": None,
@@ -277,6 +274,12 @@ def test_serde_jsonplus_json_mode() -> None:
},
}
if sys.version_info < (3, 14):
expected_result["my_pydantic_v1"] = {"foo": "foo", "bar": 1, "inner": {"hello": "hello"}}
expected_result["my_secret_str_v1"] = "meow"
assert result == expected_result
def test_serde_jsonplus_bytes() -> None:
serde = JsonPlusSerializer()
@@ -288,6 +291,20 @@ def test_serde_jsonplus_bytes() -> None:
assert serde.loads_typed(dumped) == some_bytes
def test_deserde_invalid_module() -> None:
serde = JsonPlusSerializer()
load = {
"lc": 2,
"type": "constructor",
"id": ["pprint", "pprint"],
"kwargs": {"object": "HELLO"},
}
with pytest.raises(InvalidModuleError):
serde._revive_lc2(load)
serde = JsonPlusSerializer(allowed_json_modules=[("pprint", "pprint")])
serde.loads_typed(("json", json.dumps(load).encode("utf-8")))
def test_serde_jsonplus_bytearray() -> None:
serde = JsonPlusSerializer()
+49 -42
View File
@@ -5,12 +5,13 @@ import time
import pytest
import redis
from langgraph.cache.base import FullKey
from langgraph.cache.redis import RedisCache
class TestRedisCache:
@pytest.fixture(autouse=True)
def setup(self):
def setup(self) -> None:
"""Set up test Redis client and cache."""
self.client = redis.Redis(
host="localhost", port=6379, db=0, decode_responses=False
@@ -20,21 +21,21 @@ class TestRedisCache:
except redis.ConnectionError:
pytest.skip("Redis server not available")
self.cache = RedisCache(self.client, prefix="test:cache:")
self.cache: RedisCache = RedisCache(self.client, prefix="test:cache:")
# Clean up before each test
self.client.flushdb()
def teardown_method(self):
def teardown_method(self) -> None:
"""Clean up after each test."""
try:
self.client.flushdb()
except Exception:
pass
def test_basic_set_and_get(self):
def test_basic_set_and_get(self) -> None:
"""Test basic set and get operations."""
keys = [(("graph", "node"), "key1")]
keys: list[FullKey] = [(("graph", "node"), "key1")]
values = {keys[0]: ({"result": 42}, None)}
# Set value
@@ -45,9 +46,9 @@ class TestRedisCache:
assert len(result) == 1
assert result[keys[0]] == {"result": 42}
def test_batch_operations(self):
def test_batch_operations(self) -> None:
"""Test batch set and get operations."""
keys = [
keys: list[FullKey] = [
(("graph", "node1"), "key1"),
(("graph", "node2"), "key2"),
(("other", "node"), "key3"),
@@ -68,9 +69,9 @@ class TestRedisCache:
assert result[keys[1]] == {"result": 2}
assert result[keys[2]] == {"result": 3}
def test_ttl_behavior(self):
def test_ttl_behavior(self) -> None:
"""Test TTL (time-to-live) functionality."""
key = (("graph", "node"), "ttl_key")
key: FullKey = (("graph", "node"), "ttl_key")
values = {key: ({"data": "expires_soon"}, 1)} # 1 second TTL
# Set with TTL
@@ -88,10 +89,10 @@ class TestRedisCache:
result = self.cache.get([key])
assert len(result) == 0
def test_namespace_isolation(self):
def test_namespace_isolation(self) -> None:
"""Test that different namespaces are isolated."""
key1 = (("graph1", "node"), "same_key")
key2 = (("graph2", "node"), "same_key")
key1: FullKey = (("graph1", "node"), "same_key")
key2: FullKey = (("graph2", "node"), "same_key")
values = {key1: ({"graph": 1}, None), key2: ({"graph": 2}, None)}
@@ -101,9 +102,12 @@ class TestRedisCache:
assert result[key1] == {"graph": 1}
assert result[key2] == {"graph": 2}
def test_clear_all(self):
def test_clear_all(self) -> None:
"""Test clearing all cached values."""
keys = [(("graph", "node1"), "key1"), (("graph", "node2"), "key2")]
keys: list[FullKey] = [
(("graph", "node1"), "key1"),
(("graph", "node2"), "key2"),
]
values = {keys[0]: ({"result": 1}, None), keys[1]: ({"result": 2}, None)}
self.cache.set(values)
@@ -119,9 +123,9 @@ class TestRedisCache:
result = self.cache.get(keys)
assert len(result) == 0
def test_clear_by_namespace(self):
def test_clear_by_namespace(self) -> None:
"""Test clearing cached values by namespace."""
keys = [
keys: list[FullKey] = [
(("graph1", "node"), "key1"),
(("graph2", "node"), "key2"),
(("graph1", "other"), "key3"),
@@ -142,7 +146,7 @@ class TestRedisCache:
assert len(result) == 1
assert result[keys[1]] == {"result": 2}
def test_empty_operations(self):
def test_empty_operations(self) -> None:
"""Test behavior with empty keys/values."""
# Empty get
result = self.cache.get([])
@@ -151,14 +155,14 @@ class TestRedisCache:
# Empty set
self.cache.set({}) # Should not raise error
def test_nonexistent_keys(self):
def test_nonexistent_keys(self) -> None:
"""Test getting keys that don't exist."""
keys = [(("graph", "node"), "nonexistent")]
keys: list[FullKey] = [(("graph", "node"), "nonexistent")]
result = self.cache.get(keys)
assert len(result) == 0
@pytest.mark.asyncio
async def test_async_operations(self):
async def test_async_operations(self) -> None:
"""Test async set and get operations with sync Redis client."""
# Create sync Redis client and cache (like main integration tests)
client = redis.Redis(host="localhost", port=6379, db=1, decode_responses=False)
@@ -167,9 +171,9 @@ class TestRedisCache:
except Exception:
pytest.skip("Redis not available")
cache = RedisCache(client, prefix="test:async:")
cache: RedisCache = RedisCache(client, prefix="test:async:")
keys = [(("graph", "node"), "async_key")]
keys: list[FullKey] = [(("graph", "node"), "async_key")]
values = {keys[0]: ({"async": True}, None)}
# Async set (delegates to sync)
@@ -184,7 +188,7 @@ class TestRedisCache:
client.flushdb()
@pytest.mark.asyncio
async def test_async_clear(self):
async def test_async_clear(self) -> None:
"""Test async clear operations with sync Redis client."""
# Create sync Redis client and cache (like main integration tests)
client = redis.Redis(host="localhost", port=6379, db=1, decode_responses=False)
@@ -193,9 +197,9 @@ class TestRedisCache:
except Exception:
pytest.skip("Redis not available")
cache = RedisCache(client, prefix="test:async:")
cache: RedisCache = RedisCache(client, prefix="test:async:")
keys = [(("graph", "node"), "key")]
keys: list[FullKey] = [(("graph", "node"), "key")]
values = {keys[0]: ({"data": "test"}, None)}
await cache.aset(values)
@@ -214,44 +218,44 @@ class TestRedisCache:
# Cleanup
client.flushdb()
def test_redis_unavailable_get(self):
def test_redis_unavailable_get(self) -> None:
"""Test behavior when Redis is unavailable during get operations."""
# Create cache with non-existent Redis server
bad_client = redis.Redis(
host="nonexistent", port=9999, socket_connect_timeout=0.1
)
cache = RedisCache(bad_client, prefix="test:cache:")
cache: RedisCache = RedisCache(bad_client, prefix="test:cache:")
keys = [(("graph", "node"), "key")]
keys: list[FullKey] = [(("graph", "node"), "key")]
result = cache.get(keys)
# Should return empty dict when Redis unavailable
assert result == {}
def test_redis_unavailable_set(self):
def test_redis_unavailable_set(self) -> None:
"""Test behavior when Redis is unavailable during set operations."""
# Create cache with non-existent Redis server
bad_client = redis.Redis(
host="nonexistent", port=9999, socket_connect_timeout=0.1
)
cache = RedisCache(bad_client, prefix="test:cache:")
cache: RedisCache = RedisCache(bad_client, prefix="test:cache:")
keys = [(("graph", "node"), "key")]
keys: list[FullKey] = [(("graph", "node"), "key")]
values = {keys[0]: ({"data": "test"}, None)}
# Should not raise exception when Redis unavailable
cache.set(values) # Should silently fail
@pytest.mark.asyncio
async def test_redis_unavailable_async(self):
async def test_redis_unavailable_async(self) -> None:
"""Test async behavior when Redis is unavailable."""
# Create sync cache with non-existent Redis server (like main integration tests)
bad_client = redis.Redis(
host="nonexistent", port=9999, socket_connect_timeout=0.1
)
cache = RedisCache(bad_client, prefix="test:cache:")
cache: RedisCache = RedisCache(bad_client, prefix="test:cache:")
keys = [(("graph", "node"), "key")]
keys: list[FullKey] = [(("graph", "node"), "key")]
values = {keys[0]: ({"data": "test"}, None)}
# Should return empty dict for get (delegates to sync)
@@ -261,10 +265,10 @@ class TestRedisCache:
# Should not raise exception for set (delegates to sync)
await cache.aset(values) # Should silently fail
def test_corrupted_data_handling(self):
def test_corrupted_data_handling(self) -> None:
"""Test handling of corrupted data in Redis."""
# Set some valid data first
keys = [(("graph", "node"), "valid_key")]
keys: list[FullKey] = [(("graph", "node"), "valid_key")]
values = {keys[0]: ({"data": "valid"}, None)}
self.cache.set(values)
@@ -273,33 +277,36 @@ class TestRedisCache:
self.client.set(corrupted_key, b"invalid:data:format:too:many:colons")
# Should skip corrupted entry and return only valid ones
all_keys = [keys[0], (("graph", "node"), "corrupted_key")]
all_keys: list[FullKey] = [keys[0], (("graph", "node"), "corrupted_key")]
result = self.cache.get(all_keys)
assert len(result) == 1
assert result[keys[0]] == {"data": "valid"}
def test_key_parsing_edge_cases(self):
def test_key_parsing_edge_cases(self) -> None:
"""Test key parsing with edge cases."""
# Test empty namespace
key1 = ((), "empty_ns")
key1: FullKey = ((), "empty_ns")
values = {key1: ({"data": "empty_ns"}, None)}
self.cache.set(values)
result = self.cache.get([key1])
assert result[key1] == {"data": "empty_ns"}
# Test namespace with special characters
key2 = (("graph:with:colons", "node-with-dashes"), "key_with_underscores")
key2: FullKey = (
("graph:with:colons", "node-with-dashes"),
"key_with_underscores",
)
values = {key2: ({"data": "special_chars"}, None)}
self.cache.set(values)
result = self.cache.get([key2])
assert result[key2] == {"data": "special_chars"}
def test_large_data_serialization(self):
def test_large_data_serialization(self) -> None:
"""Test handling of large data objects."""
# Create a large data structure
large_data = {"large_list": list(range(1000)), "nested": {"data": "x" * 1000}}
key = (("graph", "node"), "large_key")
key: FullKey = (("graph", "node"), "large_key")
values = {key: (large_data, None)}
self.cache.set(values)
+26 -2
View File
@@ -845,7 +845,7 @@ async def test_async_batched_vector_search_concurrent(
]
)
for results, (query, filter_) in zip(all_results, search_queries):
for results, (query, filter_) in zip(all_results, search_queries, strict=False):
assert len(results) > 0, f"No results for query '{query}' with filter {filter_}"
for result in results:
@@ -950,8 +950,8 @@ async def test_embed_with_path(fake_embeddings: CharacterEmbeddings) -> None:
assert results[0].key != results[1].key
ascore = results[0].score
bscore = results[1].score
assert ascore == bscore
assert ascore is not None and bscore is not None
assert ascore == pytest.approx(bscore, abs=1e-5)
results = await store.asearch(("test",), query="uuu")
assert len(results) == 2
@@ -1021,3 +1021,27 @@ async def test_embed_with_path(fake_embeddings: CharacterEmbeddings) -> None:
assert len(results) == 3
doc5_result = next(r for r in results if r.key == "doc5")
assert doc5_result.score is None
def test_non_ascii(fake_embeddings: CharacterEmbeddings) -> None:
"""Test support for non-ascii characters"""
store = InMemoryStore(
index={"dims": fake_embeddings.dims, "embed": fake_embeddings}
)
store.put(("user_123", "memories"), "1", {"text": "这是中文"}) # Chinese
store.put(("user_123", "memories"), "2", {"text": "これは日本語です"}) # Japanese
store.put(("user_123", "memories"), "3", {"text": "이건 한국어야"}) # Korean
store.put(("user_123", "memories"), "4", {"text": "Это русский"}) # Russian
store.put(("user_123", "memories"), "5", {"text": "यह रूसी है"}) # Hindi
result1 = store.search(("user_123", "memories"), query="这是中文")
result2 = store.search(("user_123", "memories"), query="これは日本語です")
result3 = store.search(("user_123", "memories"), query="이건 한국어야")
result4 = store.search(("user_123", "memories"), query="Это русский")
result5 = store.search(("user_123", "memories"), query="यह रूसी है")
assert result1[0].key == "1"
assert result2[0].key == "2"
assert result3[0].key == "3"
assert result4[0].key == "4"
assert result5[0].key == "5"
+687 -780
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,89 @@
from collections.abc import Sequence
from typing import Annotated, Literal, TypedDict
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.messages import BaseMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import END, StateGraph, add_messages
from langgraph.prebuilt import ToolNode
tools = [TavilySearchResults(max_results=1)]
model_oai = ChatOpenAI(temperature=0)
model_oai = model_oai.bind_tools(tools)
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages]
# Define the function that determines whether to continue or not
def should_continue(state):
messages = state["messages"]
last_message = messages[-1]
# If there are no tool calls, then we finish
if not last_message.tool_calls:
return "end"
# Otherwise if there is, we continue
else:
return "continue"
# Define the function that calls the model
def call_model(state, config):
model = model_oai
messages = state["messages"]
response = model.invoke(messages)
# We return a list, because this will get added to the existing list
return {"messages": [response]}
# Define the function to execute tools
tool_node = ToolNode(tools)
class ContextSchema(TypedDict):
model: Literal["anthropic", "openai"]
# Define a new graph
workflow = StateGraph(AgentState, context_schema=ContextSchema)
# Define the two nodes we will cycle between
workflow.add_node("agent", call_model)
workflow.add_node("action", tool_node)
# Set the entrypoint as `agent`
# This means that this node is the first one called
workflow.set_entry_point("agent")
# We now add a conditional edge
workflow.add_conditional_edges(
# First, we define the start node. We use `agent`.
# This means these are the edges taken after the `agent` node is called.
"agent",
# Next, we pass in the function that will determine which node is called next.
should_continue,
# Finally we pass in a mapping.
# The keys are strings, and the values are other nodes.
# END is a special node marking that the graph should finish.
# What will happen is we will call `should_continue`, and then the output of that
# will be matched against the keys in this mapping.
# Based on which one it matches, that node will then be called.
{
# If `tools`, then we call the tool node.
"continue": "action",
# Otherwise we finish.
"end": END,
},
)
# We now add a normal edge from `tools` to `agent`.
# This means that after `tools` is called, `agent` node is called next.
workflow.add_edge("action", "agent")
# Finally, we compile it!
# This compiles it into a LangChain Runnable,
# meaning you can use it as you would any other runnable
graph = workflow.compile()
@@ -0,0 +1,9 @@
[project]
name = "graph-prerelease-reqs-additional-deps"
version = "0.1.0"
description = "Test for prerelease stuff"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"langgraph==0.6.0"
]
@@ -0,0 +1,9 @@
[project]
name = "graph-prerelease-reqs-zuper-deps"
version = "0.1.0"
description = "Test for prerelease stuff"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"langchain-openai==0.3.0"
]
@@ -0,0 +1,13 @@
{
"python_version": "3.12",
"dependencies": [
".",
"./deps/additional_deps",
"./deps/zuper_deps"
],
"graphs": {
"agent": "./agent.py:graph"
},
"env": "../.env"
}
@@ -0,0 +1,14 @@
[project]
name = "graph-prerelease-reqs"
version = "0.1.0"
description = "Test for prerelease stuff"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"langchain-openai==1.0.0a2",
"langgraph==1.0.0a2",
"langchain_community>=0.3.0",
]
[tool.uv]
prerelease = "allow"
@@ -0,0 +1,89 @@
from collections.abc import Sequence
from typing import Annotated, Literal, TypedDict
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.messages import BaseMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import END, StateGraph, add_messages
from langgraph.prebuilt import ToolNode
tools = [TavilySearchResults(max_results=1)]
model_oai = ChatOpenAI(temperature=0)
model_oai = model_oai.bind_tools(tools)
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages]
# Define the function that determines whether to continue or not
def should_continue(state):
messages = state["messages"]
last_message = messages[-1]
# If there are no tool calls, then we finish
if not last_message.tool_calls:
return "end"
# Otherwise if there is, we continue
else:
return "continue"
# Define the function that calls the model
def call_model(state, config):
model = model_oai
messages = state["messages"]
response = model.invoke(messages)
# We return a list, because this will get added to the existing list
return {"messages": [response]}
# Define the function to execute tools
tool_node = ToolNode(tools)
class ContextSchema(TypedDict):
model: Literal["anthropic", "openai"]
# Define a new graph
workflow = StateGraph(AgentState, context_schema=ContextSchema)
# Define the two nodes we will cycle between
workflow.add_node("agent", call_model)
workflow.add_node("action", tool_node)
# Set the entrypoint as `agent`
# This means that this node is the first one called
workflow.set_entry_point("agent")
# We now add a conditional edge
workflow.add_conditional_edges(
# First, we define the start node. We use `agent`.
# This means these are the edges taken after the `agent` node is called.
"agent",
# Next, we pass in the function that will determine which node is called next.
should_continue,
# Finally we pass in a mapping.
# The keys are strings, and the values are other nodes.
# END is a special node marking that the graph should finish.
# What will happen is we will call `should_continue`, and then the output of that
# will be matched against the keys in this mapping.
# Based on which one it matches, that node will then be called.
{
# If `tools`, then we call the tool node.
"continue": "action",
# Otherwise we finish.
"end": END,
},
)
# We now add a normal edge from `tools` to `agent`.
# This means that after `tools` is called, `agent` node is called next.
workflow.add_edge("action", "agent")
# Finally, we compile it!
# This compiles it into a LangChain Runnable,
# meaning you can use it as you would any other runnable
graph = workflow.compile()
@@ -0,0 +1,11 @@
{
"python_version": "3.12",
"dependencies": [
"."
],
"graphs": {
"agent": "./agent.py:graph"
},
"env": "../.env"
}
@@ -0,0 +1,11 @@
[project]
name = "graph-prerelease-reqs"
version = "0.1.0"
description = "Test for prerelease stuff"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"langchain-openai==1.0.0a2",
"langgraph==1.0.0a2",
"langchain_community>=0.3.0",
]
+8 -7
View File
@@ -7,6 +7,7 @@ from langchain_core.messages import BaseMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import END, StateGraph, add_messages
from langgraph.prebuilt import ToolNode
from langgraph.runtime import Runtime
tools = [TavilySearchResults(max_results=1)]
@@ -17,6 +18,10 @@ model_anth = model_anth.bind_tools(tools)
model_oai = model_oai.bind_tools(tools)
class AgentContext(TypedDict):
model: Literal["anthropic", "openai"]
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages]
@@ -34,8 +39,8 @@ def should_continue(state):
# Define the function that calls the model
def call_model(state, config):
if config["configurable"].get("model", "anthropic") == "anthropic":
def call_model(state, runtime: Runtime[AgentContext]):
if runtime.context.get("model", "anthropic") == "anthropic":
model = model_anth
else:
model = model_oai
@@ -49,12 +54,8 @@ def call_model(state, config):
tool_node = ToolNode(tools)
class ContextSchema(TypedDict):
model: Literal["anthropic", "openai"]
# Define a new graph
workflow = StateGraph(AgentState, context_schema=ContextSchema)
workflow = StateGraph(AgentState, context_schema=AgentContext)
# Define the two nodes we will cycle between
workflow.add_node("agent", call_model)
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "https://langgra.ph/schema.json",
"python_version": "3.12",
"dependencies": [
"langchain_community",
+6 -6
View File
@@ -1,6 +1,6 @@
import asyncio
import json
from typing import Annotated, Optional
from typing import Annotated
from langchain_community.retrievers import WikipediaRetriever
from langchain_community.tools.tavily_search import TavilySearchResults
@@ -51,7 +51,7 @@ class Subsection(BaseModel):
class Section(BaseModel):
section_title: str = Field(..., title="Title of the section")
description: str = Field(..., title="Content of the section")
subsections: Optional[list[Subsection]] = Field(
subsections: list[Subsection] | None = Field(
default=None,
title="Titles and descriptions for each subsection of the Wikipedia page.",
)
@@ -201,8 +201,8 @@ def update_editor(editor, new_editor):
class InterviewState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
references: Annotated[Optional[dict], update_references]
editor: Annotated[Optional[Editor], update_editor]
references: Annotated[dict | None, update_references]
editor: Annotated[Editor | None, update_editor]
gen_qn_prompt = ChatPromptTemplate.from_messages(
@@ -321,7 +321,7 @@ async def search_engine(query: str):
async def gen_answer(
state: InterviewState,
config: Optional[RunnableConfig] = None,
config: RunnableConfig | None = None,
name: str = "Subject_Matter_Expert",
max_str_len: int = 15000,
):
@@ -437,7 +437,7 @@ class SubSection(BaseModel):
class WikiSection(BaseModel):
section_title: str = Field(..., title="Title of the section")
content: str = Field(..., title="Full content of the section")
subsections: Optional[list[Subsection]] = Field(
subsections: list[Subsection] | None = Field(
default=None,
title="Titles and descriptions for each subsection of the Wikipedia page.",
)
@@ -1,6 +1,6 @@
from collections.abc import Sequence
from pathlib import Path
from typing import Annotated, TypedDict
from typing import Annotated, Literal, TypedDict
from langchain_anthropic import ChatAnthropic
from langchain_community.tools.tavily_search import TavilySearchResults
@@ -8,6 +8,7 @@ from langchain_core.messages import BaseMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import END, StateGraph, add_messages
from langgraph.prebuilt import ToolNode
from langgraph.runtime import Runtime
tools = [TavilySearchResults(max_results=1)]
@@ -21,6 +22,10 @@ prompt = open(Path(__file__).parent.parent / "prompt.txt").read()
subprompt = open(Path(__file__).parent / "subprompt.txt").read()
class AgentContext(TypedDict):
model: Literal["anthropic", "openai"]
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages]
@@ -38,8 +43,8 @@ def should_continue(state):
# Define the function that calls the model
def call_model(state, config):
if config["configurable"].get("model", "anthropic") == "anthropic":
def call_model(state, runtime: Runtime[AgentContext]):
if runtime.context.get("model", "anthropic") == "anthropic":
model = model_anth
else:
model = model_oai
@@ -52,9 +57,8 @@ def call_model(state, config):
# Define the function to execute tools
tool_node = ToolNode(tools)
# Define a new graph
workflow = StateGraph(AgentState)
workflow = StateGraph(AgentState, context_schema=AgentContext)
# Define the two nodes we will cycle between
workflow.add_node("agent", call_model)

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