Compare commits

...

318 Commits

Author SHA1 Message Date
Sydney Runkle
cc72a8c45a release: langchain 1.2.7 (#34854) 2026-01-23 10:19:39 -05:00
Sydney Runkle
bc8620189c feat: dynamic tool registration via middleware (#34842)
dependent upon https://github.com/langchain-ai/langgraph/pull/6711

1. relax constraint in `factory.py` to allow for tools not
pre-registered in the `ModelRequest.tools` list
2. always add tool node if `wrap_tool_call` or `awrap_tool_call` is
implemented
3. add tests confirming you can register new tools at runtime in
`wrap_model_call` and execute them via `wrap_tool_call`

allows for the following pattern

```py
from langchain_core.messages import HumanMessage, ToolMessage
from langchain_core.tools import tool

from libs.langchain_v1.langchain.agents.factory import create_agent
from libs.langchain_v1.langchain.agents.middleware.types import (
    AgentMiddleware,
    ModelRequest,
    ToolCallRequest,
)


@tool
def get_weather(location: str) -> str:
    """Get the current weather for a location."""
    return f"The weather in {location} is sunny and 72°F."


@tool
def calculate_tip(bill_amount: float, tip_percentage: float = 20.0) -> str:
    """Calculate the tip amount for a bill."""
    tip = bill_amount * (tip_percentage / 100)
    return f"Tip: ${tip:.2f}, Total: ${bill_amount + tip:.2f}"

class DynamicToolMiddleware(AgentMiddleware):
    """Middleware that adds and handles a dynamic tool."""

    def wrap_model_call(self, request: ModelRequest, handler):
        updated = request.override(tools=[*request.tools, calculate_tip])
        return handler(updated)

    def wrap_tool_call(self, request: ToolCallRequest, handler):
        if request.tool_call["name"] == "calculate_tip":
            return handler(request.override(tool=calculate_tip))
        return handler(request)


agent = create_agent(model="openai:gpt-4o-mini", tools=[get_weather], middleware=[DynamicToolMiddleware()])
result = agent.invoke({
    "messages": [HumanMessage("What's the weather in NYC? Also calculate a 20% tip on a $85 bill")]
})
for msg in result["messages"]:
    msg.pretty_print()
```
2026-01-23 10:12:48 -05:00
Mason Daugherty
5a956b745f chore: update commit standards to enforce lowercase titles and required scopes (#34847) 2026-01-22 17:32:34 -05:00
Weichen Zhao
d899681040 feat(core): add XML format option for get_buffer_string (#34802)
## Summary

Add XML format option for `get_buffer_string()` to provide unambiguous
message serialization. This fixes role prefix ambiguity when message
content contains strings like "Human:" or "AI:".

  Fixes #34786

  ## Changes

- Add `format="xml"` parameter with proper XML escaping using
`quoteattr()` for attributes
- Add explicit validation for format parameter (raises `ValueError` for
invalid values)
  - Add comprehensive tests for XML format edge cases

<img width="1952" height="706" alt="image"
src="https://github.com/user-attachments/assets/1cd6f887-9365-43cf-a532-72d7addd8bad"
/>
<img width="2786" height="776" alt="image"
src="https://github.com/user-attachments/assets/a07b0db0-519c-46d7-b34b-b404237d812b"
/>

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-22 13:33:08 -05:00
Bodhi Russell Silberling
608d8cf99e fix(docs): fix typos in PR template (#34844)
- Fix 'inthe' -> 'in the' on line 20
- Fix grammar error 'unless or add' -> 'or add' on line 30

(Replace this entire block of text)

Read the full contributing guidelines:
https://docs.langchain.com/oss/python/contributing/overview

Thank you for contributing to LangChain! Follow these steps to have your
pull request considered as ready for review.

1. PR title: Should follow the format: TYPE(SCOPE): DESCRIPTION

  - Examples:
    - fix(anthropic): resolve flag parsing error
    - feat(core): add multi-tenant support
    - test(openai): update API usage tests
- Allowed TYPE and SCOPE values:
https://github.com/langchain-ai/langchain/blob/master/.github/workflows/pr_lint.yml#L15-L33

2. PR description:

  - Write 1-2 sentences summarizing the change.
- If this PR addresses a specific issue, please include "Fixes
#ISSUE_NUMBER" in the description to automatically close the issue when
the PR is merged.
  - If there are any breaking changes, please clearly describe them.
- If this PR depends on another PR being merged first, please include
"Depends on #PR_NUMBER" inthe description.

3. Run `make format`, `make lint` and `make test` from the root of the
package(s) you've modified.

  - We will not consider a PR unless these three are passing in CI.

Additional guidelines:

- We ask that if you use generative AI for your contribution, you
include a disclaimer.
- PRs should not touch more than one package unless absolutely
necessary.
- Do not update the `uv.lock` files unless or add dependencies to
`pyproject.toml` files (even optional ones) unless you have explicit
permission to do so by a maintainer.
2026-01-22 13:31:52 -05:00
XXt
689ce96016 docs: add missing module-level docstrings to partner integrations (#34838)
docs: add missing module-level docstrings to partner integrations

Added module-level docstrings to 6 partner integration __init__.py files
  that were missing documentation:
2026-01-22 12:05:59 -05:00
Mason Daugherty
a1df299123 fix(langchain): strip trailing whitespace from the summarization prompt (#34835) 2026-01-21 17:09:02 -05:00
Mason Daugherty
1d7a2690a2 fix(langchain): improve grammar in SummarizationMiddleware system prompt (#34834)
I had a low-grade aneurysm trying to read this
2026-01-21 17:05:02 -05:00
Eugene Yurtsev
5fa708fb14 chore(core): relax packaging constraints (#34832)
https://github.com/langchain-ai/langchain/issues/34831
2026-01-21 19:02:34 +00:00
dependabot[bot]
66038386d4 chore(deps-dev): bump setuptools from 67.8.0 to 78.1.1 in /libs/core in the uv group across 1 directory (#34825)
Bumps the uv group with 1 update in the /libs/core directory:
[setuptools](https://github.com/pypa/setuptools).

Updates `setuptools` from 67.8.0 to 78.1.1
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/pypa/setuptools/blob/main/NEWS.rst">setuptools's
changelog</a>.</em></p>
<blockquote>
<h1>v78.1.1</h1>
<h2>Bugfixes</h2>
<ul>
<li>More fully sanitized the filename in PackageIndex._download. (<a
href="https://redirect.github.com/pypa/setuptools/issues/4946">#4946</a>)</li>
</ul>
<h1>v78.1.0</h1>
<h2>Features</h2>
<ul>
<li>Restore access to _get_vc_env with a warning. (<a
href="https://redirect.github.com/pypa/setuptools/issues/4874">#4874</a>)</li>
</ul>
<h1>v78.0.2</h1>
<h2>Bugfixes</h2>
<ul>
<li>Postponed removals of deprecated dash-separated and uppercase fields
in <code>setup.cfg</code>.
All packages with deprecated configurations are advised to move before
2026. (<a
href="https://redirect.github.com/pypa/setuptools/issues/4911">#4911</a>)</li>
</ul>
<h1>v78.0.1</h1>
<h2>Misc</h2>
<ul>
<li><a
href="https://redirect.github.com/pypa/setuptools/issues/4909">#4909</a></li>
</ul>
<h1>v78.0.0</h1>
<h2>Bugfixes</h2>
<ul>
<li>Reverted distutils changes that broke the monkey patching of command
classes. (<a
href="https://redirect.github.com/pypa/setuptools/issues/4902">#4902</a>)</li>
</ul>
<h2>Deprecations and Removals</h2>
<ul>
<li>Setuptools no longer accepts options containing uppercase or dash
characters in <code>setup.cfg</code>.</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="8e4868a036"><code>8e4868a</code></a>
Bump version: 78.1.0 → 78.1.1</li>
<li><a
href="100e9a61ad"><code>100e9a6</code></a>
Merge pull request <a
href="https://redirect.github.com/pypa/setuptools/issues/4951">#4951</a></li>
<li><a
href="8faf1d7e0c"><code>8faf1d7</code></a>
Add news fragment.</li>
<li><a
href="2ca4a9fe47"><code>2ca4a9f</code></a>
Rely on re.sub to perform the decision in one expression.</li>
<li><a
href="e409e80029"><code>e409e80</code></a>
Extract _sanitize method for sanitizing the filename.</li>
<li><a
href="250a6d1797"><code>250a6d1</code></a>
Add a check to ensure the name resolves relative to the tmpdir.</li>
<li><a
href="d8390feaa9"><code>d8390fe</code></a>
Extract _resolve_download_filename with test.</li>
<li><a
href="4e1e89392d"><code>4e1e893</code></a>
Merge <a
href="https://github.com/jaraco/skeleton">https://github.com/jaraco/skeleton</a></li>
<li><a
href="3a3144f0d2"><code>3a3144f</code></a>
Fix typo: <code>pyproject.license</code> -&gt;
<code>project.license</code> (<a
href="https://redirect.github.com/pypa/setuptools/issues/4931">#4931</a>)</li>
<li><a
href="d751068fd2"><code>d751068</code></a>
Fix typo: pyproject.license -&gt; project.license</li>
<li>Additional commits viewable in <a
href="https://github.com/pypa/setuptools/compare/v67.8.0...v78.1.1">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-20 20:55:19 -05:00
Mason Daugherty
7dc2c777ea feat(infra): org membership checker (#34822) 2026-01-20 17:57:17 -05:00
Mason Daugherty
9b8c211f98 test(fireworks): fix model name (#34819)
i guess we were using an outdated/no longer supported alias
2026-01-20 16:32:45 -05:00
dependabot[bot]
a6e8c83878 chore(deps): bump langgraph-checkpoint from 2.1.2 to 3.0.0 in /libs/cli in the uv group across 1 directory (#34787)
Bumps the uv group with 1 update in the /libs/cli directory:
[langgraph-checkpoint](https://github.com/langchain-ai/langgraph).

Updates `langgraph-checkpoint` from 2.1.2 to 3.0.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/langchain-ai/langgraph/releases">langgraph-checkpoint's
releases</a>.</em></p>
<blockquote>
<h2>checkpoint==3.0.0</h2>
<p>Changes since checkpoint==2.1.2</p>
<ul>
<li>release: Checkpointers 3.0 (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6313">#6313</a>)</li>
<li>chore: Restrict &quot;json&quot; type deserialization (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6269">#6269</a>)</li>
<li>feat: adding cursory Python 3.14 support (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6298">#6298</a>)</li>
<li>style: fixes for ref docs (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6297">#6297</a>)</li>
<li>chore: drop Python 3.9 (and syntax) (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6289">#6289</a>)</li>
<li>docs: style linting (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6260">#6260</a>)</li>
<li>fix: rename away from LangGraph Platform (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6281">#6281</a>)</li>
</ul>
<h2>checkpointpostgres==3.0.0</h2>
<p>Changes since checkpointpostgres==2.0.25</p>
<ul>
<li>release: Checkpointers 3.0 (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6313">#6313</a>)</li>
<li>feat: adding cursory Python 3.14 support (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6298">#6298</a>)</li>
<li>chore: drop Python 3.9 (and syntax) (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6289">#6289</a>)</li>
<li>docs: style linting (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6260">#6260</a>)</li>
</ul>
<h2>checkpointsqlite==3.0.0</h2>
<p>Changes since checkpointsqlite==2.0.11</p>
<ul>
<li>release: Checkpointers 3.0 (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6313">#6313</a>)</li>
<li>chore: Restrict &quot;json&quot; type deserialization (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6269">#6269</a>)</li>
<li>feat: adding cursory Python 3.14 support (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6298">#6298</a>)</li>
<li>chore: drop Python 3.9 (and syntax) (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6289">#6289</a>)</li>
<li>docs: style linting (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6260">#6260</a>)</li>
<li>chore(checkpoint): bump patch version (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6244">#6244</a>)</li>
<li>chore(deps): upgrade dependencies with <code>uv lock
--upgrade</code> (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6211">#6211</a>)</li>
<li>fix(checkpoint-sqlite): Handle TTL refresh correctly in
AsyncSqliteStore.asearch (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/5213">#5213</a>)</li>
<li>chore(deps): upgrade dependencies with <code>uv lock
--upgrade</code> (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6176">#6176</a>)</li>
<li>test: Add tests for before and limit parameters for list SqliteSaver
(<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/5816">#5816</a>)</li>
<li>chore(deps): upgrade dependencies with <code>uv lock
--upgrade</code> (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6146">#6146</a>)</li>
<li>fix(checkpoint): preserve non-ascii text in InMemoryStore embeddings
(<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6111">#6111</a>)</li>
<li>feat(langgraph): implement redis node level cache (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/5834">#5834</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="fca3e4513c"><code>fca3e45</code></a>
release: Checkpointers 3.0 (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6313">#6313</a>)</li>
<li><a
href="c5744f583b"><code>c5744f5</code></a>
chore: Restrict &quot;json&quot; type deserialization (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6269">#6269</a>)</li>
<li><a
href="c4144bb48f"><code>c4144bb</code></a>
release: langgraph + langgraph-prebuilt v1.0.0 (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6300">#6300</a>)</li>
<li><a
href="2c3e380a35"><code>2c3e380</code></a>
feat: adding cursory Python 3.14 support (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6298">#6298</a>)</li>
<li><a
href="7e666b58cd"><code>7e666b5</code></a>
style: fixes for ref docs (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6297">#6297</a>)</li>
<li><a
href="3f400b38d1"><code>3f400b3</code></a>
fix(cli): install local deps in editable mode (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6294">#6294</a>)</li>
<li><a
href="6527df688c"><code>6527df6</code></a>
chore: release rcs for prebuilt + langgraph (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6296">#6296</a>)</li>
<li><a
href="aec841bd2a"><code>aec841b</code></a>
chore(prebuilt): un-deprecate tool node for now (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6295">#6295</a>)</li>
<li><a
href="2d3121a17c"><code>2d3121a</code></a>
chore: drop Python 3.9 (and syntax) (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6289">#6289</a>)</li>
<li><a
href="abb96c0e2f"><code>abb96c0</code></a>
chore(cli): re-word schema arguments (<a
href="https://redirect.github.com/langchain-ai/langgraph/issues/6243">#6243</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/langchain-ai/langgraph/compare/checkpoint==2.1.2...checkpoint==3.0.0">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-17 01:07:59 -05:00
Mason Daugherty
0d3c4e9817 docs(langchain): nit (#34788) 2026-01-17 01:07:33 -05:00
dependabot[bot]
89e1594196 chore(deps): bump the uv group across 5 directories with 5 updates (#34785)
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 <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/langchain-ai/langchain/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-16 22:28:13 -05:00
dependabot[bot]
a84722e2d7 chore(deps): bump the uv group across 8 directories with 6 updates (#34773)
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 <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/langchain-ai/langchain/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-16 14:33:31 -05:00
Mason Daugherty
7e40de7800 fix(infra): block release on deepagents tests failure, bound min version for testing (#34784)
`deepagents` requires Python >= 3.11. Note: this won't display in the
action title in the UI if requesting 3.10, and it will also still show
`(3.10, 3.13)` since that's what the integration packages are testing
against. `deepagents` matrix title will be accurate.
2026-01-16 14:32:29 -05:00
Mason Daugherty
97b3d6dae1 chore(core, langchain): add version consistency check pre-commit hooks (#34782) 2026-01-16 14:24:46 -05:00
Mason Daugherty
624799838c release(langchain): 1.2.6 (#34781) 2026-01-16 14:19:33 -05:00
Mason Daugherty
cb2b85bb1d feat(infra): test against deepagents (#34779)
Add testing for `deepagents` both (1) on scheduled interval and (2)
release of `langchain-core` or `langchain` to ensure compatibility.
Should catch breaking changes early.
2026-01-16 14:13:22 -05:00
Mason Daugherty
5581600e9e refactor(infra): integration_tests.yml (#34778)
update the job names and structure for better readability - non breaking
2026-01-16 13:36:21 -05:00
Mason Daugherty
28eceabd8b chore(infra): add warning for function signature changes to agent files (#34776) 2026-01-16 11:52:26 -05:00
Mason Daugherty
ca00e4fed9 fix(langchain): SummarizationMiddleware signature mismatch & config invocation (#34775)
Re: #34763
2026-01-16 11:46:10 -05:00
ccurme
57279c7b81 release(langchain): 1.2.5 (#34772) 2026-01-16 11:07:20 -05:00
Mason Daugherty
09c3c52fd0 fix(langchain): add metadata configuration to summarization model invocation (#34763)
We need to set `{"metadata": {"lc_source": "summarization"}}` on the
invocation so that consumers (e.g. `deepagents-cli`) can see that a
summarization LLM call is being made, and therefore take any necessary
actions (such as updating the status line to say `'Currently
summarizing...'`

See https://github.com/langchain-ai/deepagents/pull/742 for more

Related to #34693 (but for outbound)
2026-01-15 15:39:12 -05:00
ccurme
8a257e777b feat(langchain): update summarization prompt (#34754) 2026-01-15 14:17:55 -05:00
Sydney Runkle
73ebaddcf0 chore: add tests for agent name metadata when streaming (#34764) 2026-01-15 15:38:15 +00:00
Sydney Runkle
ee6fce5586 Revert "metadata"
This reverts commit 13301a779e.
2026-01-15 10:13:35 -05:00
Sydney Runkle
13301a779e metadata 2026-01-15 10:12:28 -05:00
Mason Daugherty
331d57b429 fix(infra): remove file based label conflicts (#34759) 2026-01-14 22:31:05 -05:00
Mason Daugherty
d4663be53d fix(infra): remove edited from PR labeler triggers (#34760)
file-based only needs to update on new commits (`synchronize`)
2026-01-14 22:28:52 -05:00
Mason Daugherty
0ab5010bcf chore: refine issue templates (#34758) 2026-01-14 21:25:18 -05:00
Mason Daugherty
3899154daf docs(core): enhance docstring for RunnableConfig for clarity on total=False (#34756) 2026-01-14 16:38:33 -05:00
Sydney Runkle
1d60235b1b release: langchain 1.2.4 (#34755) 2026-01-14 14:24:31 -05:00
Sydney Runkle
b522ce7b31 chore(langchain): add agent name metadata (#34743)
Adding `lc_agent_name` to default agent config so that said metadata can
be used in LS for a nicer tracing devx

<img width="801" height="304" alt="Screenshot 2026-01-13 at 5 17 07 PM"
src="https://github.com/user-attachments/assets/0c72a52d-4b56-4ace-bf27-89680ebb4e39"
/>
2026-01-14 14:57:35 +00:00
dependabot[bot]
3356d05557 chore(deps): bump the uv group across 3 directories with 1 update (#34741)
Bumps the uv group with 1 update in the /libs/langchain directory:
[filelock](https://github.com/tox-dev/py-filelock).
Bumps the uv group with 1 update in the /libs/text-splitters directory:
[filelock](https://github.com/tox-dev/py-filelock).
Bumps the uv group with 1 update in the /libs/partners/chroma directory:
[filelock](https://github.com/tox-dev/py-filelock).

Updates `filelock` from 3.19.1 to 3.20.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tox-dev/py-filelock/releases">filelock's
releases</a>.</em></p>
<blockquote>
<h2>3.20.3</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<ul>
<li>Fix TOCTOU symlink vulnerability in SoftFileLock by <a
href="https://github.com/gaborbernat"><code>@​gaborbernat</code></a> in
<a
href="https://redirect.github.com/tox-dev/filelock/pull/465">tox-dev/filelock#465</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/tox-dev/filelock/compare/3.20.2...3.20.3">https://github.com/tox-dev/filelock/compare/3.20.2...3.20.3</a></p>
<h2>3.20.2</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<ul>
<li>Support Unix systems without O_NOFOLLOW by <a
href="https://github.com/mwilliamson"><code>@​mwilliamson</code></a> in
<a
href="https://redirect.github.com/tox-dev/filelock/pull/463">tox-dev/filelock#463</a></li>
<li>[pre-commit.ci] pre-commit autoupdate by <a
href="https://github.com/pre-commit-ci"><code>@​pre-commit-ci</code></a>[bot]
in <a
href="https://redirect.github.com/tox-dev/filelock/pull/464">tox-dev/filelock#464</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/mwilliamson"><code>@​mwilliamson</code></a>
made their first contribution in <a
href="https://redirect.github.com/tox-dev/filelock/pull/463">tox-dev/filelock#463</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/tox-dev/filelock/compare/3.20.1...3.20.2">https://github.com/tox-dev/filelock/compare/3.20.1...3.20.2</a></p>
<h2>3.20.1</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<ul>
<li>CVE-2025-68146: Fix TOCTOU symlink vulnerability in lock file
creation by <a
href="https://github.com/gaborbernat"><code>@​gaborbernat</code></a> in
<a
href="https://redirect.github.com/tox-dev/filelock/pull/461">tox-dev/filelock#461</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/tox-dev/filelock/compare/3.20.0...3.20.1">https://github.com/tox-dev/filelock/compare/3.20.0...3.20.1</a></p>
<h2>3.20.0</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<ul>
<li>Add tox.toml to sdist by <a
href="https://github.com/mtelka"><code>@​mtelka</code></a> in <a
href="https://redirect.github.com/tox-dev/filelock/pull/436">tox-dev/filelock#436</a></li>
<li>Update docs with example by <a
href="https://github.com/znichollscr"><code>@​znichollscr</code></a> in
<a
href="https://redirect.github.com/tox-dev/filelock/pull/438">tox-dev/filelock#438</a></li>
<li>Add 3.14 support and drop 3.9 by <a
href="https://github.com/gaborbernat"><code>@​gaborbernat</code></a> in
<a
href="https://redirect.github.com/tox-dev/filelock/pull/448">tox-dev/filelock#448</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/mtelka"><code>@​mtelka</code></a> made
their first contribution in <a
href="https://redirect.github.com/tox-dev/filelock/pull/436">tox-dev/filelock#436</a></li>
<li><a
href="https://github.com/znichollscr"><code>@​znichollscr</code></a>
made their first contribution in <a
href="https://redirect.github.com/tox-dev/filelock/pull/438">tox-dev/filelock#438</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/tox-dev/filelock/compare/3.19.1...3.20.0">https://github.com/tox-dev/filelock/compare/3.19.1...3.20.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="41b42dd2c7"><code>41b42dd</code></a>
Fix TOCTOU symlink vulnerability in SoftFileLock (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/465">#465</a>)</li>
<li><a
href="f2e7d4046b"><code>f2e7d40</code></a>
[pre-commit.ci] pre-commit autoupdate (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/464">#464</a>)</li>
<li><a
href="50888548eb"><code>5088854</code></a>
Support Unix systems without O_NOFOLLOW (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/463">#463</a>)</li>
<li><a
href="377f62251d"><code>377f622</code></a>
[pre-commit.ci] pre-commit autoupdate (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/460">#460</a>)</li>
<li><a
href="4724d7f8c3"><code>4724d7f</code></a>
Fix TOCTOU symlink vulnerability in lock file creation (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/461">#461</a>)</li>
<li><a
href="cb69414a23"><code>cb69414</code></a>
Bump actions/upload-artifact from 5 to 6 (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/459">#459</a>)</li>
<li><a
href="0769294f14"><code>0769294</code></a>
Bump actions/download-artifact from 6 to 7 (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/458">#458</a>)</li>
<li><a
href="414193a188"><code>414193a</code></a>
[pre-commit.ci] pre-commit autoupdate (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/457">#457</a>)</li>
<li><a
href="1456797beb"><code>1456797</code></a>
[pre-commit.ci] pre-commit autoupdate (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/456">#456</a>)</li>
<li><a
href="8d6bf90af3"><code>8d6bf90</code></a>
Bump actions/checkout from 5 to 6 (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/455">#455</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/tox-dev/py-filelock/compare/3.19.1...3.20.3">compare
view</a></li>
</ul>
</details>
<br />

Updates `filelock` from 3.19.1 to 3.20.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tox-dev/py-filelock/releases">filelock's
releases</a>.</em></p>
<blockquote>
<h2>3.20.3</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<ul>
<li>Fix TOCTOU symlink vulnerability in SoftFileLock by <a
href="https://github.com/gaborbernat"><code>@​gaborbernat</code></a> in
<a
href="https://redirect.github.com/tox-dev/filelock/pull/465">tox-dev/filelock#465</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/tox-dev/filelock/compare/3.20.2...3.20.3">https://github.com/tox-dev/filelock/compare/3.20.2...3.20.3</a></p>
<h2>3.20.2</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<ul>
<li>Support Unix systems without O_NOFOLLOW by <a
href="https://github.com/mwilliamson"><code>@​mwilliamson</code></a> in
<a
href="https://redirect.github.com/tox-dev/filelock/pull/463">tox-dev/filelock#463</a></li>
<li>[pre-commit.ci] pre-commit autoupdate by <a
href="https://github.com/pre-commit-ci"><code>@​pre-commit-ci</code></a>[bot]
in <a
href="https://redirect.github.com/tox-dev/filelock/pull/464">tox-dev/filelock#464</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/mwilliamson"><code>@​mwilliamson</code></a>
made their first contribution in <a
href="https://redirect.github.com/tox-dev/filelock/pull/463">tox-dev/filelock#463</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/tox-dev/filelock/compare/3.20.1...3.20.2">https://github.com/tox-dev/filelock/compare/3.20.1...3.20.2</a></p>
<h2>3.20.1</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<ul>
<li>CVE-2025-68146: Fix TOCTOU symlink vulnerability in lock file
creation by <a
href="https://github.com/gaborbernat"><code>@​gaborbernat</code></a> in
<a
href="https://redirect.github.com/tox-dev/filelock/pull/461">tox-dev/filelock#461</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/tox-dev/filelock/compare/3.20.0...3.20.1">https://github.com/tox-dev/filelock/compare/3.20.0...3.20.1</a></p>
<h2>3.20.0</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<ul>
<li>Add tox.toml to sdist by <a
href="https://github.com/mtelka"><code>@​mtelka</code></a> in <a
href="https://redirect.github.com/tox-dev/filelock/pull/436">tox-dev/filelock#436</a></li>
<li>Update docs with example by <a
href="https://github.com/znichollscr"><code>@​znichollscr</code></a> in
<a
href="https://redirect.github.com/tox-dev/filelock/pull/438">tox-dev/filelock#438</a></li>
<li>Add 3.14 support and drop 3.9 by <a
href="https://github.com/gaborbernat"><code>@​gaborbernat</code></a> in
<a
href="https://redirect.github.com/tox-dev/filelock/pull/448">tox-dev/filelock#448</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/mtelka"><code>@​mtelka</code></a> made
their first contribution in <a
href="https://redirect.github.com/tox-dev/filelock/pull/436">tox-dev/filelock#436</a></li>
<li><a
href="https://github.com/znichollscr"><code>@​znichollscr</code></a>
made their first contribution in <a
href="https://redirect.github.com/tox-dev/filelock/pull/438">tox-dev/filelock#438</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/tox-dev/filelock/compare/3.19.1...3.20.0">https://github.com/tox-dev/filelock/compare/3.19.1...3.20.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="41b42dd2c7"><code>41b42dd</code></a>
Fix TOCTOU symlink vulnerability in SoftFileLock (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/465">#465</a>)</li>
<li><a
href="f2e7d4046b"><code>f2e7d40</code></a>
[pre-commit.ci] pre-commit autoupdate (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/464">#464</a>)</li>
<li><a
href="50888548eb"><code>5088854</code></a>
Support Unix systems without O_NOFOLLOW (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/463">#463</a>)</li>
<li><a
href="377f62251d"><code>377f622</code></a>
[pre-commit.ci] pre-commit autoupdate (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/460">#460</a>)</li>
<li><a
href="4724d7f8c3"><code>4724d7f</code></a>
Fix TOCTOU symlink vulnerability in lock file creation (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/461">#461</a>)</li>
<li><a
href="cb69414a23"><code>cb69414</code></a>
Bump actions/upload-artifact from 5 to 6 (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/459">#459</a>)</li>
<li><a
href="0769294f14"><code>0769294</code></a>
Bump actions/download-artifact from 6 to 7 (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/458">#458</a>)</li>
<li><a
href="414193a188"><code>414193a</code></a>
[pre-commit.ci] pre-commit autoupdate (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/457">#457</a>)</li>
<li><a
href="1456797beb"><code>1456797</code></a>
[pre-commit.ci] pre-commit autoupdate (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/456">#456</a>)</li>
<li><a
href="8d6bf90af3"><code>8d6bf90</code></a>
Bump actions/checkout from 5 to 6 (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/455">#455</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/tox-dev/py-filelock/compare/3.19.1...3.20.3">compare
view</a></li>
</ul>
</details>
<br />

Updates `filelock` from 3.19.1 to 3.20.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tox-dev/py-filelock/releases">filelock's
releases</a>.</em></p>
<blockquote>
<h2>3.20.3</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<ul>
<li>Fix TOCTOU symlink vulnerability in SoftFileLock by <a
href="https://github.com/gaborbernat"><code>@​gaborbernat</code></a> in
<a
href="https://redirect.github.com/tox-dev/filelock/pull/465">tox-dev/filelock#465</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/tox-dev/filelock/compare/3.20.2...3.20.3">https://github.com/tox-dev/filelock/compare/3.20.2...3.20.3</a></p>
<h2>3.20.2</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<ul>
<li>Support Unix systems without O_NOFOLLOW by <a
href="https://github.com/mwilliamson"><code>@​mwilliamson</code></a> in
<a
href="https://redirect.github.com/tox-dev/filelock/pull/463">tox-dev/filelock#463</a></li>
<li>[pre-commit.ci] pre-commit autoupdate by <a
href="https://github.com/pre-commit-ci"><code>@​pre-commit-ci</code></a>[bot]
in <a
href="https://redirect.github.com/tox-dev/filelock/pull/464">tox-dev/filelock#464</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/mwilliamson"><code>@​mwilliamson</code></a>
made their first contribution in <a
href="https://redirect.github.com/tox-dev/filelock/pull/463">tox-dev/filelock#463</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/tox-dev/filelock/compare/3.20.1...3.20.2">https://github.com/tox-dev/filelock/compare/3.20.1...3.20.2</a></p>
<h2>3.20.1</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<ul>
<li>CVE-2025-68146: Fix TOCTOU symlink vulnerability in lock file
creation by <a
href="https://github.com/gaborbernat"><code>@​gaborbernat</code></a> in
<a
href="https://redirect.github.com/tox-dev/filelock/pull/461">tox-dev/filelock#461</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/tox-dev/filelock/compare/3.20.0...3.20.1">https://github.com/tox-dev/filelock/compare/3.20.0...3.20.1</a></p>
<h2>3.20.0</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<ul>
<li>Add tox.toml to sdist by <a
href="https://github.com/mtelka"><code>@​mtelka</code></a> in <a
href="https://redirect.github.com/tox-dev/filelock/pull/436">tox-dev/filelock#436</a></li>
<li>Update docs with example by <a
href="https://github.com/znichollscr"><code>@​znichollscr</code></a> in
<a
href="https://redirect.github.com/tox-dev/filelock/pull/438">tox-dev/filelock#438</a></li>
<li>Add 3.14 support and drop 3.9 by <a
href="https://github.com/gaborbernat"><code>@​gaborbernat</code></a> in
<a
href="https://redirect.github.com/tox-dev/filelock/pull/448">tox-dev/filelock#448</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/mtelka"><code>@​mtelka</code></a> made
their first contribution in <a
href="https://redirect.github.com/tox-dev/filelock/pull/436">tox-dev/filelock#436</a></li>
<li><a
href="https://github.com/znichollscr"><code>@​znichollscr</code></a>
made their first contribution in <a
href="https://redirect.github.com/tox-dev/filelock/pull/438">tox-dev/filelock#438</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/tox-dev/filelock/compare/3.19.1...3.20.0">https://github.com/tox-dev/filelock/compare/3.19.1...3.20.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="41b42dd2c7"><code>41b42dd</code></a>
Fix TOCTOU symlink vulnerability in SoftFileLock (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/465">#465</a>)</li>
<li><a
href="f2e7d4046b"><code>f2e7d40</code></a>
[pre-commit.ci] pre-commit autoupdate (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/464">#464</a>)</li>
<li><a
href="50888548eb"><code>5088854</code></a>
Support Unix systems without O_NOFOLLOW (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/463">#463</a>)</li>
<li><a
href="377f62251d"><code>377f622</code></a>
[pre-commit.ci] pre-commit autoupdate (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/460">#460</a>)</li>
<li><a
href="4724d7f8c3"><code>4724d7f</code></a>
Fix TOCTOU symlink vulnerability in lock file creation (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/461">#461</a>)</li>
<li><a
href="cb69414a23"><code>cb69414</code></a>
Bump actions/upload-artifact from 5 to 6 (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/459">#459</a>)</li>
<li><a
href="0769294f14"><code>0769294</code></a>
Bump actions/download-artifact from 6 to 7 (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/458">#458</a>)</li>
<li><a
href="414193a188"><code>414193a</code></a>
[pre-commit.ci] pre-commit autoupdate (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/457">#457</a>)</li>
<li><a
href="1456797beb"><code>1456797</code></a>
[pre-commit.ci] pre-commit autoupdate (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/456">#456</a>)</li>
<li><a
href="8d6bf90af3"><code>8d6bf90</code></a>
Bump actions/checkout from 5 to 6 (<a
href="https://redirect.github.com/tox-dev/py-filelock/issues/455">#455</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/tox-dev/py-filelock/compare/3.19.1...3.20.3">compare
view</a></li>
</ul>
</details>
<br />


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 <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/langchain-ai/langchain/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 15:59:08 -05:00
Mason Daugherty
1ead03c79d feat(standard-tests): ensure final chunk has chunk_position='last' (#34704) 2026-01-13 10:55:21 -05:00
Mason Daugherty
2ff1d23bba docs(core): clean up callbacks param descriptions (#34738)
many were unnecessarily verbose
2026-01-13 10:25:50 -05:00
Mason Daugherty
3289ee20ed fix(core): correctly guard against non-text-block types (#34729)
# Before

```python
if isinstance(block, dict) and "text" in block:
    text = block["text"]
    break
```
Extracts text from any `dict` with a `'text'` key, including
thinking/reasoning blocks.

# After

```python
if isinstance(block, dict) and "text" in block:
    block_type = block.get("type")
    if block_type is None or block_type == "text":
        text = block["text"]
        break
```

Skips blocks with explicit non-text types (e.g., `type: 'thinking'`).

# Justification

Models like Gemini 3 return structured content with multiple block
types:

```python
[
    {"type": "thinking", "text": "let me reason..."},
    {"type": "text", "text": "The answer is 42"}
]
```

The old logic extracted `'let me reason...'` (the thinking block)
because it matched first. The new logic skips it and correctly extracts
`'The answer is 42'`.

The `ChatGeneration.text` field is used by `on_llm_new_token(token,
chunk=chunk)` callbacks during streaming. Consequently, it would get
tokens incorrectly for reasoning blocks.

Related: #34727
2026-01-13 10:11:00 -05:00
Mason Daugherty
3d687ea8fb chore: update twitter URLs (#34736) 2026-01-13 01:54:11 -05:00
David Fernandez
5b401fa414 refactor(core): generalize comma_list utility to support any Iterable (#34714)
Updates `comma_list` in `libs/core/langchain_core/utils/strings.py` to
accept `Iterable[Any]` instead of `list[Any]`, making the utility more
flexible.

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
2026-01-12 20:26:59 -05:00
amelvil2-ford
381f0a3971 test(langchain): delete ontotext graphdb test (#34710)
The code behind this functionality has been moved to the
langchain-community repository, and there are tests there to exercise
this functionality.

Fixes #33392

Co-authored-by: amelvil2 <amelvil2>
2026-01-12 20:26:45 -05:00
skyvanguard
34e867e92b fix(core): add explicit tags parameter to sync LLMManagerMixin methods (#34722)
## Summary
- Adds explicit `tags: list[str] | None = None` parameter to sync
`LLMManagerMixin` methods
- Aligns sync methods with their async counterparts in
`AsyncCallbackHandler`

## Changes
Added `tags` parameter to:
- `on_llm_new_token`
- `on_llm_end`
- `on_llm_error`

## Why
- Sync handlers receive `tags` via `**kwargs`, but it was undocumented
in the method signature
- Async handlers already have `tags` explicitly documented
- This improves IDE autocompletion and type hints for sync handlers

Closes #34720

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

Co-authored-by: skyvanguard <skyvanguard@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 20:19:05 -05:00
Mason Daugherty
0b99ca4fcd docs(core): enhance docstrings for ToolCall and ToolCallChunk (#34719) 2026-01-12 15:50:28 -05:00
Sydney Runkle
5799aa1045 chore: add tests for private state attr use (really, lack thereof) (#34725)
If a user injects a private state value, it should be ignored (and is)!
2026-01-12 20:47:44 +00:00
Mason Daugherty
cf5b011055 fix(infra): improve package section extraction regex in auto-labeling (#34724)
was broken for privileged template
2026-01-12 15:40:31 -05:00
Mason Daugherty
2ab225769d fix(infra): exclude .ambr files from trailing whitespace check (#34723) 2026-01-12 15:35:45 -05:00
Mason Daugherty
1dc2600cd4 docs(langchain): clarify model ID usage for reliable behavior (#34718)
Clarify the preference for using exact model IDs from provider
documentation over aliases to ensure reliable behavior in face of
upstream backend changes.
2026-01-12 15:10:59 -05:00
David Fernandez
6bcc4a1af1 docs: Fix TODO in Ollama compatibility docstring (#34713)
Replaces a leftover TODO in
`libs/partners/ollama/langchain_ollama/_compat.py` with a proper return
value description.
2026-01-12 12:52:25 -05:00
ccurme
725d204b95 fix(langchain): tag messages generated from summarization (#34693) 2026-01-12 09:26:09 -05:00
Shreyansh Singh Gautam
2ef23882d2 fix(core): add tool_call_id to on_tool_error event data (#33731)
# Add `tool_call_id` to `on_tool_error` event data

## Summary

This PR addresses issue #33597 by adding `tool_call_id` to the
`on_tool_error` callback event data. This enables users to link tool
errors to specific tool calls in stateless agent implementations, which
is essential for building OpenAI-compatible APIs and tracking tool
execution flows.

## Problem

When streaming events using `astream_events` with `version="v2"`, the
`on_tool_error` event only included the error and input data, but lacked
the `tool_call_id`. This made it difficult to:

- Link errors to specific tool calls in stateless agent scenarios
- Implement OpenAI-compatible APIs that require tool call tracking
- Track tool execution flows when using `run_id` is not sufficient

## Solution

The fix adds `tool_call_id` propagation through the callback chain:

1. **Pass `tool_call_id` to callbacks**: Updated `BaseTool.run()` and
`BaseTool.arun()` to pass `tool_call_id` to both `on_tool_start` and
`on_tool_error` callbacks
2. **Store in event stream handler**: Modified
`_AstreamEventsCallbackHandler` to store `tool_call_id` in run info
during `on_tool_start`
3. **Include in error events**: Updated `on_tool_error` handler to
extract and include `tool_call_id` in the event data

## Changes

- **`libs/core/langchain_core/tools/base.py`**:
- Pass `tool_call_id` to `on_tool_start` in both sync and async methods
  - Pass `tool_call_id` to `on_tool_error` when errors occur

- **`libs/core/langchain_core/tracers/event_stream.py`**:
  - Store `tool_call_id` in run info during `on_tool_start`
  - Extract `tool_call_id` from kwargs or run info in `on_tool_error`
  - Include `tool_call_id` in the `on_tool_error` event data

## Testing

The fix was verified by:

1. Direct tool invocation: Confirmed `tool_call_id` appears in
`on_tool_error` event data when calling tools directly
2. Agent integration: Tested with `create_agent` to ensure
`tool_call_id` is present in error events during agent execution

```python
# Example verification
async for event in agent.astream_events(
    {"messages": "Please demonstrate a tool error"},
    version="v2",
):
    if event["event"] == "on_tool_error":
        assert "tool_call_id" in event["data"]  # ✓ Now passes
        print(event["data"]["tool_call_id"])
```

## Backward Compatibility

-  Fully backward compatible: `tool_call_id` is optional (can be
`None`)
-  No breaking changes: All changes are additive
-  Existing code continues to work without modification

## Related Issues

Fixes #33597

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
2026-01-10 02:35:13 -05:00
Bhavesh Sharma
e261924030 fix(core): improve error message for missing title in JSON schema functions (#34683)
Changes Created
I have fixed the issue where a generic and misleading error message was
displayed when a JSON schema was missing the top-level
title
 key.

[Fix: Improve error message for missing title in JSON schema
functions](https://github.com/Bhavesh007Sharma/langchain/tree/fix-json-schema-title-error)
File Modified: 
libs/core/langchain_core/utils/function_calling.py

I updated the 
convert_to_openai_function
 validation logic to specifically check for 
dict
 inputs that look like schemas (
type
 or 
properties
 keys present) but are missing the 
title
 key.

# Before (Generic Error)
raise ValueError(
    f"Unsupported function\n\n{function}\n\nFunctions must be passed in"
" as Dict, pydantic.BaseModel, or Callable. If they're a dict they must"
" either be in OpenAI function format or valid JSON schema with
top-level"
    " 'title' and 'description' keys."
)
# After (Specific Error)
if isinstance(function, dict) and ("type" in function or "properties" in
function):
    msg = (
        "Unsupported function\n\nTo use a JSON schema as a function, "
"it must have a top-level 'title' key to be used as the function name."
    )
    raise ValueError(msg)
Verification Results
Automated Tests
I created a reproduction script 
reproduce_issue.py
 to confirm the behavior.

Before Fix: The script would have raised the generic "Unsupported
function" error claiming description was also required.
After Fix: The script now confirms that the new, specific error message
is raised when
title
 is missing.
(Note: Verification was performed by inspecting the code logic and
running a lightweight reproduction script locally, as full suite
verification had environment dependency issues.)

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
2026-01-09 23:10:09 -05:00
Krud-x
d22cfaf7c6 fix(core): make yield_keys prefix keyword-only to match BaseStore (#34659)
This PR fixes a signature mismatch between BaseStore and its concrete
implementations by making the `prefix` parameter keyword-only in
`yield_keys` and `ayield_keys`.

This aligns the implementations with the BaseStore interface contract,
prevents Liskov Substitution Principle violations, and ensures
consistent
method signatures across store backends.

Fixes #32637

Breaking changes 
None. This change only enforces the existing abstract interface and does
not modify runtime behavior

Testing
- Verified that existing test suites pass after the signature fix.

Parts of this contribution were assisted by generative AI for
code navigation and drafting. All final design decisions and changes
were
reviewed and validated manually.

---------

Co-authored-by: Khagesh-Anayasmi <khagesh.desai@anayasmi.in>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-09 23:07:47 -05:00
Mason Daugherty
3bd8c0c4a3 fix(standard-tests): add type ignore (#34696)
Regression introduced in 8e3c6b109f

The commit changed the return annotation of `with_structured_output`
from `typing.Dict | BaseModel` to `builtins.dict[str, Any] | BaseModel`.
Since `BaseModel` refers to `pydantic.BaseModel (v2)`, but the test
`test_structured_output_pydantic_2_v1` uses `pydantic.v1.BaseModel`,
mypy's `warn_unreachable` setting flags the `isinstance` checks as
unreachable (since a class can't be both a `dict` and a different
`BaseModel` type).

Switching to `builtins.dict[str, Any]` made the type more precise, which
exposed this type incompatibility that was always latent but hidden by
the looser `typing.Dict` annotation.
2026-01-09 23:07:05 -05:00
Christophe Bornet
a7b943bbe3 fix(langchain): activate test_return_direct_spec tests, fix types (#34565)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2026-01-09 22:52:12 -05:00
Christophe Bornet
5fbf270c9d chore(langchain): fix types in test_todo, test_tool_retry (#34503)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-09 22:50:20 -05:00
Christophe Bornet
e73b027686 chore(langchain): fix types in test_shell_tool (#34502)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-09 22:46:56 -05:00
Christophe Bornet
ecd19ff71f chore(langchain): activate mypy warn_return_any rule (#34549)
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-09 22:46:25 -05:00
Christophe Bornet
cb0d227d8a chore(langchain): fix types in test_tool_selection and test_tool_emulator (#34499)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-09 22:37:54 -05:00
Christophe Bornet
b688e36e38 chore(langchain): fix types in test_shell_execution_policies (#34498)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-09 22:31:53 -05:00
Christophe Bornet
606ef38e74 chore(langchain): improve ignore_missing_imports config (#34551)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-09 22:18:45 -05:00
Christophe Bornet
36e590ca5f test(langchain): complete and activate test_responses tests (#34560)
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-09 22:17:03 -05:00
Christophe Bornet
fc417aaf17 fix(langchain): activate mypy warn-unreachable (#34553)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-09 22:11:16 -05:00
Christophe Bornet
5dc8ba3c99 chore(langchain): fix types in test_injected_runtime_create_agent, test_create_agent_tool_validation (#34568) 2026-01-09 21:50:18 -05:00
Christophe Bornet
f1ab8c5c80 chore(langchain): fix types in test_response_format and test_state_schema (#34571) 2026-01-09 21:49:16 -05:00
Christophe Bornet
bfe0a26547 chore(langchain): remove generic from FakeToolCallingModel (#34572)
* Making `FakeToolCallingModel` generic on its `structured_response`
doesn't help anywhere in typing.
* There are more than 120 references of `FakeToolCallingModel` in the
code where you get ` error: Need type annotation for "model"
[var-annotated]` because mypy can't resolve the generic type (we don't
see them atm because they are in files temporarily excluded from mypy
checking). We would need to explicitly type them to
`FakeToolCallingModel[Any]`

Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-09 21:48:33 -05:00
Christophe Bornet
bb5bd1181f chore(langchain): fix types in test_context_editing, test_agent_name, test_response_format_integration (#34574)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-09 21:47:46 -05:00
Mason Daugherty
9093c6effe chore(core): bump lock (#34695) 2026-01-09 21:42:41 -05:00
Christophe Bornet
8cb7dbd37b chore(core): improve types for RunnableLambda (#34539)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-09 21:42:27 -05:00
Christophe Bornet
2a2a4067ca chore(core): improve types for StreamingRunnable (#34540) 2026-01-09 21:34:50 -05:00
Christophe Bornet
5e9765d811 chore(langchain): fix types in test_overrides (#34635)
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-09 18:31:13 -05:00
Mason Daugherty
703736a1e3 feat(langchain): add state to _ModelRequestOverrides (#34692)
Appears `override()`'s docstring in `langgraph` already shows
`state=new_state` as a valid usage pattern

Works since `dataclasses.replace()` accepts any field, but the
`TypedDicts` weren't updated to match. Caused mypy to flag legitimate
usage as an error.
2026-01-09 18:28:24 -05:00
Christophe Bornet
61fd703e5f chore(langchain): fix types in test_tools (#34592)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2026-01-09 18:05:28 -05:00
Christophe Bornet
4e40c2766a chore(langchain): fix types in test_summarization (#34656)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2026-01-09 17:54:42 -05:00
Christophe Bornet
9ce73a73f8 test(langchain): activate test_responses_spec tests (#34564)
description by @mdrxy

- Enable `test_responses_spec.py` integration tests that were previously
skipped at module level
- Widen `ToolStrategy.schema` type annotation from `type[SchemaT]` to
`type[SchemaT] | dict[str, Any]` to match actual supported usage (JSON
schema dicts were already handled at runtime)
- Fix type annotations and linting issues in test file (modernize to
`dict`/`list`, add return types, prefix unused `_request` param)
- Improve generic typing in `load_spec` utility with bounded `TypeVar`

Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-09 17:44:33 -05:00
Christophe Bornet
b4cd67ac15 style(langchain): fix some ruff preview rules (#34663)
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-09 17:41:05 -05:00
Christophe Bornet
8e3c6b109f style(core): fix some noqa escapes (#34675)
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-09 17:36:08 -05:00
Christophe Bornet
fd69425439 style(text-splitters): fix some ruff preview rules (#34665)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2026-01-09 17:28:18 -05:00
Christophe Bornet
e6dde3267a chore(langchain): fix types in test_framework (#34567)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2026-01-09 17:24:38 -05:00
Christophe Bornet
23c4c506d3 chore(langchain): fix types in memory_assert, conftest, conftest_checkpointer and conftest_store (#34636)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2026-01-09 17:18:05 -05:00
Christophe Bornet
d1404e63bb chore(langchain): fix types in test_system_message (#34634)
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-09 17:17:57 -05:00
Mason Daugherty
18c25e9f10 chore: ban relative imports on all packages (#34691) 2026-01-09 17:02:24 -05:00
Christophe Bornet
8e824d9ec4 style: bump ruff version to 0.14.11 (#34674)
With ruff 0.14.11+, we can remove `PLW1510` from `unfixable` (see
https://github.com/astral-sh/ruff/issues/17091)
2026-01-09 16:30:24 -05:00
Sydney Runkle
fbe9babb34 fix: remove relative imports (#34680)
standardizing on absolute imports rather than relative across the
codebase
2026-01-09 13:00:51 -05:00
Sydney Runkle
9bd028d04a fix: disable int tests on release temporarily (#34685) 2026-01-09 12:42:25 -05:00
Mason Daugherty
2e8744559d fix(langchain,langchain-classic): more descriptive error msg when dep is not installed (#34679) 2026-01-09 12:41:55 -05:00
ccurme
19edaa8acb chore(openai): delete outdated test (#34682) 2026-01-09 12:37:44 -05:00
Sydney Runkle
b500244250 fix: rm anth test (#34684) 2026-01-09 12:37:33 -05:00
Sydney Runkle
d972d00b3a chore: dropping openai from release matrix (#34681) 2026-01-09 11:22:49 -05:00
Guofang.Tang
384158daec fix(langchain): infer provider from mixed-case prefixes (#34672)
Fix provider inference for mixed-case model prefixes and add matching
unit coverage.
2026-01-09 11:07:14 -05:00
Sydney Runkle
c080296bed release: langchain-core 1.2.7 (#34678) 2026-01-09 16:02:38 +00:00
Sydney Runkle
323c76504a fix: add test confirming we don't inject args based on args_schema alone (#34677)
pending exclusion from function signature
2026-01-09 11:00:13 -05:00
Sydney Runkle
ed2aa9f747 fix: don't trace injected args only found in signature (#34670)
for the case when they're not included in the `args_schema`

this was predicted by @eyurtsev's comment here:
https://github.com/langchain-ai/langchain/pull/33729/files#r2475538173

pairing w/ this PR in mcp adapters:
https://github.com/langchain-ai/langchain-mcp-adapters/pull/407
2026-01-09 09:58:34 -05:00
Mason Daugherty
76da99e022 release(langchain): 1.2.3 (#34668) 2026-01-08 15:24:32 -05:00
Aman Gupta
2847814c70 feat(core): add more file extensions to ignore in HTML link extraction (#34552)
# feat(core): add more file extensions to ignore in HTML link extraction

## Description
This PR enhances the HTML link extraction utility in  
`libs/core/langchain_core/utils/html.py` by expanding the
`SUFFIXES_TO_IGNORE` list to include additional common binary file
extensions:

- `.webp`
- `.pdf`
- `.docx`
- `.xlsx`
- `.pptx`
- `.pptm`

These file types are non-HTML, non-crawlable resources. Ignoring them
prevents `find_all_links` and `extract_sub_links` from mistakenly
treating such binary assets as navigable links. This improves link
filtering, reduces unnecessary crawling, and aligns behavior with
typical web scraping expectations.

## Summary of Changes
- **Updated** `libs/core/langchain_core/utils/html.py`: Added `.webp`,
`.pdf`, `.docx`, `.xlsx`, `.pptx`, `.pptm` to `SUFFIXES_TO_IGNORE`.

## Related Issues
N/A

## Verification
- `ruff check libs/core/langchain_core/utils/html.py`: **Passed**  
- `mypy libs/core/langchain_core/utils/html.py`: **Passed**  
- `pytest libs/core/tests/unit_tests/utils/test_html.py`: **Passed** (11
tests)

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-08 14:40:22 -05:00
ccurme
d383f00489 refactor(langchain): engage summarization based on reported usage_metadata (#34632) 2026-01-08 11:12:00 -05:00
Aman Gupta
50c5bb5607 refactor(core): improve docstrings for HTML link extraction utilities (#34550)
# refactor(core): improve docstrings for HTML link extraction utilities

## Description
This PR updates and clarifies the docstrings for `find_all_links` and
`extract_sub_links` in
`libs/core/langchain_core/utils/html.py`.

The previous return-value descriptions were vague (e.g., "all links",
"sub links"). They have now been revised to clearly describe the
behavior and output of each function:

- **find_all_links** → “A list of all links found in the HTML.”
- **extract_sub_links** → “A list of absolute paths to sub links.”

These improvements make the utilities more understandable and
developer-friendly without altering functionality.

## Verification
- `ruff check libs/core/langchain_core/utils/html.py`: **Passed**  
- `pytest libs/core/tests/unit_tests/utils/test_html.py`: **Passed**

## Checklists
- PR title follows the required format: `TYPE(SCOPE): DESCRIPTION`  
- Changes are limited to the `langchain-core` package  
- `make format`, `make lint`, and `make test` pass
2026-01-08 10:21:17 -05:00
Mason Daugherty
2b6911d9af fix(langchain): keep tool call / AIMessage pairings when summarizing (#34609)
Fixes #34282

**Before:** When using agents with tools (like file reading, web search,
etc.), the conversation looks like this:

```
[User]     "Read these 10 files and summarize them"
[AI]       "I'll read all 10 files" + [tool_call: read_file x 10]
[Tool]     "Contents of file1.txt..."
[Tool]     "Contents of file2.txt..."
[Tool]     "Contents of file3.txt..."
... (7 more tool responses)
```

When the conversation gets too long, `SummarizationMiddleware` kicks in
to compress older messages. The problem was:

If you asked to keep the last 6 messages, you'd get:

```
[Summary]  "Here's what happened before..."
[Tool]     "Contents of file5.txt..."
[Tool]     "Contents of file6.txt..."
[Tool]     "Contents of file7.txt..."
[Tool]     "Contents of file8.txt..."
[Tool]     "Contents of file9.txt..."
[Tool]     "Contents of file10.txt..."
```

The AI's original request to read the files (`[AI]` message with
`tool_calls`) was summarized away, but the tool responses remained. This
caused the error:

```
Error code: 400 - "No tool call found for function call output with call_id..."
```

Many APIs require that every tool response has a matching tool request.
Without the AI message, the tool responses are "orphaned."

## The fix

Now when the cutoff lands on tool messages, we **move backward** to
include the AI message that requested those tools:

Same scenario, keeping last 6 messages:

```
[Summary]  "Here's what happened before..."
[AI]       "I'll read all 10 files" + [tool_call: read_file x 10]
[Tool]     "Contents of file1.txt..."
[Tool]     "Contents of file2.txt..."
... (all 10 tool responses)
```

The AI message is preserved along with its tool responses, keeping them
paired together.

## Practical examples

### Example 1: Parallel tool calls

**Scenario:** Agent reads 10 files in parallel, summarization triggers
(see above)

### Example 2: Mixed conversation

**Scenario:** User asks question, AI uses tools, user says thanks

```
[User]     "What's the weather?"
[AI]       "Let me check" + [tool_call: get_weather]
[Tool]     "72F and sunny"
[AI]       "It's 72F and sunny!"
[User]     "Thanks!"
```

Keeping last 2 messages:

| Before (Bug) | After (Fix) |
|--------------|-------------|
| Only `[User] "Thanks!"` kept | `[AI] + [Tool] + [AI] + [User]` all
kept |
| Lost the weather info | Tool pair preserved with response |

### Example 3: Multiple tool sequences

```
[User]     "Search for X"
[AI]       [tool_call: search]
[Tool]     "Results for X"
[User]     "Now search for Y"
[AI]       [tool_call: search]
[Tool]     "Results for Y"
[User]     "Great!"
```

**Keeping last 3 messages:** If cutoff lands on `[Tool] "Results for
Y"`, we now include `[AI] [tool_call: search]` to keep the pair
together.
2026-01-08 10:07:56 -05:00
Guofang.Tang
f805ea9601 test(langchain): cover chat model provider inference (#34657)
Add unit coverage for chat model provider inference across common model
name prefixes. This improves regression protection without touching
runtime

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
2026-01-08 09:59:12 -05:00
Stephan Günther
0276cc0290 fix(langchain): fix copy-paste error on azure_openai embedding provider map (#34655)
Fixes a bug introduced with commit 85f1ba2 (released in `langchain ==
1.2.1`).

Whenever the index embedding of the langgraph-server is configured with
`azure_openai` provider, the wrong class is going to be initialized (and
fails to do so if the now unexpected credentials in environment variable
`OPENAI_API_KEY` is not provided).

Example configuration file `langgraph.json` that will reproduce the
issue:
(see
https://docs.langchain.com/langsmith/cli#adding-semantic-search-to-the-store)

```json
{
  "dependencies": ["."],
  "graphs": {
    "chat": "src/agents/chat/graph.py:graph",
  },
  "store": {
    "index": {
      "embed": "azure_openai:text-embedding-3-small",
      "dims": 1536
    }
  },
  "python_version": "3.13",
  "image_distro": "wolfi"
}
```
2026-01-08 09:54:53 -05:00
Eugene Yurtsev
ceca38d3fe fix(langchain): add test to verify version (#34644)
verify version in langchain to avoid accidental drift
2026-01-07 22:36:10 +00:00
Eugene Yurtsev
5554a36ad5 release(langchain): release 1.2.2 (#34643)
Release langchain 1.2.2
2026-01-07 17:27:58 -05:00
Harrison Chase
bda22aa1d9 fix(langchain): handle parallel usage of the todo tool in planning middleware (#34637)
The agent should only make a single call to update the todo list at a
time. A parallel call doesn't make sense, but also cannot work as
there's no obvious reducer to use.

On parallel calls of the todo tool, we return ToolMessage containing to
guide the LLM to not call the tool in parallel.

---------

Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2026-01-07 17:23:56 -05:00
Manas karthik
48cd13114f test(core): add edge case for empty examples in LengthBasedExampleSelector (#34641) 2026-01-07 15:26:53 -05:00
Mohammad Mohtashim
e6a9694f5d fix(core): fix strict schema generation for functions with optional args (#34599) 2026-01-07 15:13:18 -05:00
ccurme
25bb36de81 release(openai): 1.1.7 (#34640) 2026-01-07 14:34:23 -05:00
OysterMax
92afcaae60 fix(openai): raise proper exception OpenAIRefusalError on structured output refusal (#34619) 2026-01-07 14:34:02 -05:00
Sujal M H
7ad1c19d9c fix: handle empty assistant content in Responses API (#34272) (#34296) 2026-01-07 14:21:55 -05:00
Christophe Bornet
f10225184d chore(langchain): fix types in test_wrap_model_call (#34573) 2026-01-07 11:49:46 -05:00
Chris Papademetrious
0c7b7e045d feat(core): support custom message separator in get_buffer_string() (#34569) 2026-01-07 11:46:17 -05:00
Aarav Dugar
4c86e8ba39 chore(groq): document vision support (#34620) 2026-01-07 11:37:05 -05:00
Manas karthik
048de6dfb6 test(text-splitters): add edge case tests for CharacterTextSplitter (#34628) 2026-01-07 11:06:44 -05:00
Mason Daugherty
557eddfd51 refactor(core): add warning for fallback GPT-2 tokenizer usage (#34621) 2026-01-06 19:11:10 -05:00
Mason Daugherty
aa9c63b96a release(langchain): 1.2.1 (#34622) 2026-01-06 19:10:49 -05:00
Mason Daugherty
8aeff95341 fix(core,langchain): use get_buffer_string for message summarization (#34607)
Fixes #34517

Supersedes #34557, #34570

Fixes token inflation in `SummarizationMiddleware` that caused context
window overflow during summarization.

**Root cause:** When formatting messages for the summary prompt,
`str(messages)` was implicitly called, which includes all Pydantic
metadata fields (`usage_metadata`, `response_metadata`,
`additional_kwargs`, etc.). This caused the stringified representation
to use ~2.5x more tokens than `count_tokens_approximately` estimates.

**Problem:**
- Summarization triggers at 85% of context window based on
`count_tokens_approximately`
- But `str(messages)` in the prompt uses 2.5x more tokens
- Results in `ContextLengthExceeded`

**Fix:** Use `get_buffer_string()` to format messages, which produces
compact output:

```
Human: What's the weather?
AI: Let me check...[tool_calls]
Tool: 72°F and sunny
```

Instead of verbose Pydantic repr:

```python
[HumanMessage(content='What's the weather?', additional_kwargs={}, response_metadata={}), ...]
```
2026-01-06 19:05:03 -05:00
Christophe Bornet
0438f8c277 chore(langchain): fix types in test_model_fallback (#34615) 2026-01-06 13:07:18 -05:00
Christophe Bornet
7f4f130479 chore(langchain): fix types in test_pii (#34617) 2026-01-06 13:06:25 -05:00
ccurme
6537939f53 chore(langchain): add admonition around redaction_rules (#34618) 2026-01-06 13:01:09 -05:00
Ademola Balogun
a2529cd805 fix(langchain): correct typo 'langchain experiment' to 'langchain_experimental' in error messages (#34608)
Fixed typo in ImportError messages where "langchain experiment" should
be "langchain_experimental" for consistency with the actual package
name.

This helps improve clarity for users who encounter these error messages
when trying to use deprecated tools that have moved to the
langchain_experimental package.

Related issues: #13858, #13859

Co-authored-by: Ademola <ademicho@gmail>
2026-01-05 18:10:06 -05:00
ccurme
c1f1641018 fix(anthropic): fix version (#34606) 2026-01-05 16:03:20 -05:00
ccurme
225e0fa8c9 release(anthropic): 1.3.1 (#34605) 2026-01-05 15:55:15 -05:00
Loganaden Velvindron
f021e899dc fix(anthropic): CVE-2025-68664 (#34563) 2026-01-05 15:51:25 -05:00
lwtaiyty
578cef9622 fix(anthropic): skip cache_control for code_execution blocks (#34579) 2026-01-05 15:40:59 -05:00
Christophe Bornet
7979fd3d9f chore(langchain): fix types in test_composition (#34580) 2026-01-05 14:49:34 -05:00
Christophe Bornet
3b65985551 chore(langchain): fix types in test_decorators (#34583) 2026-01-05 14:47:10 -05:00
Christophe Bornet
c4babed5c6 chore(langchain): fix types in test_wrap_tool_call (#34600) 2026-01-05 14:38:31 -05:00
Christophe Bornet
5ae53fdfb3 chore(langchain): fix types in test_model_call_limit_types (#34601) 2026-01-05 14:37:03 -05:00
Christophe Bornet
901690ceec chore(langchain): fix types in test_file_search and test_human_in_the_loop (#34602) 2026-01-05 14:34:35 -05:00
ゆり
be2c7f1aa8 test(core): add tests for formatting utils and merge functions (#34511)
## Summary
Add comprehensive test coverage for previously untested utilities in
`langchain-core`.

## Changes

### New file: `test_formatting.py` (18 tests)

Tests for `StrictFormatter` class:
- `test_vformat_with_keyword_args` - basic functionality
- `test_vformat_with_multiple_keyword_args` - multiple placeholders
- `test_vformat_with_empty_string` - edge case
- `test_vformat_with_no_placeholders` - literal strings
- `test_vformat_raises_on_positional_args` - error handling
- `test_vformat_raises_on_multiple_positional_args` - error handling
- `test_vformat_with_special_characters` - newlines, tabs
- `test_vformat_with_unicode` - emoji, CJK characters
- `test_vformat_with_format_spec` - format specifications
- `test_vformat_with_nested_braces` - escaped braces

Tests for `validate_input_variables`:
- `test_validate_input_variables_success` - valid input
- `test_validate_input_variables_with_extra_variables` - extra vars
allowed
- `test_validate_input_variables_with_missing_variable` - KeyError
- `test_validate_input_variables_empty_format` - edge case
- `test_validate_input_variables_no_placeholders` - edge case

Tests for `formatter` singleton:
- `test_formatter_is_strict_formatter` - type check
- `test_formatter_format_works` - functionality
- `test_formatter_rejects_positional_args` - error handling

### Extended `test_utils.py` (14 new tests)

Tests for `merge_lists`:
- Parametrized tests covering None handling, simple merge, empty lists,
index-based merging
- `test_merge_lists_multiple_others` - merging 3+ lists
- `test_merge_lists_all_none` - all None inputs

Tests for `merge_obj`:
- Parametrized tests for None, strings, dicts, lists, equal values
- `test_merge_obj_type_mismatch` - TypeError on type mismatch
- `test_merge_obj_unmergeable_values` - ValueError on different values
- `test_merge_obj_tuple_raises` - ValueError for tuples

## Test plan
- [x] Tests follow existing patterns in the codebase
- [x] All tests are unit tests (no network calls)
- [x] Tests cover happy paths and error conditions
- [x] Tests verify no mutation of input data

## AI Disclosure
This contribution was developed with AI assistance (Claude Code).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: yurekami <yurekami@users.noreply.github.com>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-05 14:20:11 -05:00
ccurme
b5c5ba0a5f release(xai): 1.2.1 (#34604) 2026-01-05 13:55:38 -05:00
ccurme
944b43dd25 fix(xai): count reasoning tokens in output total (#34603) 2026-01-05 13:25:30 -05:00
aroun-coumar
730a3676f8 fix(core): strip message IDs from cache keys using model_copy (#33915)
**Description:**  

*Closes
#[33883](https://github.com/langchain-ai/langchain/issues/33883)*

Chat model cache keys are generated by serializing messages via
`dumps(messages)`. The optional `BaseMessage.id` field (a UUID used
solely for tracing/threading) is included in this serialization, causing
functionally identical messages to produce different cache keys. This
results in repeated API calls, cache bloat, and degraded performance in
production workloads (e.g., agents, RAG chains, long conversations).

This change normalizes messages **only for cache key generation** by
stripping the nonsemantic `id` field using Pydantic V2’s
`model_copy(update={"id": None})`. The normalization is applied in both
synchronous and asynchronous cache paths (`_generate_with_cache` /
`_agenerate_with_cache`) immediately before `dumps()`.

```python
normalized_messages = [
    msg.model_copy(update={"id": None})
    if getattr(msg, "id", None) is not None
    else msg
    for msg in messages
]
prompt = dumps(normalized_messages)

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2026-01-05 10:37:10 -05:00
Julia (Juli) Huang
cd5b36456a fix(text-splitters): HTMLSemanticPreservingSplitter nested preserved … (#34587)
Summary
Fixes an issue where HTMLSemanticPreservingSplitter failed to preserve
elements nested inside non-container tags. With these changes, preserved
elements are now correctly detected and handled at any nesting depth.

Root Cause
`_process_element()` only recursed into a small set of hard-coded
container tags (`html`, `body`, `div`, `main`). For other tags, the
subtree was flattened into text, preventing nested preserved elements
(inside `<p>`, `<section>`, `<article>`, etc.) from being detected.


Fix
- Updated traversal logic in _process_element (html.py) to recursively
process child elements for any tag that contains nested elements
- Avoided duplicate text extraction
- Preserved correct placeholder ordering
- Treated leaf nodes as text only

Tests
Adds regression tests covering preserved elements nested inside
non-container tags, including:
- table inside section
- nested divs
- code inside paragraph

All existing tests pass (make lint, format, test, etc).

Breaking changes
None.

Fixes
Fixes #31569

Disclaimer
GitHub Copilot was used to assist with test case design in
test_text_splitters.py and documentation comments; all code logic was
manually implemented and reviewed.

---------

Co-authored-by: julih <julih@julihs-MacBook-Pro.local>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2026-01-05 10:28:27 -05:00
Mohan Kumar S
13cfdf1676 fix(core): exclude injected args from tool schema (#34582) 2026-01-05 09:59:59 -05:00
Andre Roelofs
c25f3847d0 refactor(core): select chunk_id via ranking and remove extra allocation (#34588) 2026-01-05 09:13:05 -05:00
Christophe Bornet
7ca0efde04 chore(langchain): fix types in test_diagram and test_sync_async_wrappers (#34591) 2026-01-05 09:05:24 -05:00
repeat-Q
9495eb348d docs: add LangChain Academy link to Additional resources (#34597) 2026-01-05 08:55:46 -05:00
Christophe Bornet
e5d4acf681 style(langchain): add ruff rule PLC0415 (#34559) 2026-01-04 01:26:04 -05:00
ccurme
659eab2607 release(core): 1.2.6 (#34586) 2026-01-02 16:20:20 -05:00
Angus Jelinek
458a186540 chore(core): Update LangChainTracer to use Pydantic v2 methods (#34541) 2026-01-02 16:02:13 -05:00
ccurme
a7aad60989 fix(xai): ensure citations are streamed just once (#34556) 2025-12-31 18:01:41 -05:00
ccurme
9da28bac86 release(xai): 1.2.0 (#34555) 2025-12-31 16:37:21 -05:00
ccurme
0b91774263 fix(xai): stream usage metadata by default (#34531) 2025-12-31 16:30:52 -05:00
weiii668
5517ef37fb docs(core): add docstrings to internal helper functions (#34525)
Co-authored-by: weiii668 <your-email@example.com>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-30 21:58:00 -06:00
Mason Daugherty
2bbe4216e0 docs(core): refresh content.py docstrings (#34546)
minor formatting improvements and increased disambiguation between `id`
and `file_id` for `FileContentBlock` in response to
https://github.com/langchain-ai/langchain-google/pull/1477
2025-12-30 20:44:47 -06:00
Pádraic Slattery
fcc02f78e4 chore(deps): Update outdated GitHub Actions versions (#34544)
This PR updates an outdated GitHub Action version.

- Updated `astral-sh/setup-uv` from `v6` to `v7` in
`.github/actions/uv_setup/action.yml`

Looks like this was missed as part of
https://github.com/langchain-ai/langchain/pull/33457 so hopefully safe
to bring it into alignment.
2025-12-30 20:42:22 -06:00
Mason Daugherty
721bf15430 fix(langchain): resolve race condition in ShellSession.execute() (#34535)
Addresses a flaky test

When executing `exit 1` as a startup command, the shell process
terminates immediately. The code then tries to write a marker command
(`printf '...'`) to stdin, but the pipe is already broken because the
shell has exited, causing `BrokenPipeError`.
2025-12-29 18:16:08 -06:00
Mason Daugherty
dcfd9c0e04 fix(infra): use langchain_v1 for dev container deps (#34534) 2025-12-29 18:10:40 -06:00
Christophe Bornet
e03d6b80d5 chore(deps): bump mypy to v1.19 and ruff to v1.14 (#34521)
* Set mypy to >=1.19.1,<1.20
* Set ruff to >=0.14.10,<0.15
2025-12-29 18:07:55 -06:00
Mason Daugherty
33378f16fb feat(infra): add .dockerignore for codespaces (#34533) 2025-12-29 17:58:28 -06:00
Christophe Bornet
ea25f5ebdd chore(text-splitters): bump dependency locks for python 3.14 (#34522)
* Support sentence-transformers optional dep on python 3.14
* Bump some dep locks to use pre-built wheels instead of building them
(murmurhash, cymem, preshed, thinc, srsly, blis)
* Still not possible to use spacy: even though there are wheels
available, spacy depends on Pydantic v1 which doesn't work on Python
3.14.
* Speeds up installation and CI.

Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-29 17:55:34 -06:00
Christophe Bornet
04c0c1bdc3 chore(langchain-classic): bump markupsafe lock for python 3.14 (#34523)
Bump lock of MarkupSafe to 3.0.3 which has Python 3.14 pre-built wheels.
Speeds up installation and CI.

Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-29 17:55:26 -06:00
Efe Çelik
c1f5d0963d fix: typo: saved the world 'wether' -> 'whether' (#34524)
Changed "wether" to "whether" in test comments.
2025-12-29 17:28:09 -06:00
Mason Daugherty
e81f00fb29 docs(standard-tests): remove autodoc comment (#34532) 2025-12-29 17:25:52 -06:00
Mason Daugherty
9ecf6360af feat(infra): add more pre-commit hooks (#34519) 2025-12-29 02:14:20 -06:00
JJ
7ce68f27da fix(docs): correct Code of Conduct link in README (#34518)
The Code of Conduct link was pointing to a non-existent file path.
Updated to use GitHub's community standards tab URL which correctly
displays the Code of Conduct.

Changed from:

https://github.com/langchain-ai/langchain/blob/master/.github/CODE_OF_CONDUCT.md

To:
https://github.com/langchain-ai/langchain/?tab=coc-ov-file

(Replace this entire block of text)

Read the full contributing guidelines:
https://docs.langchain.com/oss/python/contributing/overview

Thank you for contributing to LangChain! Follow these steps to have your
pull request considered as ready for review.

1. PR title: Should follow the format: TYPE(SCOPE): DESCRIPTION

  - Examples:
    - fix(anthropic): resolve flag parsing error
    - feat(core): add multi-tenant support
    - test(openai): update API usage tests
- Allowed TYPE and SCOPE values:
https://github.com/langchain-ai/langchain/blob/master/.github/workflows/pr_lint.yml#L15-L33

2. PR description:

  - Write 1-2 sentences summarizing the change.
- If this PR addresses a specific issue, please include "Fixes
#ISSUE_NUMBER" in the description to automatically close the issue when
the PR is merged.
  - If there are any breaking changes, please clearly describe them.
- If this PR depends on another PR being merged first, please include
"Depends on #PR_NUMBER" inthe description.

3. Run `make format`, `make lint` and `make test` from the root of the
package(s) you've modified.

  - We will not consider a PR unless these three are passing in CI.

Additional guidelines:

- We ask that if you use generative AI for your contribution, you
include a disclaimer.
- PRs should not touch more than one package unless absolutely
necessary.
- Do not update the `uv.lock` files unless or add dependencies to
`pyproject.toml` files (even optional ones) unless you have explicit
permission to do so by a maintainer.
2025-12-29 01:47:25 -06:00
Christophe Bornet
03ae39747b refactor(core): fix some missing generic types (#31658)
See
https://mypy.readthedocs.io/en/stable/config_file.html#confval-disallow_any_generics

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2025-12-27 16:53:08 -06:00
Sarah Clark
10de0a5364 fix(langchain-classic): pass default to config.getoption (#34034)
Fixes #34033

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-27 16:36:51 -06:00
Mason Daugherty
30ac1da0de release(standard-tests): 1.1.2 (#34507) 2025-12-27 03:01:56 -06:00
Dragos Bobolea
6d447f89d9 fix(fireworks): bind_tools(strict: bool) and reasoning_content (#34343)
Extract strict from kwargs and pass it to convert_to_openai_tool when
converting tools. This ensures that when strict is provided, it's
properly used during tool conversion and removed from kwargs before
calling the parent bind method.

Also extract reasoning_content from API responses and store it in
additional_kwargs for AIMessage objects.

Fixes https://github.com/langchain-ai/langchain/issues/34341 and
https://github.com/langchain-ai/langchain/issues/34342

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-27 02:42:06 -06:00
Christophe Bornet
5ef9f6e036 style(core): add ruff RUF012 rule (#34492)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-27 02:36:28 -06:00
Connor Hyatt
e3939ade5a fix(core): support (message class, template) tuples in ChatPromptTemplate.from_messages (#33989)
### Description

`ChatPromptTemplate.from_messages` supports multiple tuple formats for
defining message templates. One documented format is `(message class,
template)`, which allows users to specify the message type using the
class directly:

```python
ChatPromptTemplate.from_messages([
    (SystemMessage, "You are a helpful assistant named {name}."),
    (HumanMessage, "{input}"),
])
```

However, this syntax was broken. Passing a tuple like `(HumanMessage,
"{input}")` would raise a Pydantic validation error because the
conversion logic in `_convert_to_message_template` didn't handle
`BaseMessage` subclasses—it only recognized string-based role
identifiers like `"human"` or `"system"`.

This PR adds the missing branch to detect when the first element of a
tuple is a message class (by checking for the `type` class attribute)
and routes it through `_create_template_from_message_type`, which
already knows how to create the appropriate `MessagePromptTemplate` for
each message type.

### Changes

- Updated `_convert_to_message_template` to properly support `(message
class, template)` tuples

### Testing

Added 16 comprehensive unit tests covering:

- Basic usage with `HumanMessage`, `AIMessage`, and `SystemMessage`
classes
- Integration with `invoke()` method
- Mixed syntax (message class tuples alongside string tuples)
- Multiple template variables
- Edge cases: empty templates, static text (no variables)
- Correct extraction of `input_variables`
- Partial variables support
- Combination with `MessagesPlaceholder`
- Mustache template format
- Template operations: `append()`, `extend()`, concatenation, and
slicing
- Special characters and unicode in templates

### Issue

Fixes #33791

### Dependencies

None

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-27 02:20:33 -06:00
Miguel Athie
b0e4ef3158 test(core): add regression test for list-index $ref resolution (#34097)
This PR adds a regression test covering the JSON Schema `$ref` pattern
found in
MCP-style schemas, where a `$ref` points into a list-based structure
such as:


#/properties/body/anyOf/1/properties/Message/properties/bccRecipients/items

This pattern historically failed due to incorrect handling of numeric
list
components in `_retrieve_ref`. The underlying bug has since been fixed,
and
this test ensures coverage so we don't regress on list-index `$ref`
resolution.

The new test (`test_dereference_refs_list_index_items_ref_mcp_like`)
verifies:

- correct traversal into `anyOf[1]`
- proper dereferencing of `items.$ref`
- no errors thrown
- `ccRecipients.items` is identical to the resolved schema of
`bccRecipients.items`

No code changes are included, just the one test — this PR adds coverage
to preserve the expected
behavior and documents support for this real-world MCP schema pattern.

Related to #32012.

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-27 02:18:51 -06:00
gjeltep
ca7790f895 fix(core): fix callback manager merge mixing handlers (#32028) (#33617)
## Description
Fixed `BaseCallbackManager.merge()` method to correctly preserve the
distinction between `handlers` and `inheritable_handlers` during merge
operations.

Previously, the merge method was using `add_handler()` which incorrectly
added handlers to both lists when `inherit=True`, causing
cross-contamination between regular and inheritable handlers.

The fix directly passes the combined handler lists to the constructor
instead of using `add_handler()`, ensuring proper separation is
maintained.

## Issue
Fixes #32028

## Dependencies
None

## Testing
- Modified existing test `test_merge_preserves_handler_distinction()` to
verify handlers remain properly separated after merge

## Checklist
- [x] **Breaking Changes**: No breaking changes - only fixes incorrect
behavior
- [x] **Type Hints**: All functions have complete type annotations
- [x] **Tests**: Fix is fully tested with existing unit test
- [x] **Security**: No security implications
- [x] **Documentation**: No documentation changes needed - bug fix only
- [x] **Code Quality**: Passes lint and format checks
- [x] **Commit Message**: Follows Conventional Commits format

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-27 02:01:59 -06:00
Christophe Bornet
5884fb9523 style(text-splitters,standard-tests,cli): add ruff TC and RUF012 rules (#34495)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-27 01:41:33 -06:00
Christophe Bornet
0bd862b814 style(langchain): add ruff rule RUF012 (#34497)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2025-12-27 01:36:47 -06:00
Christophe Bornet
85f1ba2351 refactor(langchain): refactor optional imports logic (#32813)
* Use `importlib` to load dynamically the classes
* Removes missing package warnings

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-27 01:02:32 -06:00
Christophe Bornet
d46187201d style: add ruff ISC001 rule (#34493)
ISC001 doesn't conflict anymore with the formatter. See
https://github.com/astral-sh/ruff/issues/8272

Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-26 21:39:56 -06:00
Christophe Bornet
3d78cc69f1 style(langchain): add TC ruff rules (#34496)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-26 21:37:57 -06:00
Christophe Bornet
a92c032ff6 style(core): fix mypy no-any-return violations (#34204)
* FIxed where possible
* Used `cast` when not possible to fix

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-26 21:35:27 -06:00
Christophe Bornet
88b5f22f1c style(langchain): fix some ruff preview rules (#34504) 2025-12-26 21:34:54 -06:00
Mason Daugherty
78b2d51edc docs(core): image url docstring enhancement (#34488) 2025-12-25 23:10:48 -06:00
Harikrishna KP
294dda8df2 test(core): URL-encode bgColor parameter in mermaid.ink API calls (#34466)
## Problem

The `draw_mermaid_png()` function fails with HTTP 400 when using named
background colors like `white`. This is because named colors get
prefixed with `!` (e.g., `!white`) but this special character is not
URL-encoded before being added to the API URL.

As reported in #34444, the URL parameter `bgColor=!white` causes
mermaid.ink to return a 400 Bad Request error.

## Solution

URL-encode the `background_color` parameter using `urllib.parse.quote()`
before constructing the API URL. This ensures special characters like
`!` are properly encoded as `%21`.

## Changes

- Added `import urllib.parse` 
- URL-encode `background_color` value with
`urllib.parse.quote(str(background_color), safe="")`
- Added 2 unit tests:
- `test_mermaid_bgcolor_url_encoding`: Verifies named colors are
properly encoded
- `test_mermaid_bgcolor_hex_not_encoded`: Verifies hex colors work
correctly

## Testing

```bash
pytest tests/unit_tests/runnables/test_graph.py::test_mermaid_bgcolor_url_encoding -v
pytest tests/unit_tests/runnables/test_graph.py::test_mermaid_bgcolor_hex_not_encoded -v
```

Both tests pass.

Fixes #34444

---
*This contribution was made with AI assistance (Claude).*

Co-authored-by: Mr-Neutr0n <mrneutron@users.noreply.github.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-25 21:41:46 -06:00
Christophe Bornet
21c7cf1fa0 style(langchain): fix some PLC0415 rules (#34475)
The remaining ones are solved in
https://github.com/langchain-ai/langchain/pull/32813
2025-12-25 21:38:12 -06:00
Christophe Bornet
2212137931 style(core): fix some noqa: ARG rules (#34437) 2025-12-25 21:31:02 -06:00
Nhan Nguyen
e99ccbc126 fix(core): URL-encode bgColor in mermaid API calls (#34461)
URL-encode the bgColor parameter to fix 400 errors from mermaid.ink API.

The `!` character in `!white` was not encoded, causing API failures.

Fixes #34444
2025-12-25 21:30:09 -06:00
Rudra Tiwari
75e237643a perf(core): move origin type map to module level in function_calling.py (#34481)
Moves `_ORIGIN_MAP` dict from inside `_py_38_safe_origin()` to module
level constant. This avoids dict allocation on every function call,
reducing garbage collection pressure during frequent tool conversions.

The function is called during typed dict to pydantic model conversion
which happens during tool binding and invocation - a hot path in
LangChain.

**Testing:** `make lint` passes

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2025-12-25 21:29:31 -06:00
Christophe Bornet
1f403cf612 style(core): add ruff rules TC (#34476)
* Fixed a few TC
* Added a few Pydantic classes to
`flake8-type-checking.runtime-evaluated-base-classes` (not as much as I
would have imagined)
* Added a few `noqa: TC`
* Activated TC rules
2025-12-25 21:23:31 -06:00
Rudra Tiwari
451e8496e7 perf(core): precompile hex color regex pattern at module level (#34480)
Moves hex color validation regex from inside
`_render_mermaid_using_api()` to module-level constant
`_HEX_COLOR_PATTERN`. This avoids recompiling the regex on every
function call, improving performance when rendering multiple Mermaid
graphs.


**Testing:**
- `make lint` passes
- `make test` passes

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
2025-12-25 21:22:08 -06:00
ccurme
d4b7a6542e release(langchain-classic): 1.0.1 (#34467) 2025-12-23 17:48:48 -05:00
Mason Daugherty
75b07b3d4e docs(core): update to indicate betas (#34457) 2025-12-22 17:54:37 -06:00
Mason Daugherty
2e0bed6a21 release(core): 1.2.5 (#34456) 2025-12-22 17:37:44 -06:00
ccurme
5ec0fa69de fix(core): serialization patch (#34455)
- `allowed_objects` kwarg in `load`
- escape lc-ser formatted dicts on `dump`
- fix for jinja2

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
2025-12-22 17:33:31 -06:00
Christophe Bornet
6a416c6186 style(langchain): add ruff rules PT (#34434) 2025-12-21 19:31:50 -06:00
Vishwajeet Kumar
3dcafac79b feat(langchain): enhance init_chat_model with improved validation (#34226)
## Summary
Enhances the `init_chat_model` function with comprehensive input
validation, improved model inference patterns, and better error handling
to provide a significantly improved user experience.

## Changes Made
-  **Input Validation**: Added comprehensive type and value checking
for all parameters
-  **Enhanced Model Inference**: Improved pattern matching with
case-insensitive support and new model patterns
-  **Better Error Messages**: Detailed error messages with examples and
documentation links
-  **Comprehensive Tests**: Added extensive test coverage for all new
functionality
-  **Documentation**: Enhanced docstrings and examples

## Backward Compatibility
All changes are fully backward compatible. No breaking changes
introduced.

## Testing
- Added 6 new test functions covering input validation, model inference,
and error handling
- All existing tests continue to pass
- Comprehensive parametrized testing for various model patterns

## User Experience Improvements
- Better error messages help users quickly resolve configuration issues
- Enhanced model inference reduces the need to specify providers
explicitly
- Comprehensive input validation catches issues early with helpful
guidance

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-19 23:50:19 -06:00
Christophe Bornet
d3e9c4d29d fix(core): RunnablePick method return types (#34208)
Following https://github.com/langchain-ai/langchain/pull/31321, the
`Output` type of `RunnablePick` is `Any`.
2025-12-19 23:47:46 -06:00
rari404
1cc4dc7cc9 fix(core): preserve Field(description=...) in @tool decorator (#34354)
## Summary

Fixes #34247

When using `Annotated[type, Field(description="...")]` syntax with the
`@tool` decorator, field descriptions were being lost during schema
generation. The `_get_annotation_description()` function only checked
for string annotations but not for Pydantic `FieldInfo` objects.

## Changes

- Extended `_get_annotation_description()` to also extract descriptions
from `FieldInfo` objects within `Annotated` types
- Added import for `pydantic.fields.FieldInfo`
- Added unit test to verify `Field(description=...)` is preserved

## Why this approach

The fix is minimal and targeted - it extends the existing description
extraction logic rather than restructuring the schema generation. This
maintains backward compatibility while supporting both annotation
styles:

```python
# Both now work correctly:
topic: Annotated[str, "The research topic"]           # existing
topic: Annotated[str, Field(description="...")]       # now fixed
```

## Known limitation

This fix only handles `pydantic.fields.FieldInfo` (Pydantic v2). The v1
compatibility layer (`pydantic.v1.fields.FieldInfo`) is a different
class and will not have descriptions extracted. This is intentional:

- Pydantic v1 is deprecated; users should migrate to v2
- The v1 compat layer exists for legacy model migration, not new tool
definitions
- Duck-typing on `description` attribute could match unintended objects

If v1 `Field` support is needed, it can be addressed in a follow-up PR
with explicit handling.

## Testing

- Added `test_tool_field_description_preserved()` covering required and
optional params
- Verified existing `test_tool_annotated_descriptions` still passes
- Lint and type checks pass

---

> [!NOTE]
> This PR was developed with AI agent assistance (Factory/Droid).

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
2025-12-19 23:14:23 -06:00
Nhan Nguyen
398c067f30 fix(core): populate default args from tool's args_schema (#34399)
## Summary
- Fixes issue where Pydantic default values from `args_schema` were not
passed to tool functions when the caller omits optional arguments
- Modified `_parse_input()` in `libs/core/langchain_core/tools/base.py`
to include fields with non-None defaults
- Added unit tests to verify default args behavior for both sync and
async tools

## Problem
When a tool has an `args_schema` with default values:
```python
class SearchArgs(BaseModel):
    query: str = Field(..., description="Search query")
    page: int = Field(default=1, description="Page number")
    size: int = Field(default=10, description="Results per page")

@tool("search", args_schema=SearchArgs)
def search_tool(query: str, page: int, size: int) -> str:
    return f"query={query}, page={page}, size={size}"

# This threw: TypeError: search_tool() missing 2 required positional arguments
search_tool.invoke({"query": "test"})
```

The defaults from `args_schema` were being discarded because
`_parse_input()` filtered validated results to only include keys from
the original input.

## Solution
Changed the filtering logic to:
1. Include all fields that were in the original input (validated)
2. Also include fields with non-None defaults from the Pydantic schema

This applies user-defined defaults (like `Field(default=1)`) while
excluding synthetic fields from `*args`/`**kwargs` which have
`default=None`.

## Test plan
- [x] Added `test_tool_args_schema_default_values` - tests sync tool
with defaults
- [x] Added `test_tool_args_schema_default_values_async` - tests async
tool with defaults
- [x] All existing tests pass (150 passed, 4 skipped)
- [x] Lint passes

Fixes #34384

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2025-12-19 23:14:13 -06:00
rari404
d84eef667a fix(core): use tool_calls instead of deprecated function_call in get_buffer_string (#34355)
## Summary

Fixes #33970

`get_buffer_string` was only checking for the deprecated `function_call`
field in `additional_kwargs`, which modern LLM providers no longer
return. This fix updates the function to check for the modern
`tool_calls` field first, falling back to `function_call` for legacy
compatibility.

## Changes

- Check `AIMessage.tool_calls` first (modern standard)
- Fall back to `additional_kwargs["function_call"]` (legacy support)
- Added 3 unit tests covering tool_calls, empty content, and precedence
behavior

## Testing

```python
# Before fix: tool_calls info was lost
msg = AIMessage(content="Hi", tool_calls=[{"name": "search", ...}])
get_buffer_string([msg])  # "AI: Hi" (no tool info)

# After fix: tool_calls are included
get_buffer_string([msg])  # "AI: Hi[{\"name\": \"search\", ...}]"
```

- All existing `get_buffer_string` tests pass
- Legacy `function_call` behavior preserved

---

> [!NOTE]
> This PR was developed with AI agent assistance (Factory/Droid).

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
2025-12-19 22:37:56 -06:00
Mason Daugherty
8d93720c70 fix(fireworks): models used in tests & naming schema (#34125)
it appears their docs are wrong? will wait a few days and see
2025-12-19 22:21:17 -06:00
Mason Daugherty
85c401f648 feat(core): add PEP 702 __deprecated__ attribute support to @deprecated (#34257)
Adds [PEP 702](https://peps.python.org/pep-0702/) `__deprecated__`
attribute support to the `@deprecated` decorator, enabling IDE and type
checker integration for deprecation warnings.

---

PEP 702 introduced the `__deprecated__` attribute convention, which type
checkers (Pyright, mypy) and IDEs (VS Code with Pylance, PyCharm) can
use to surface deprecations directly in the editor. This PR sets
`__deprecated__` on all objects decorated with `@deprecated`.

With this change, developers using supported IDEs will see:

- **Strikethrough text** on deprecated symbols
- **Hover messages** showing the deprecation reason and suggested
alternative
- **Diagnostic warnings** during type checking (e.g., `pyright`, `mypy`)

### References

- [PEP 702 – Marking deprecations using the type
system](https://peps.python.org/pep-0702/)
- [`typing.deprecated`
specification](https://typing.python.org/en/latest/spec/directives.html#deprecated)
2025-12-19 21:07:37 -06:00
Mason Daugherty
04ec6cacaf fix(core): ensure tool_call_count is never null (#34431)
add truthiness check to guard against `None`
2025-12-19 21:04:01 -06:00
Mason Daugherty
ed9bd6e3ad feat(core): automatically count and store meta for tool call count (#33756)
Adds automatic tool call counting to tracing by means of a new
`store_tool_call_count_in_run()`, which calls on newly added
`count_tool_calls_in_run()`.

Runs on successful LLM completion. Does not run on errored runs.
2025-12-19 20:41:57 -06:00
Mason Daugherty
c739afd45b chore(infra): remove jupyter recommended extensions (#34430) 2025-12-19 20:24:58 -06:00
James
4fbeffcfee feat(core): add 'approximate' alias in place of count_tokens_approximately (#33045)
### Description: 
earlier we have to use like below:
```python
from langchain_core.messages import trim_messages
from langchain_core.messages.utils import count_tokens_approximately

trim_messages(..., token_counter=count_tokens_approximately)
```
Now can be used as like this also
```python
from langchain_core.messages import trim_messages

trim_messages(..., token_counter="approximate")
```
- [x] **Added tests**
- [x] **Lint and test**: Run this as I made change in langchain/core, uv
run --group test pytest tests/unit_tests/messages/test_utils.py -v
<img width="1006" height="66" alt="image"
src="https://github.com/user-attachments/assets/c6938c29-a781-4e7f-871b-8e888ee764b7"
/>

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2025-12-19 19:25:29 -06:00
Christophe Bornet
72f1d79022 chore(core): fix some ruff preview rules (#34425)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-19 14:33:42 -06:00
Saurav Sapkota
f6297ced67 fix(openai): handle function_call content in token counting (#34379) 2025-12-19 15:17:40 -05:00
Mohammad Mohtashim
4804bd6ec2 docs(langchain): Docstring improved to show Streaming custom events (#34353) 2025-12-19 14:15:10 -05:00
Mason Daugherty
10087ac024 release(core): 1.2.4 (#34429) 2025-12-19 13:05:17 -06:00
Christophe Lamarche
f752c1a07f feat(langchain): Add support to google_genai provider in init_embeddings (#34388) 2025-12-19 14:04:13 -05:00
Hunter Lovell
7902fa3238 feat(core): add usage_metadata to metadata in LangChainTracer (#34414)
Adds `usage_metadata` (token counts, etc.) to the run metadata in
`LangChainTracer`.

When an LLM run ends, usage metadata is extracted from all generations
and aggregated using the existing `add_usage` helper, then stored in
`run.extra["metadata"]["usage_metadata"]`.

The original data in outputs remains unchanged.

Also, see #34415

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-19 12:59:52 -06:00
Sujal M H
4be9407b09 fix(openai): filter function_call blocks in token counting (#34396) 2025-12-19 13:53:44 -05:00
Hunter Lovell
9225bff326 fix(core): defer persisting traces for iterator inputs (#34416)
ref https://github.com/langchain-ai/langchainjs/pull/9665

Fixes trace persistence for iterator/generator inputs (like
`RunnableGenerator`) where the full input isn't available at chain
start. Instead of POSTing a run with incomplete inputs on start and
PATCHing later, this defers the POST until chain end when inputs are
fully realized.

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
2025-12-19 12:45:22 -06:00
Mason Daugherty
d4cb740e0c revert(infra): temp disable lockfile CI check (#34428)
#34397
2025-12-19 12:42:11 -06:00
Sai-Srikar-Boddupalli
e5c9912a89 docs: Fix typo in Zapier NLA API description (#34424) 2025-12-19 13:23:10 -05:00
Christophe Bornet
8bca31f8c4 chore(core): fix some docstrings (#34426) 2025-12-19 13:08:10 -05:00
Kesku
c5baa3ac27 feat(perplexity): overhaul integration with official SDK and Search API (#34412) 2025-12-19 12:58:41 -05:00
ccurme
795e746ca7 release(core): 1.2.3 (#34421) 2025-12-18 15:06:32 -05:00
ccurme
6519a5675b fix(core): allow unknown blocks in convert_to_openai_messages (#34420) 2025-12-18 14:22:53 -05:00
ccurme
e9f7cd3e0e release(openai): 1.1.6: update max input tokens for gpt-5 series (#34419) 2025-12-18 12:49:59 -05:00
ccurme
5c94e47d14 release(openai): 1.1.5 (#34409) 2025-12-17 14:04:37 -05:00
ccurme
e0950f29b7 fix(openai): rely on langchain-core for setting chunk_position (#34404) 2025-12-17 12:44:12 -05:00
Mason Daugherty
71778cb721 feat(infra): add CI check for out of date lockfiles (#34397) 2025-12-16 22:23:25 -05:00
Mason Daugherty
37d8666276 release(openai): 1.1.4 (#34395) 2025-12-16 14:55:18 -05:00
Mason Daugherty
c286c06f16 revert(openai): switch model from nano to mini when using flex (#34394)
Reverts langchain-ai/langchain#34336
2025-12-16 14:45:19 -05:00
Mason Daugherty
b83e9b1056 release(standard-tests): 1.1.1 (#34393) 2025-12-16 14:25:12 -05:00
Mason Daugherty
c1f66611fc chore(core): bump lockfile (#34392) 2025-12-16 14:21:11 -05:00
Mason Daugherty
f93bc48915 release(core): 1.2.2 (#34391) 2025-12-16 14:17:47 -05:00
Mason Daugherty
516d74b6df fix(core): use get_type_hints for Python 3.14 TypedDict compatibility (#34390)
Replace direct `__annotations__` access with `get_type_hints()` in
`_convert_any_typed_dicts_to_pydantic` to handle [PEP
649](https://peps.python.org/pep-0649/) deferred annotations in Python
3.14:

> [`Changed in version 3.14: Annotations are now lazily evaluated by
default`](https://docs.python.org/3/reference/compound_stmts.html#annotations)

Before:

```python
class MyTool(TypedDict):
    name: str

MyTool.__annotations__  # {'name': 'str'} - string, not type
issubclass('str', ...)  # TypeError: arg 1 must be a class
```

After:

```python
get_type_hints(MyTool)  # {'name': <class 'str'>} - actual type
```

Fixes #34291
2025-12-16 14:08:01 -05:00
Mason Daugherty
c85f7b6061 docs(standard-tests): throw more descriptive errors for some streaming cases (#34389) 2025-12-16 11:22:35 -05:00
tom1299
f167c35243 fix(openai): Correct hyperlinks in documentation of function with_structured_output (#34385)
Just a small fix of some broken hyperlinks in the documentation of the
function `langchain_openai/chat_models/base.py#with_structured_output`
and a rephrase of the reference to supported models.

Co-authored-by: Thomas Reuhl <thomas.reuhl@telekom.de>
2025-12-16 10:49:57 -05:00
dependabot[bot]
b8a76cb6e9 chore(deps): bump actions/download-artifact from 6 to 7 (#34361)
Bumps
[actions/download-artifact](https://github.com/actions/download-artifact)
from 6 to 7.
<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>v7.0.0</h2>
<h2>v7 - What's new</h2>
<blockquote>
<p>[!IMPORTANT]
actions/download-artifact@v7 now runs on Node.js 24 (<code>runs.using:
node24</code>) and requires a minimum Actions Runner version of 2.327.1.
If you are using self-hosted runners, ensure they are updated before
upgrading.</p>
</blockquote>
<h3>Node.js 24</h3>
<p>This release updates the runtime to Node.js 24. v6 had preliminary
support for Node 24, however this action was by default still running on
Node.js 20. Now this action by default will run on Node.js 24.</p>
<h2>What's Changed</h2>
<ul>
<li>Update GHES guidance to include reference to Node 20 version by <a
href="https://github.com/patrikpolyak"><code>@​patrikpolyak</code></a>
in <a
href="https://redirect.github.com/actions/download-artifact/pull/440">actions/download-artifact#440</a></li>
<li>Download Artifact Node24 support by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/415">actions/download-artifact#415</a></li>
<li>fix: update <code>@​actions/artifact</code> to fix Node.js 24
punycode deprecation by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/451">actions/download-artifact#451</a></li>
<li>prepare release v7.0.0 for Node.js 24 support by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/452">actions/download-artifact#452</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/patrikpolyak"><code>@​patrikpolyak</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/download-artifact/pull/440">actions/download-artifact#440</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/download-artifact/pull/415">actions/download-artifact#415</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/download-artifact/compare/v6.0.0...v7.0.0">https://github.com/actions/download-artifact/compare/v6.0.0...v7.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="37930b1c2a"><code>37930b1</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/452">#452</a>
from actions/download-artifact-v7-release</li>
<li><a
href="72582b9e0a"><code>72582b9</code></a>
doc: update readme</li>
<li><a
href="0d2ec9d4cb"><code>0d2ec9d</code></a>
chore: release v7.0.0 for Node.js 24 support</li>
<li><a
href="fd7ae8fda6"><code>fd7ae8f</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/451">#451</a>
from actions/fix-storage-blob</li>
<li><a
href="d484700543"><code>d484700</code></a>
chore: restore minimatch.dep.yml license file</li>
<li><a
href="03a808050e"><code>03a8080</code></a>
chore: remove obsolete dependency license files</li>
<li><a
href="56fe6d904b"><code>56fe6d9</code></a>
chore: update <code>@​actions/artifact</code> license file to 5.0.1</li>
<li><a
href="8e3ebc4ab4"><code>8e3ebc4</code></a>
chore: update package-lock.json with <code>@​actions/artifact</code><a
href="https://github.com/5"><code>@​5</code></a>.0.1</li>
<li><a
href="1e3c4b4d49"><code>1e3c4b4</code></a>
fix: update <code>@​actions/artifact</code> to ^5.0.0 for Node.js 24
punycode fix</li>
<li><a
href="458627d354"><code>458627d</code></a>
chore: use local <code>@​actions/artifact</code> package for Node.js 24
testing</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/download-artifact/compare/v6...v7">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=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>
2025-12-15 09:57:13 -05:00
dependabot[bot]
dbcdf0b702 chore(deps): bump actions/upload-artifact from 5 to 6 (#34360)
Bumps
[actions/upload-artifact](https://github.com/actions/upload-artifact)
from 5 to 6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/upload-artifact/releases">actions/upload-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.0</h2>
<h2>v6 - What's new</h2>
<blockquote>
<p>[!IMPORTANT]
actions/upload-artifact@v6 now runs on Node.js 24 (<code>runs.using:
node24</code>) and requires a minimum Actions Runner version of 2.327.1.
If you are using self-hosted runners, ensure they are updated before
upgrading.</p>
</blockquote>
<h3>Node.js 24</h3>
<p>This release updates the runtime to Node.js 24. v5 had preliminary
support for Node.js 24, however this action was by default still running
on Node.js 20. Now this action by default will run on Node.js 24.</p>
<h2>What's Changed</h2>
<ul>
<li>Upload Artifact Node 24 support by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/719">actions/upload-artifact#719</a></li>
<li>fix: update <code>@​actions/artifact</code> for Node.js 24 punycode
deprecation by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/744">actions/upload-artifact#744</a></li>
<li>prepare release v6.0.0 for Node.js 24 support by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/upload-artifact/pull/745">actions/upload-artifact#745</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/upload-artifact/compare/v5.0.0...v6.0.0">https://github.com/actions/upload-artifact/compare/v5.0.0...v6.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="b7c566a772"><code>b7c566a</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/745">#745</a>
from actions/upload-artifact-v6-release</li>
<li><a
href="e516bc8500"><code>e516bc8</code></a>
docs: correct description of Node.js 24 support in README</li>
<li><a
href="ddc45ed9bc"><code>ddc45ed</code></a>
docs: update README to correct action name for Node.js 24 support</li>
<li><a
href="615b319bd2"><code>615b319</code></a>
chore: release v6.0.0 for Node.js 24 support</li>
<li><a
href="017748b48f"><code>017748b</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/upload-artifact/issues/744">#744</a>
from actions/fix-storage-blob</li>
<li><a
href="38d4c7997f"><code>38d4c79</code></a>
chore: rebuild dist</li>
<li><a
href="7d27270e0c"><code>7d27270</code></a>
chore: add missing license cache files for <code>@​actions/core</code>,
<code>@​actions/io</code>, and mi...</li>
<li><a
href="5f643d3c94"><code>5f643d3</code></a>
chore: update license files for <code>@​actions/artifact</code><a
href="https://github.com/5"><code>@​5</code></a>.0.1 dependencies</li>
<li><a
href="1df1684032"><code>1df1684</code></a>
chore: update package-lock.json with <code>@​actions/artifact</code><a
href="https://github.com/5"><code>@​5</code></a>.0.1</li>
<li><a
href="b5b1a91840"><code>b5b1a91</code></a>
fix: update <code>@​actions/artifact</code> to ^5.0.0 for Node.js 24
punycode fix</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/upload-artifact/compare/v5...v6">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 09:56:56 -05:00
ccurme
beb2ee6edf chore(infra): add openai back to core release test matrix (#34372)
Reverts langchain-ai/langchain#34020
2025-12-15 09:56:16 -05:00
ccurme
9f61ed8b81 release(langchain): 1.2 (#34373) 2025-12-15 09:49:49 -05:00
ccurme
6cff82d02e release(core): 1.2.1 (#34370) 2025-12-15 09:28:46 -05:00
Mason Daugherty
0cd72b50fb release(text-splitters): 1.1.0 (#34346) 2025-12-13 20:13:03 -05:00
Mason Daugherty
1a3cd46d88 release(anthropic): 1.3.1 (#34337) 2025-12-12 17:37:55 -05:00
Viktor Taranenko
470160cf81 fix(anthropic): prevent crash with cache_control and empty message content (#34025) 2025-12-12 17:32:11 -05:00
Mason Daugherty
20b8342fdf test(openai): switch model from nano to mini when using flex (#34336)
Issues with combining flex and nano

```shell
FAILED tests/integration_tests/chat_models/test_base.py::test_openai_invoke - openai.InternalServerError: Error code: 500 - {'error': {'message': 'The server had an error while processing your request. Sorry about that!', 'type': 'server_error', 'param': None, 'code': None}}
FAILED tests/integration_tests/chat_models/test_base.py::test_stream - openai.InternalServerError: Error code: 500 - {'error': {'message': 'The server had an error processing your request. Sorry about that! You can retry your request, or contact us through our help center at help.openai.com if you keep seeing this error. (Please include the request ID req_e726769d95994fd4bccbe55680a35f59 in your email.)', 'type': 'server_error', 'param': None, 'code': None}}
FAILED tests/integration_tests/chat_models/test_base.py::test_flex_usage_responses[False] - openai.InternalServerError: Error code: 500 - {'error': {'message': 'An error occurred while processing your request. You can retry your request, or contact us through our help center at help.openai.com if the error persists. Please include the request ID req_935316418319494d8682e4adcd67ab47 in your message.', 'type': 'server_error', 'param': None, 'code': 'server_error'}}
FAILED tests/integration_tests/chat_models/test_base.py::test_flex_usage_responses[True] - openai.APIError: An error occurred while processing your request. You can retry your request, or contact us through our help center at help.openai.com if the error persists. Please include the request ID req_f3c164d0d1f045a5a0f5965ab5c253bf in your message.
```
2025-12-12 17:17:11 -05:00
Mason Daugherty
2f8af61218 release(huggingface): 1.2.0 (#34335) 2025-12-12 17:16:38 -05:00
Mason Daugherty
81758e22f3 release(mistralai): 1.1.1 (#34334) 2025-12-12 17:08:30 -05:00
Mason Daugherty
54241f4d06 fix(langchain): shell output multithreading race condition (#34333)
If the `stdout` "done marker" arrives before the `stderr` output is
enqueued, the method returns early without capturing the `stderr` line.

The two reader threads run independently with no synchronization
guaranteeing `stderr` arrives before the done marker.

In environments with Python 3.10, timing differences can cause the
`stdout` marker to win the race, resulting in `<no output>` instead of
`[stderr]` error.

Observed as a flaky test on `test_stderr_output_labeling` in CI:

```shell
FAILED tests/unit_tests/agents/middleware/implementations/test_shell_tool.py::test_stderr_output_labeling - AssertionError: assert '[stderr] error' in '<no output>'
```
2025-12-12 17:06:18 -05:00
Mason Daugherty
7c9223d2b2 release(standard-tests): 1.1.0 (#34331) 2025-12-12 16:55:41 -05:00
Mason Daugherty
3342e4d62d release(groq): 1.1.1 (#34332) 2025-12-12 16:52:56 -05:00
Mason Daugherty
5842110dbc release(ollama): 1.0.1 (#34330) 2025-12-12 16:46:28 -05:00
Mason Daugherty
62db04c43a revert: make integration tests output verbose (#34329)
Reverts langchain-ai/langchain#34327
2025-12-12 16:40:41 -05:00
dumko2001
fb892ee50a feat(groq): Allow kwargs in with_structured_output to override tool_choice (#34053) 2025-12-12 16:16:26 -05:00
Mason Daugherty
8ad0e9f267 chore(infra,openai): make integration tests output verbose (#34327)
to match anthropic

without this, have to wait until all tests fail to begin debugging / see
output

also add timeout since it was missing
2025-12-12 15:34:01 -05:00
Mason Daugherty
d0b13e926d release(openai): 1.1.3 (#34325) 2025-12-12 15:18:02 -05:00
Mason Daugherty
6fa4a45311 chore(anthropic): bump min core version (#34326) 2025-12-12 15:17:36 -05:00
Mason Daugherty
97dd5f2cb8 release(anthropic): 1.3.0 (#34324) 2025-12-12 15:10:49 -05:00
Deshbhushan Patil
2a82fbc0ff test(ollama): Add unit test for ChatOllama reasoning parameter (#34095) 2025-12-12 14:48:04 -05:00
Towseef Altaf
0e5e33ba03 fix(openai): correct image resize aspect ratio caps (#34192) 2025-12-12 14:34:17 -05:00
Christophe Bornet
fc35544e0d chore(standard-tests): enable mypy disallow_any_generics rule (#34222)
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
Co-authored-by: Sydney Runkle <sydneymarierunkle@gmail.com>
2025-12-12 14:30:27 -05:00
rari404
15cc090e52 fix(core): handle None arguments in parse_tool_call (#34242) 2025-12-12 13:57:34 -05:00
rari404
0f940d74b2 feat(text-splitters): add R programming language support (#34241) 2025-12-12 13:34:22 -05:00
Nhan Nguyen
7829b722b1 fix(mistralai): handle null content in tool call responses (#34268) 2025-12-12 13:18:56 -05:00
Christophe Bornet
914730cf8d chore(core): fix some types related to ToolCallChunk (#34283) 2025-12-12 13:15:57 -05:00
ccurme
c3738ea376 chore(anthropic): make test agnostic of python version (#34320) 2025-12-12 18:10:14 +00:00
ccurme
cd124a0949 release(core): 1.2 (#34319) 2025-12-12 13:08:34 -05:00
Mason Daugherty
57ff48e62e docs(anthropic): clean up docstrings (#34317)
migration to docs
2025-12-12 11:30:34 -05:00
ccurme
bc232e6d03 release(chroma): 1.1 (#34316) 2025-12-12 11:20:47 -05:00
itaismith
be32382d92 feat(chroma): Add Search API (#34273) 2025-12-12 11:14:47 -05:00
Georgey
16c984ef0a fix(langchain-classic): fix init_chat_model for HuggingFace models (#33943) 2025-12-12 11:05:48 -05:00
Mason Daugherty
13dd115d1d docs(anthropic): nit comments (#34314) 2025-12-12 10:33:23 -05:00
Mason Daugherty
75d365418b style(core): docs nit (#34312) 2025-12-12 10:33:14 -05:00
Mason Daugherty
2cff369cdc feat(anthropic): accept TypedDict for built-in tool types (#34279)
Widen `bind_tools` to accept `TypedDict` via `Mapping` so that users may
import and use Anthropic's built-in tool types:

```python
import subprocess

from anthropic.types.beta import BetaToolBash20250124Param
from langchain.tools import tool

tool_spec = BetaToolBash20250124Param(
    name="bash",
    type="bash_20250124",
    strict=True,
)

@tool(extras={"provider_tool_definition": tool_spec})
def bash(*, command: str, restart: bool = False, **kw):
    """Execute a bash command."""
    if restart:
        return "Bash session restarted"
    try:
        result = subprocess.run(
            command,
            shell=True,
            capture_output=True,
            text=True,
            timeout=30,
        )
        return result.stdout + result.stderr
    except Exception as e:
        return f"Error: {e}"

# Bind bash tool to your model
```

---------

Co-authored-by: Chester Curme <chester.curme@gmail.com>
2025-12-12 10:29:12 -05:00
Christophe Bornet
f5b6eecf72 refactor(standard-tests): improve VCR config (#33968)
Use of the fixture `_base_vcr_config` is deprecated with alternative
function `base_vcr_config()`
This way:
* we don't need to import `_base_vcr_config` seen as unused (which leads
to ruff violations PLC0414 and F811)
* we don't need to make a copy since a new dict is created at each
function invocation

Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-12 10:14:26 -05:00
Jacob Lee
a528ea1796 feat(openai): Use responses API if model is gpt-5.2-pro (#34306) 2025-12-12 10:11:15 -05:00
Paul
bf6a5eb122 fix(huggingface): Helper logic for init_chat_model with HuggingFace backend (#34259) 2025-12-12 10:05:16 -05:00
j3r0lin
5720dea41b fix(openai): handle missing 'text' key in responses API content blocks (#34198) 2025-12-12 09:39:12 -05:00
Mohammad Mohtashim
087107557f chore(ollama,groq): Filtering Parameters in bind_tools for Ollama and Groq (#34167) 2025-12-12 09:24:24 -05:00
dumko2001
05ba853548 fix(ollama): pop unsupported 'strict' argument in ChatOllama (#34114) 2025-12-12 09:13:11 -05:00
Christophe Bornet
3fb90666be chore(langchain): cleanup ruff config (#32810)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
Co-authored-by: Sydney Runkle <sydneymarierunkle@gmail.com>
2025-12-12 09:08:48 -05:00
Sydney Runkle
6a2a149f89 fix: little lint thing (#34310)
to be merged into https://github.com/langchain-ai/langchain/pull/32810
2025-12-12 08:47:51 -05:00
Christophe Bornet
bbc1d46efe chore(langchain): check agents integration tests with mypy (#34308) 2025-12-12 07:55:34 -05:00
Mason Daugherty
d6b5f05f33 refactor(anthropic): comments and _BUILTIN_TOOL_PREFIXES (#34305) 2025-12-11 16:57:22 -05:00
Mason Daugherty
10377a7373 fix(core): widen openai tool/function conversion input type to Mapping (#34304)
Motivated by changes to accept `TypedDict` tool types (e.g. in case of
Anthropic/Claude built-in tools)
2025-12-11 16:33:53 -05:00
ccurme
373ad8ac2c release(openai): 1.1.2 (#34302) 2025-12-11 16:20:57 -05:00
Mason Daugherty
5eec11e2db docs(anthropic): fix line number highlighting (#34303) 2025-12-11 16:12:01 -05:00
Jacob Lee
badc0cf1b6 fix(openai): Allow temperature when reasoning is set to the string 'none' (#34298)
Co-authored-by: Chester Curme <chester.curme@gmail.com>
2025-12-11 15:57:04 -05:00
Mason Daugherty
3b7abdff96 feat(anthropic): auto-apply mcp beta header (#34301)
and update docstring example
2025-12-11 15:49:32 -05:00
Mason Daugherty
4aebfbad59 docs(anthropic): use named betas param in docstring example (#34300) 2025-12-11 15:48:13 -05:00
Mason Daugherty
ae1f03fbe0 docs(anthropic): cleanup nits (#34299) 2025-12-11 15:17:56 -05:00
ccurme
46dbb3967e chore(anthropic): update test_tool_search cassette (#34297) 2025-12-11 10:53:52 -05:00
Mason Daugherty
dd0b990ba5 chore(infra): delete copilot instructions (#34294)
and some files we inherit from org root
2025-12-11 01:51:00 -05:00
ccurme
5aa46501cf fix(langchain): add sentinel value to ProviderStrategy / strict (#34290) 2025-12-10 16:25:06 -05:00
ccurme
92df109dd5 chore(langchain): add end to end test for strict mode in provider strategy (#34289) 2025-12-10 15:48:47 -05:00
Towseef Altaf
d27fb0c432 feat(langchain,openai): add strict flag to ProviderStrategy structured output (#34149) 2025-12-10 15:35:23 -05:00
ccurme
69dd39c461 fix(anthropic): ignore null values of caller on tool_use blocks (#34286) 2025-12-10 13:13:02 -05:00
ccurme
41cebfe4fb chore(core): add admonitions around use of load (#34285) 2025-12-10 11:36:46 -05:00
ccurme
5350967ddc feat(anthropic): support mcp_toolset in bind_tools (#34284) 2025-12-10 14:39:35 +00:00
Mason Daugherty
7542278997 feat(core,anthropic): extras on BaseTool (#34120) 2025-12-10 09:37:14 -05:00
Mason Daugherty
ff6e3558d7 docs(fireworks,groq,huggingface,mistralai,ollama,openai): x-ref convert_to_openai_tool (#34276) 2025-12-09 19:51:04 -05:00
Mason Daugherty
585e12e53b chore(infra): delete SECURITY.md (#34270)
Will be inherited from `langchain-ai/.github`
2025-12-09 15:01:53 -05:00
Sydney Runkle
73ba156a7d release: langchain-core 1.1.3 (#34266) 2025-12-09 14:50:53 +00:00
Eugene Yurtsev
395c8d0bd4 fix(core): undo jinja2 restrictions (#34072)
Reverting jinja2 restrictions that made the feature unusable
2025-12-09 09:46:36 -05:00
Sydney Runkle
34d31b8394 fix: remove partial usage for retriever func + afunc (#34265)
Added test that fails on `master`.

`ToolNode` uses `get_type_hints` which doesn't work properly w/ partial
funcs on Python 3.12+

The diff here is nice anyways when we inline the logic.
2025-12-09 14:43:14 +00:00
Eugene Yurtsev
2aa0555941 chore(infra): update security.md file (#34258)
Move to github security features for intake

---------

Co-authored-by: Lauren Hirata Singh <lauren@langchain.dev>
2025-12-08 21:47:55 +00:00
Mason Daugherty
dff229d018 fix(openai): add missing tools param to ChatOpenAI with_structured_output (#34075) 2025-12-08 15:47:31 -05:00
Mason Daugherty
b009ca4d23 feat(standard-tests): invocation model override (#34170)
inspired by noticing `ChatGoogleGenerativeAI` failed to do so
2025-12-08 15:44:22 -05:00
Mason Daugherty
0254c12cb0 feat(standard-tests): ensure only one chunk sets model_name in usage_metadata (#34224) 2025-12-08 15:41:39 -05:00
Mason Daugherty
2faed37ff1 feat(anthropic): document and test fine grained tool streaming (#34118)
https://platform.claude.com/docs/en/agents-and-tools/tool-use/fine-grained-tool-streaming
2025-12-08 15:34:56 -05:00
Mason Daugherty
d886dcfba5 fix(standard-tests)!: remove deprecated has_tool_choice property (#34174)
Deprecated since `0.3.15`

This was marked as being removed in `0.3.20` but never was
2025-12-08 15:31:55 -05:00
Mason Daugherty
91d5ca275d feat(anthropic): use model profile for max output tokens (#34163)
Need(?) to adjust tests to also pull from model profile? currently
hardcoded
2025-12-08 15:31:16 -05:00
Mason Daugherty
dcb670f395 feat(anthropic): auto append relevant beta headers for computer use (#34117)
in addition to documenting it


https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool
2025-12-08 15:25:36 -05:00
ccurme
85012ae601 chore(infra): update default lib on release workflow (#34256) 2025-12-08 14:35:43 -05:00
ccurme
aa0f4fb927 release(langchain): 1.1.3 (#34255) 2025-12-08 14:29:40 -05:00
Sydney Runkle
d18cdc6f32 feat: add agent name to AIMessage (#34254) 2025-12-08 14:23:12 -05:00
Mason Daugherty
8a5f46322b feat(anthropic): tool search support (#34119) 2025-12-08 10:46:37 -05:00
Mason Daugherty
a0e86b18bf release(core): 1.1.2 (#34253)
and bump deps
2025-12-08 10:24:03 -05:00
Nhan Nguyen
6affec92ce fix(core): pass tool_call_id to on_tool_start callback (#34235)
## Summary

When invoking a tool with a `ToolCall`, the `tool_call_id` is extracted
but was **not forwarded** to callback handlers in `on_tool_start`. This
made it impossible for callback handlers to correlate tool executions
with the original LLM tool calls.

This fix adds `tool_call_id=tool_call_id` to both:
- Sync `run()` method's `on_tool_start` call
- Async `arun()` method's `on_tool_start` call

## Changes

- **`libs/core/langchain_core/tools/base.py`**: Added `tool_call_id`
parameter to `on_tool_start` calls (2 lines)
- **`libs/core/tests/unit_tests/test_tools.py`**: Added 6 comprehensive
tests covering:
  - Sync tool invocation via `invoke()`
  - Async tool invocation via `ainvoke()`
  - `tool_call_id` is `None` when invoked without a ToolCall
  - Empty string `tool_call_id` edge case
  - Direct `run()` method
  - Direct `arun()` method

## Test plan

- [x] All 147 existing tests pass
- [x] 6 new tests added and passing
- [x] Linting passes

Fixes #34168

---

This PR was developed with AI assistance (Claude).

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-08 10:15:18 -05:00
Christophe Bornet
a64aee310c chore(core): improve typing of messages utils functions (#34225)
With this we get the correct types for `_runnable_support` annotated
functions.
* return list[BaseMessage] when messages is not None
* return Runnable when messages is None
* typing of function args
2025-12-08 09:59:43 -05:00
Paul
ba6c2590ae fix(core): prevent async task garbage collection (RUF006) (#34238)
# PR Title: fix(core): prevent async task garbage collection (RUF006)

## Description
This PR addresses a cryptic issue (flagged by Ruff rule RUF006) where
`asyncio` tasks created via `loop.create_task` could be garbage
collected mid-execution because no strong reference was maintained.

In `libs/core/langchain_core/language_models/llms.py`, the retry
decorator's `_before_sleep` hook creates a fire-and-forget task for
logging/callbacks. If the garbage collector runs before this task
completes, the task may be destroyed, leading to silent failures.

## Changes
- Introduced a module-level set `_background_tasks` to hold strong
references to running tasks.
- Updated `_before_sleep` to add new tasks to this set.
- Added a `done_callback` to remove the task from the set upon
completion, preventing memory leaks.

## Verification
- Verified logic with a standalone script to ensure tasks are
added/removed from the set correctly.
- This is a standard pattern recommended in the Python `asyncio`
documentation.

## Checklist
- [x] I have read the contributing guidelines.
- [x] I have run tests locally (logic verification).

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 09:50:55 -05:00
Christophe Bornet
bb71f53585 chore(core): use anext and deprecate py_anext (#34211)
LangChain uses Python 3.10+ so `py_anext` isn't needed anymore.

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-12-08 09:50:40 -05:00
Mason Daugherty
9875ffbabc feat(core): support google maps grounding in genai block translator (#34244)
https://github.com/langchain-ai/langchain-google/pull/1330

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 09:44:43 -05:00
ccurme
b5efafe80c release(openai): 1.1.1 (#34252) 2025-12-08 09:23:13 -05:00
Marlene
ff3353f02f fix(openai): Fixing error that comes up using the Responses API with built-in tools and custom tools (#34136) 2025-12-08 09:10:44 -05:00
Mason Daugherty
3ace4e3680 docs(core,groq,openai): nits for ref docs (#34243) 2025-12-07 19:45:38 -05:00
Mason Daugherty
80c397019f docs(core): improve style for refs (#34227) 2025-12-05 15:41:22 -05:00
Mason Daugherty
4a42158e6c feat(anthropic): add effort support (#34116) 2025-12-05 13:44:42 -05:00
Mason Daugherty
7ba3e80057 test(openai): mark test_structured_output_and_tools flaky (#34223)
Often raises `KeyError: 'explanation'`
2025-12-05 11:26:17 -05:00
김주호
50e27a447b feat(langchain): add support for Upstage (Solar) in init_chat_model (#34220) 2025-12-05 09:37:37 -05:00
Sydney Runkle
78c10f8790 chore: update core dep in lockfiles (#34216) 2025-12-04 15:30:42 -05:00
Mason Daugherty
ccfc9f795a chore(infra): delete duplicate forum link (#34214) 2025-12-04 14:53:49 -05:00
Mason Daugherty
b21926fe6c docs(core): update StrOutputParser docstring (#34213) 2025-12-04 14:53:36 -05:00
Sydney Runkle
f1ad0da8f5 release: langchain-core 1.1.1 (#34212) 2025-12-04 14:44:18 -05:00
455 changed files with 23961 additions and 11002 deletions

View File

@@ -26,7 +26,7 @@
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Run commands after the container is created
"postCreateCommand": "uv sync && echo 'LangChain (Python) dev environment ready!'",
"postCreateCommand": "cd libs/langchain_v1 && uv sync && echo 'LangChain (Python) dev environment ready!'",
// Configure tool-specific properties.
"customizations": {
"vscode": {
@@ -42,7 +42,7 @@
"GitHub.copilot-chat"
],
"settings": {
"python.defaultInterpreterPath": ".venv/bin/python",
"python.defaultInterpreterPath": "libs/langchain_v1/.venv/bin/python",
"python.formatting.provider": "none",
"[python]": {
"editor.formatOnSave": true,

34
.dockerignore Normal file
View File

@@ -0,0 +1,34 @@
# Git
.git
.github
# Python
__pycache__
*.pyc
*.pyo
.venv
.mypy_cache
.pytest_cache
.ruff_cache
*.egg-info
.tox
# IDE
.idea
.vscode
# Worktree
worktree
# Test artifacts
.coverage
htmlcov
coverage.xml
# Build artifacts
dist
build
# Misc
*.log
.DS_Store

View File

@@ -1,132 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
conduct@langchain.dev.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -1,6 +0,0 @@
# Contributing to LangChain
Hi there! Thank you for even being interested in contributing to LangChain.
As an open-source project in a rapidly developing field, we are extremely open to contributions, whether they involve new features, improved infrastructure, better documentation, or bug fixes.
To learn how to contribute to LangChain, please follow the [contribution guide here](https://docs.langchain.com/oss/python/contributing).

View File

@@ -1,5 +1,5 @@
name: "\U0001F41B Bug Report"
description: Report a bug in LangChain. To report a security issue, please instead use the security option below. For questions, please use the LangChain forum.
description: Report a bug in LangChain. To report a security issue, please instead use the security option (below). For questions, please use the LangChain forum (below).
labels: ["bug"]
type: bug
body:
@@ -76,7 +76,7 @@ body:
validations:
required: true
attributes:
label: Example Code (Python)
label: Reproduction Steps / Example Code (Python)
description: |
Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case.

View File

@@ -1,9 +1,6 @@
blank_issues_enabled: false
version: 2.1
contact_links:
- name: 📚 Documentation issue
url: https://github.com/langchain-ai/docs/issues/new?template=01-langchain.yml
about: Report an issue related to the LangChain documentation
- name: 💬 LangChain Forum
url: https://forum.langchain.com/
about: General community discussions and support
@@ -13,6 +10,6 @@ contact_links:
- name: 📚 API Reference Documentation
url: https://reference.langchain.com/python/
about: View the official LangChain API reference documentation
- name: 💬 LangChain Forum
url: https://forum.langchain.com/
about: Ask questions and get help from the community
- name: 📚 Documentation issue
url: https://github.com/langchain-ai/docs/issues/new?template=01-langchain.yml
about: Report an issue related to the LangChain documentation

View File

@@ -1,5 +1,5 @@
name: "✨ Feature Request"
description: Request a new feature or enhancement for LangChain. For questions, please use the LangChain forum.
description: Request a new feature or enhancement for LangChain. For questions, please use the LangChain forum (below).
labels: ["feature request"]
type: feature
body:

View File

@@ -17,7 +17,7 @@ Thank you for contributing to LangChain! Follow these steps to have your pull re
- Write 1-2 sentences summarizing the change.
- If this PR addresses a specific issue, please include "Fixes #ISSUE_NUMBER" in the description to automatically close the issue when the PR is merged.
- If there are any breaking changes, please clearly describe them.
- If this PR depends on another PR being merged first, please include "Depends on #PR_NUMBER" inthe description.
- If this PR depends on another PR being merged first, please include "Depends on #PR_NUMBER" in the description.
3. Run `make format`, `make lint` and `make test` from the root of the package(s) you've modified.
@@ -27,4 +27,4 @@ Additional guidelines:
- We ask that if you use generative AI for your contribution, you include a disclaimer.
- PRs should not touch more than one package unless absolutely necessary.
- Do not update the `uv.lock` files unless or add dependencies to `pyproject.toml` files (even optional ones) unless you have explicit permission to do so by a maintainer.
- Do not update the `uv.lock` files or add dependencies to `pyproject.toml` files (even optional ones) unless you have explicit permission to do so by a maintainer.

View File

@@ -27,7 +27,7 @@ runs:
using: composite
steps:
- name: Install uv and set the python version
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
version: ${{ env.UV_VERSION }}
python-version: ${{ inputs.python-version }}

View File

@@ -1,330 +0,0 @@
# Global Development Guidelines for LangChain Projects
## Core Development Principles
### 1. Maintain Stable Public Interfaces ⚠️ CRITICAL
**Always attempt to preserve function signatures, argument positions, and names for exported/public methods.**
**Bad - Breaking Change:**
```python
def get_user(id, verbose=False): # Changed from `user_id`
pass
```
**Good - Stable Interface:**
```python
def get_user(user_id: str, verbose: bool = False) -> User:
"""Retrieve user by ID with optional verbose output."""
pass
```
**Before making ANY changes to public APIs:**
- Check if the function/class is exported in `__init__.py`
- Look for existing usage patterns in tests and examples
- Use keyword-only arguments for new parameters: `*, new_param: str = "default"`
- Mark experimental features clearly with docstring admonitions (using MkDocs Material, like `!!! warning`)
🧠 *Ask yourself:* "Would this change break someone's code if they used it last week?"
### 2. Code Quality Standards
**All Python code MUST include type hints and return types.**
**Bad:**
```python
def p(u, d):
return [x for x in u if x not in d]
```
**Good:**
```python
def filter_unknown_users(users: list[str], known_users: set[str]) -> list[str]:
"""Filter out users that are not in the known users set.
Args:
users: List of user identifiers to filter.
known_users: Set of known/valid user identifiers.
Returns:
List of users that are not in the known_users set.
"""
return [user for user in users if user not in known_users]
```
**Style Requirements:**
- Use descriptive, **self-explanatory variable names**. Avoid overly short or cryptic identifiers.
- Attempt to break up complex functions (>20 lines) into smaller, focused functions where it makes sense
- Avoid unnecessary abstraction or premature optimization
- Follow existing patterns in the codebase you're modifying
### 3. Testing Requirements
**Every new feature or bugfix MUST be covered by unit tests.**
**Test Organization:**
- Unit tests: `tests/unit_tests/` (no network calls allowed)
- Integration tests: `tests/integration_tests/` (network calls permitted)
- Use `pytest` as the testing framework
**Test Quality Checklist:**
- [ ] Tests fail when your new logic is broken
- [ ] Happy path is covered
- [ ] Edge cases and error conditions are tested
- [ ] Use fixtures/mocks for external dependencies
- [ ] Tests are deterministic (no flaky tests)
Checklist questions:
- [ ] Does the test suite fail if your new logic is broken?
- [ ] Are all expected behaviors exercised (happy path, invalid input, etc)?
- [ ] Do tests use fixtures or mocks where needed?
```python
def test_filter_unknown_users():
"""Test filtering unknown users from a list."""
users = ["alice", "bob", "charlie"]
known_users = {"alice", "bob"}
result = filter_unknown_users(users, known_users)
assert result == ["charlie"]
assert len(result) == 1
```
### 4. Security and Risk Assessment
**Security Checklist:**
- No `eval()`, `exec()`, or `pickle` on user-controlled input
- Proper exception handling (no bare `except:`) and use a `msg` variable for error messages
- Remove unreachable/commented code before committing
- Race conditions or resource leaks (file handles, sockets, threads).
- Ensure proper resource cleanup (file handles, connections)
**Bad:**
```python
def load_config(path):
with open(path) as f:
return eval(f.read()) # ⚠️ Never eval config
```
**Good:**
```python
import json
def load_config(path: str) -> dict:
with open(path) as f:
return json.load(f)
```
### 5. Documentation Standards
**Use Google-style docstrings with Args and Returns sections for all public functions.**
**Insufficient Documentation:**
```python
def send_email(to, msg):
"""Send an email to a recipient."""
```
**Complete Documentation:**
```python
def send_email(to: str, msg: str, *, priority: str = "normal") -> bool:
"""
Send an email to a recipient with specified priority.
Args:
to: The email address of the recipient.
msg: The message body to send.
priority: Email priority level.
Returns:
True if email was sent successfully, False otherwise.
Raises:
InvalidEmailError: If the email address format is invalid.
SMTPConnectionError: If unable to connect to email server.
"""
```
**Documentation Guidelines:**
- Types go in function signatures, NOT in docstrings
- Focus on "why" rather than "what" in descriptions
- Document all parameters, return values, and exceptions
- Keep descriptions concise but clear
📌 *Tip:* Keep descriptions concise but clear. Only document return values if non-obvious.
### 6. Architectural Improvements
**When you encounter code that could be improved, suggest better designs:**
**Poor Design:**
```python
def process_data(data, db_conn, email_client, logger):
# Function doing too many things
validated = validate_data(data)
result = db_conn.save(validated)
email_client.send_notification(result)
logger.log(f"Processed {len(data)} items")
return result
```
**Better Design:**
```python
@dataclass
class ProcessingResult:
"""Result of data processing operation."""
items_processed: int
success: bool
errors: List[str] = field(default_factory=list)
class DataProcessor:
"""Handles data validation, storage, and notification."""
def __init__(self, db_conn: Database, email_client: EmailClient):
self.db = db_conn
self.email = email_client
def process(self, data: List[dict]) -> ProcessingResult:
"""Process and store data with notifications.
Args:
data: List of data items to process.
Returns:
ProcessingResult with details of the operation.
"""
validated = self._validate_data(data)
result = self.db.save(validated)
self._notify_completion(result)
return result
```
**Design Improvement Areas:**
If there's a **cleaner**, **more scalable**, or **simpler** design, highlight it and suggest improvements that would:
- Reduce code duplication through shared utilities
- Make unit testing easier
- Improve separation of concerns (single responsibility)
- Make unit testing easier through dependency injection
- Add clarity without adding complexity
- Prefer dataclasses for structured data
## Development Tools & Commands
### Package Management
```bash
# Add package
uv add package-name
# Sync project dependencies
uv sync
uv lock
```
### Testing
```bash
# Run unit tests (no network)
make test
# Don't run integration tests, as API keys must be set
# Run specific test file
uv run --group test pytest tests/unit_tests/test_specific.py
```
### Code Quality
```bash
# Lint code
make lint
# Format code
make format
# Type checking
uv run --group lint mypy .
```
### Dependency Management Patterns
**Local Development Dependencies:**
```toml
[tool.uv.sources]
langchain-core = { path = "../core", editable = true }
langchain-tests = { path = "../standard-tests", editable = true }
```
**For tools, use the `@tool` decorator from `langchain_core.tools`:**
```python
from langchain_core.tools import tool
@tool
def search_database(query: str) -> str:
"""Search the database for relevant information.
Args:
query: The search query string.
"""
# Implementation here
return results
```
## Commit Standards
**Use Conventional Commits format for PR titles:**
- `feat(core): add multi-tenant support`
- `!fix(cli): resolve flag parsing error` (breaking change uses exclamation mark)
- `docs: update API usage examples`
- `docs(openai): update API usage examples`
## Framework-Specific Guidelines
- Follow the existing patterns in `langchain_core` for base abstractions
- Implement proper streaming support where applicable
- Avoid deprecated components
### Partner Integrations
- Follow the established patterns in existing partner libraries
- Implement standard interfaces (`BaseChatModel`, `BaseEmbeddings`, etc.)
- Include comprehensive integration tests
- Document API key requirements and authentication
---
## Quick Reference Checklist
Before submitting code changes:
- [ ] **Breaking Changes**: Verified no public API changes
- [ ] **Type Hints**: All functions have complete type annotations
- [ ] **Tests**: New functionality is fully tested
- [ ] **Security**: No dangerous patterns (eval, silent failures, etc.)
- [ ] **Documentation**: Google-style docstrings for public functions
- [ ] **Code Quality**: `make lint` and `make format` pass
- [ ] **Architecture**: Suggested improvements where applicable
- [ ] **Commit Message**: Follows Conventional Commits format

View File

@@ -118,17 +118,6 @@ xai:
- any-glob-to-any-file:
- "libs/partners/xai/**/*"
# Infrastructure and DevOps
infra:
- changed-files:
- any-glob-to-any-file:
- ".github/**/*"
- "Makefile"
- ".pre-commit-config.yaml"
- "scripts/**/*"
- "docker/**/*"
- "Dockerfile*"
github_actions:
- changed-files:
- any-glob-to-any-file:
@@ -142,22 +131,3 @@ dependencies:
- "uv.lock"
- "**/requirements*.txt"
- "**/poetry.lock"
# Documentation
documentation:
- changed-files:
- any-glob-to-any-file:
- "**/*.md"
- "**/*.rst"
- "**/README*"
# Security related changes
security:
- changed-files:
- any-glob-to-any-file:
- "**/*security*"
- "**/*auth*"
- "**/*credential*"
- "**/*secret*"
- "**/*token*"
- ".github/workflows/security*"

View File

@@ -47,6 +47,12 @@ jobs:
cache-suffix: lint-${{ inputs.working-directory }}
working-directory: ${{ inputs.working-directory }}
# - name: "🔒 Verify Lockfile is Up-to-Date"
# working-directory: ${{ inputs.working-directory }}
# run: |
# unset UV_FROZEN
# uv lock --check
- name: "📦 Install Lint & Typing Dependencies"
working-directory: ${{ inputs.working-directory }}
run: |

View File

@@ -19,7 +19,7 @@ on:
required: true
type: string
description: "From which folder this pipeline executes"
default: "libs/langchain"
default: "libs/langchain_v1"
release-version:
required: true
type: string
@@ -77,7 +77,7 @@ jobs:
working-directory: ${{ inputs.working-directory }}
- name: Upload build
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: dist
path: ${{ inputs.working-directory }}/dist/
@@ -208,7 +208,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v7
with:
name: dist
path: ${{ inputs.working-directory }}/dist/
@@ -258,7 +258,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v7
with:
name: dist
path: ${{ inputs.working-directory }}/dist/
@@ -394,9 +394,10 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
if: false # temporarily skip
strategy:
matrix:
partner: [anthropic]
partner: [openai, anthropic]
fail-fast: false # Continue testing other partners if one fails
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
@@ -430,7 +431,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v7
if: startsWith(inputs.working-directory, 'libs/core')
with:
name: dist
@@ -470,6 +471,67 @@ jobs:
uv pip install ../../core/dist/*.whl
make integration_tests
# Test external packages that depend on langchain-core/langchain against the new release
# Only runs for core and langchain_v1 releases to catch breaking changes before publish
test-dependents:
name: "🐍 Python ${{ matrix.python-version }}: ${{ matrix.package.path }}"
needs:
- build
- release-notes
- test-pypi-publish
- pre-release-checks
runs-on: ubuntu-latest
permissions:
contents: read
# Only run for core or langchain_v1 releases
if: startsWith(inputs.working-directory, 'libs/core') || startsWith(inputs.working-directory, 'libs/langchain_v1')
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.13"]
package:
- name: deepagents
repo: langchain-ai/deepagents
path: libs/deepagents
# No API keys needed for now - deepagents `make test` only runs unit tests
steps:
- uses: actions/checkout@v6
with:
path: langchain
- uses: actions/checkout@v6
with:
repository: ${{ matrix.package.repo }}
path: ${{ matrix.package.name }}
- name: Set up Python + uv
uses: "./langchain/.github/actions/uv_setup"
with:
python-version: ${{ matrix.python-version }}
- uses: actions/download-artifact@v7
with:
name: dist
path: dist/
- name: Install ${{ matrix.package.name }} with local packages
# External dependents don't have [tool.uv.sources] pointing to this repo,
# so we install the package normally then override with the built wheel.
run: |
cd ${{ matrix.package.name }}/${{ matrix.package.path }}
# Install the package with test dependencies
uv sync --group test
# Override with the built wheel from this release
uv pip install $GITHUB_WORKSPACE/dist/*.whl
- name: Run ${{ matrix.package.name }} tests
run: |
cd ${{ matrix.package.name }}/${{ matrix.package.path }}
make test
publish:
# Publishes the package to PyPI
needs:
@@ -477,7 +539,10 @@ jobs:
- release-notes
- test-pypi-publish
- pre-release-checks
- test-prior-published-packages-against-new-core
- test-dependents
# - test-prior-published-packages-against-new-core
# Run if all needed jobs succeeded or were skipped (test-dependents only runs for core/langchain_v1)
if: ${{ !cancelled() && !failure() }}
runs-on: ubuntu-latest
permissions:
# This permission is used for trusted publishing:
@@ -499,7 +564,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v7
with:
name: dist
path: ${{ inputs.working-directory }}/dist/
@@ -539,7 +604,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@v6
- uses: actions/download-artifact@v7
with:
name: dist
path: ${{ inputs.working-directory }}/dist/

View File

@@ -17,8 +17,8 @@ jobs:
script: |
const body = context.payload.issue.body || "";
// Extract text under "### Package"
const match = body.match(/### Package\s+([\s\S]*?)\n###/i);
// Extract text under "### Package" (handles " (Required)" suffix and being last section)
const match = body.match(/### Package[^\n]*\n([\s\S]*?)(?:\n###|$)/i);
if (!match) return;
const packageSection = match[1].trim();

View File

@@ -1,8 +1,8 @@
# Routine integration tests against partner libraries with live API credentials.
#
# Uses `make integration_tests` for each library in the matrix.
# Uses `make integration_tests` within each library being tested.
#
# Runs daily. Can also be triggered manually for immediate updates.
# Runs daily with the option to trigger manually.
name: "⏰ Integration Tests"
run-name: "Run Integration Tests - ${{ inputs.working-directory-force || 'all libs' }} (Python ${{ inputs.python-version-force || '3.10, 3.13' }})"
@@ -24,17 +24,29 @@ permissions:
env:
UV_FROZEN: "true"
DEFAULT_LIBS: '["libs/partners/openai", "libs/partners/anthropic", "libs/partners/fireworks", "libs/partners/groq", "libs/partners/mistralai", "libs/partners/xai", "libs/partners/google-vertexai", "libs/partners/google-genai", "libs/partners/aws"]'
DEFAULT_LIBS: >-
["libs/partners/openai",
"libs/partners/anthropic",
"libs/partners/fireworks",
"libs/partners/groq",
"libs/partners/mistralai",
"libs/partners/xai",
"libs/partners/google-vertexai",
"libs/partners/google-genai",
"libs/partners/aws"]
jobs:
# Generate dynamic test matrix based on input parameters or defaults
# Only runs on the main repo (for scheduled runs) or when manually triggered
compute-matrix:
# Defend against forks running scheduled jobs, but allow manual runs from forks
if: github.repository_owner == 'langchain-ai' || github.event_name != 'schedule'
runs-on: ubuntu-latest
name: "📋 Compute Test Matrix"
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
python-version-min-3-11: ${{ steps.set-matrix.outputs.python-version-min-3-11 }}
steps:
- name: "🔢 Generate Python & Library Matrix"
id: set-matrix
@@ -47,9 +59,16 @@ jobs:
# python-version should default to 3.10 and 3.13, but is overridden to [PYTHON_VERSION_FORCE] if set
# working-directory should default to DEFAULT_LIBS, but is overridden to [WORKING_DIRECTORY_FORCE] if set
python_version='["3.10", "3.13"]'
python_version_min_3_11='["3.11", "3.13"]'
working_directory="$DEFAULT_LIBS"
if [ -n "$PYTHON_VERSION_FORCE" ]; then
python_version="[\"$PYTHON_VERSION_FORCE\"]"
# Bound forced version to >= 3.11 for packages requiring it
if [ "$(echo "$PYTHON_VERSION_FORCE >= 3.11" | bc -l)" -eq 1 ]; then
python_version_min_3_11="[\"$PYTHON_VERSION_FORCE\"]"
else
python_version_min_3_11='["3.11"]'
fi
fi
if [ -n "$WORKING_DIRECTORY_FORCE" ]; then
working_directory="[\"$WORKING_DIRECTORY_FORCE\"]"
@@ -57,8 +76,10 @@ jobs:
matrix="{\"python-version\": $python_version, \"working-directory\": $working_directory}"
echo $matrix
echo "matrix=$matrix" >> $GITHUB_OUTPUT
echo "python-version-min-3-11=$python_version_min_3_11" >> $GITHUB_OUTPUT
# Run integration tests against partner libraries with live API credentials
build:
integration-tests:
if: github.repository_owner == 'langchain-ai' || github.event_name != 'schedule'
name: "🐍 Python ${{ matrix.python-version }}: ${{ matrix.working-directory }}"
runs-on: ubuntu-latest
@@ -74,15 +95,27 @@ jobs:
- uses: actions/checkout@v6
with:
path: langchain
# These libraries exist outside of the monorepo and need to be checked out separately
- uses: actions/checkout@v6
with:
repository: langchain-ai/langchain-google
path: langchain-google
- name: "🔐 Authenticate to Google Cloud"
id: "auth"
uses: google-github-actions/auth@v3
with:
credentials_json: "${{ secrets.GOOGLE_CREDENTIALS }}"
- uses: actions/checkout@v6
with:
repository: langchain-ai/langchain-aws
path: langchain-aws
- name: "🔐 Configure AWS Credentials"
uses: aws-actions/configure-aws-credentials@v5
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: "📦 Organize External Libraries"
run: |
rm -rf \
@@ -97,27 +130,27 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: "🔐 Authenticate to Google Cloud"
id: "auth"
uses: google-github-actions/auth@v3
with:
credentials_json: "${{ secrets.GOOGLE_CREDENTIALS }}"
- name: "🔐 Configure AWS Credentials"
uses: aws-actions/configure-aws-credentials@v5
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: "📦 Install Dependencies"
# Partner packages use [tool.uv.sources] in their pyproject.toml to resolve
# langchain-core/langchain to local editable installs, so `uv sync` automatically
# tests against the versions from the current branch (not published releases).
# TODO: external google/aws don't have local resolution since they live in
# separate repos, so they pull `core`/`langchain_v1` from PyPI. We should update
# their dev groups to use git source dependencies pointing to the current
# branch's latest commit SHA to fully test against local langchain changes.
run: |
echo "Running scheduled tests, installing dependencies with uv..."
cd langchain/${{ matrix.working-directory }}
uv sync --group test --group test_integration
- name: "🚀 Run Integration Tests"
# WARNING: All secrets below are available to every matrix job regardless of
# which package is being tested. This is intentional for simplicity, but means
# any test file could technically access any key. Only use for trusted code.
env:
LANGCHAIN_TESTS_USER_AGENT: ${{ secrets.LANGCHAIN_TESTS_USER_AGENT }}
AI21_API_KEY: ${{ secrets.AI21_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_FILES_API_IMAGE_ID: ${{ secrets.ANTHROPIC_FILES_API_IMAGE_ID }}
@@ -155,7 +188,6 @@ jobs:
WATSONX_APIKEY: ${{ secrets.WATSONX_APIKEY }}
WATSONX_PROJECT_ID: ${{ secrets.WATSONX_PROJECT_ID }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
LANGCHAIN_TESTS_USER_AGENT: ${{ secrets.LANGCHAIN_TESTS_USER_AGENT }}
run: |
cd langchain/${{ matrix.working-directory }}
make integration_tests
@@ -179,3 +211,59 @@ jobs:
# grep will exit non-zero if the target message isn't found,
# and `set -e` above will cause the step to fail.
echo "$STATUS" | grep 'nothing to commit, working tree clean'
# Test dependent packages against local packages to catch breaking changes
test-dependents:
# Defend against forks running scheduled jobs, but allow manual runs from forks
if: github.repository_owner == 'langchain-ai' || github.event_name != 'schedule'
name: "🐍 Python ${{ matrix.python-version }}: ${{ matrix.package.path }}"
runs-on: ubuntu-latest
needs: [compute-matrix]
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
# deepagents requires Python >= 3.11, use bounded version from compute-matrix
python-version: ${{ fromJSON(needs.compute-matrix.outputs.python-version-min-3-11) }}
package:
- name: deepagents
repo: langchain-ai/deepagents
path: libs/deepagents
steps:
- uses: actions/checkout@v6
with:
path: langchain
- uses: actions/checkout@v6
with:
repository: ${{ matrix.package.repo }}
path: ${{ matrix.package.name }}
- name: "🐍 Set up Python ${{ matrix.python-version }} + UV"
uses: "./langchain/.github/actions/uv_setup"
with:
python-version: ${{ matrix.python-version }}
- name: "📦 Install ${{ matrix.package.name }} with Local"
# Unlike partner packages (which use [tool.uv.sources] for local resolution),
# external dependents live in separate repos and need explicit overrides to
# test against the langchain versions from the current branch, as their
# pyproject.toml files point to released versions.
run: |
cd ${{ matrix.package.name }}/${{ matrix.package.path }}
# Install the package with test dependencies
uv sync --group test
# Override langchain packages with local versions
uv pip install \
-e $GITHUB_WORKSPACE/langchain/libs/core \
-e $GITHUB_WORKSPACE/langchain/libs/langchain_v1
# No API keys needed for now - deepagents `make test` only runs unit tests
- name: "🚀 Run ${{ matrix.package.name }} Tests"
run: |
cd ${{ matrix.package.name }}/${{ matrix.package.path }}
make test

View File

@@ -8,7 +8,7 @@ on:
# Safe since we're not checking out or running the PR's code
# Never check out the PR's head in a pull_request_target job
pull_request_target:
types: [opened, synchronize, reopened, edited]
types: [opened, synchronize, reopened]
jobs:
labeler:

View File

@@ -27,12 +27,18 @@
# * release — prepare a new release
#
# Allowed Scope(s) (optional):
# core, cli, langchain, langchain_v1, langchain-classic, model-profiles,
# core, cli, langchain, langchain-classic, model-profiles,
# standard-tests, text-splitters, docs, anthropic, chroma, deepseek, exa,
# fireworks, groq, huggingface, mistralai, nomic, ollama, openai,
# perplexity, prompty, qdrant, xai, infra, deps
#
# Multiple scopes can be used by separating them with a comma.
# Multiple scopes can be used by separating them with a comma. For example:
#
# feat(core,cli): add multitenant support to core and cli
#
# Note: PRs touching the langchain package should use the 'langchain' scope. It is not
# acceptable to omit the scope for changes to the langchain package, despite it being
# the main package & name of the repo.
#
# Rules:
# 1. The 'Type' must start with a lowercase letter.

View File

@@ -0,0 +1,148 @@
# Automatically tag issues and pull requests as "external" or "internal"
# based on whether the author is a member of the langchain-ai
# GitHub organization.
#
# Setup Requirements:
# 1. Create a GitHub App with permissions:
# - Repository: Issues (write), Pull requests (write)
# - Organization: Members (read)
# 2. Install the app on your organization and this repository
# 3. Add these repository secrets:
# - ORG_MEMBERSHIP_APP_ID: Your app's ID
# - ORG_MEMBERSHIP_APP_PRIVATE_KEY: Your app's private key
#
# The GitHub App token is required to check private organization membership.
# Without it, the workflow will fail.
name: Tag External Contributions
on:
issues:
types: [opened]
pull_request_target:
types: [opened]
jobs:
tag-external:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.ORG_MEMBERSHIP_APP_ID }}
private-key: ${{ secrets.ORG_MEMBERSHIP_APP_PRIVATE_KEY }}
- name: Check if contributor is external
id: check-membership
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const { owner, repo } = context.repo;
const author = context.payload.sender.login;
try {
// Check if the author is a member of the langchain-ai organization
// This requires org:read permissions to see private memberships
const membership = await github.rest.orgs.getMembershipForUser({
org: 'langchain-ai',
username: author
});
// Check if membership is active (not just pending invitation)
if (membership.data.state === 'active') {
console.log(`User ${author} is an active member of langchain-ai organization`);
core.setOutput('is-external', 'false');
} else {
console.log(`User ${author} has pending membership in langchain-ai organization`);
core.setOutput('is-external', 'true');
}
} catch (error) {
if (error.status === 404) {
console.log(`User ${author} is not a member of langchain-ai organization`);
core.setOutput('is-external', 'true');
} else {
console.error('Error checking membership:', error);
console.log('Status:', error.status);
console.log('Message:', error.message);
// If we can't determine membership due to API error, assume external for safety
core.setOutput('is-external', 'true');
}
}
- name: Add external label to issue
if: steps.check-membership.outputs.is-external == 'true' && github.event_name == 'issues'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
const issue_number = context.payload.issue.number;
await github.rest.issues.addLabels({
owner,
repo,
issue_number,
labels: ['external']
});
console.log(`Added 'external' label to issue #${issue_number}`);
- name: Add external label to pull request
if: steps.check-membership.outputs.is-external == 'true' && github.event_name == 'pull_request_target'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
const pull_number = context.payload.pull_request.number;
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pull_number,
labels: ['external']
});
console.log(`Added 'external' label to pull request #${pull_number}`);
- name: Add internal label to issue
if: steps.check-membership.outputs.is-external == 'false' && github.event_name == 'issues'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
const issue_number = context.payload.issue.number;
await github.rest.issues.addLabels({
owner,
repo,
issue_number,
labels: ['internal']
});
console.log(`Added 'internal' label to issue #${issue_number}`);
- name: Add internal label to pull request
if: steps.check-membership.outputs.is-external == 'false' && github.event_name == 'pull_request_target'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
const pull_number = context.payload.pull_request.number;
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pull_number,
labels: ['internal']
});
console.log(`Added 'internal' label to pull request #${pull_number}`);

View File

@@ -1,8 +0,0 @@
With the deprecation of v0 docs, the following files will need to be migrated/supported
in the new docs repo:
- run_notebooks.yml: New repo should run Integration tests on code snippets?
- people.yml: Need to fix and somehow display on the new docs site
- Subsequently, `.github/actions/people/`
- _test_doc_imports.yml
- check-broken-links.yml

View File

@@ -1,4 +1,24 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: no-commit-to-branch # prevent direct commits to protected branches
args: ["--branch", "master"]
- id: check-yaml # validate YAML syntax
args: ["--unsafe"] # allow custom tags
- id: check-toml # validate TOML syntax
- id: end-of-file-fixer # ensure files end with a newline
- id: trailing-whitespace # remove trailing whitespace from lines
exclude: \.ambr$
# Text normalization hooks for consistent formatting
- repo: https://github.com/sirosen/texthooks
rev: 0.6.8
hooks:
- id: fix-smartquotes # replace curly quotes with straight quotes
- id: fix-spaces # replace non-standard spaces (e.g., non-breaking) with regular spaces
# Per-package format and lint hooks for the monorepo
- repo: local
hooks:
- id: core
@@ -97,3 +117,15 @@ repos:
entry: make -C libs/partners/qdrant format lint
files: ^libs/partners/qdrant/
pass_filenames: false
- id: core-version
name: check core version consistency
language: system
entry: make -C libs/core check_version
files: ^libs/core/(pyproject\.toml|langchain_core/version\.py)$
pass_filenames: false
- id: langchain-v1-version
name: check langchain version consistency
language: system
entry: make -C libs/langchain_v1 check_version
files: ^libs/langchain_v1/(pyproject\.toml|langchain/__init__\.py)$
pass_filenames: false

View File

@@ -6,8 +6,6 @@
"ms-toolsai.jupyter",
"ms-toolsai.jupyter-keymap",
"ms-toolsai.jupyter-renderers",
"ms-toolsai.vscode-jupyter-cell-tags",
"ms-toolsai.vscode-jupyter-slideshow",
"yzhang.markdown-all-in-one",
"davidanson.vscode-markdownlint",
"bierner.markdown-mermaid",

View File

@@ -72,7 +72,15 @@ uv run --group lint mypy .
#### Commit standards
Suggest PR titles that follow Conventional Commits format. Refer to .github/workflows/pr_lint for allowed types and scopes.
Suggest PR titles that follow Conventional Commits format. Refer to .github/workflows/pr_lint for allowed types and scopes. Note that all commit/PR titles should be in lowercase with the exception of proper nouns/named entities. All PR titles should include a scope with no exceptions. For example:
```txt
feat(langchain): add new chat completion feature
fix(core): resolve type hinting issue in vector store
chore(anthropic): update infrastructure dependencies
```
Note how `feat(langchain)` includes a scope even though it is the main package and name of the repo.
#### Pull request guidelines
@@ -85,6 +93,7 @@ Suggest PR titles that follow Conventional Commits format. Refer to .github/work
### Maintain stable public interfaces
CRITICAL: Always attempt to preserve function signatures, argument positions, and names for exported/public methods. Do not make breaking changes.
You should warn the developer for any function signature changes, regardless of whether they look breaking or not.
**Before making ANY changes to public APIs:**

View File

@@ -72,7 +72,15 @@ uv run --group lint mypy .
#### Commit standards
Suggest PR titles that follow Conventional Commits format. Refer to .github/workflows/pr_lint for allowed types and scopes.
Suggest PR titles that follow Conventional Commits format. Refer to .github/workflows/pr_lint for allowed types and scopes. Note that all commit/PR titles should be in lowercase with the exception of proper nouns/named entities. All PR titles should include a scope with no exceptions. For example:
```txt
feat(langchain): add new chat completion feature
fix(core): resolve type hinting issue in vector store
chore(anthropic): update infrastructure dependencies
```
Note how `feat(langchain)` includes a scope even though it is the main package and name of the repo.
#### Pull request guidelines
@@ -85,6 +93,7 @@ Suggest PR titles that follow Conventional Commits format. Refer to .github/work
### Maintain stable public interfaces
CRITICAL: Always attempt to preserve function signatures, argument positions, and names for exported/public methods. Do not make breaking changes.
You should warn the developer for any function signature changes, regardless of whether they look breaking or not.
**Before making ANY changes to public APIs:**

View File

@@ -19,7 +19,7 @@
<a href="https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/langchain-ai/langchain" target="_blank"><img src="https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode" alt="Open in Dev Containers"></a>
<a href="https://codespaces.new/langchain-ai/langchain" target="_blank"><img src="https://github.com/codespaces/badge.svg" alt="Open in Github Codespace" title="Open in Github Codespace" width="150" height="20"></a>
<a href="https://codspeed.io/langchain-ai/langchain" target="_blank"><img src="https://img.shields.io/endpoint?url=https://codspeed.io/badge.json" alt="CodSpeed Badge"></a>
<a href="https://twitter.com/langchainai" target="_blank"><img src="https://img.shields.io/twitter/url/https/twitter.com/langchainai.svg?style=social&label=Follow%20%40LangChainAI" alt="Twitter / X"></a>
<a href="https://x.com/langchain" target="_blank"><img src="https://img.shields.io/twitter/url/https/twitter.com/langchain.svg?style=social&label=Follow%20%40LangChain" alt="Twitter / X"></a>
</div>
LangChain is a framework for building agents and LLM-powered applications. It helps you chain together interoperable components and third-party integrations to simplify AI application development all while future-proofing decisions as the underlying technology evolves.
@@ -71,4 +71,5 @@ To improve your LLM application development, pair LangChain with:
- [API Reference](https://reference.langchain.com/python) Detailed reference on navigating base packages and integrations for LangChain.
- [Contributing Guide](https://docs.langchain.com/oss/python/contributing/overview) Learn how to contribute to LangChain projects and find good first issues.
- [Code of Conduct](https://github.com/langchain-ai/langchain/blob/master/.github/CODE_OF_CONDUCT.md) Our community guidelines and standards for participation.
- [Code of Conduct](https://github.com/langchain-ai/langchain/?tab=coc-ov-file) Our community guidelines and standards for participation.
- [LangChain Academy](https://academy.langchain.com/) Comprehensive, free courses on LangChain libraries and products, made by the LangChain team.

View File

@@ -1,80 +0,0 @@
# Security Policy
LangChain has a large ecosystem of integrations with various external resources like local and remote file systems, APIs and databases. These integrations allow developers to create versatile applications that combine the power of LLMs with the ability to access, interact with and manipulate external resources.
## Best practices
When building such applications, developers should remember to follow good security practices:
* [**Limit Permissions**](https://en.wikipedia.org/wiki/Principle_of_least_privilege): Scope permissions specifically to the application's need. Granting broad or excessive permissions can introduce significant security vulnerabilities. To avoid such vulnerabilities, consider using read-only credentials, disallowing access to sensitive resources, using sandboxing techniques (such as running inside a container), specifying proxy configurations to control external requests, etc., as appropriate for your application.
* **Anticipate Potential Misuse**: Just as humans can err, so can Large Language Models (LLMs). Always assume that any system access or credentials may be used in any way allowed by the permissions they are assigned. For example, if a pair of database credentials allows deleting data, it's safest to assume that any LLM able to use those credentials may in fact delete data.
* [**Defense in Depth**](https://en.wikipedia.org/wiki/Defense_in_depth_(computing)): No security technique is perfect. Fine-tuning and good chain design can reduce, but not eliminate, the odds that a Large Language Model (LLM) may make a mistake. It's best to combine multiple layered security approaches rather than relying on any single layer of defense to ensure security. For example: use both read-only permissions and sandboxing to ensure that LLMs are only able to access data that is explicitly meant for them to use.
Risks of not doing so include, but are not limited to:
* Data corruption or loss.
* Unauthorized access to confidential information.
* Compromised performance or availability of critical resources.
Example scenarios with mitigation strategies:
* A user may ask an agent with access to the file system to delete files that should not be deleted or read the content of files that contain sensitive information. To mitigate, limit the agent to only use a specific directory and only allow it to read or write files that are safe to read or write. Consider further sandboxing the agent by running it in a container.
* A user may ask an agent with write access to an external API to write malicious data to the API, or delete data from that API. To mitigate, give the agent read-only API keys, or limit it to only use endpoints that are already resistant to such misuse.
* A user may ask an agent with access to a database to drop a table or mutate the schema. To mitigate, scope the credentials to only the tables that the agent needs to access and consider issuing READ-ONLY credentials.
If you're building applications that access external resources like file systems, APIs or databases, consider speaking with your company's security team to determine how to best design and secure your applications.
## Reporting OSS Vulnerabilities
LangChain is partnered with [huntr by Protect AI](https://huntr.com/) to provide
a bounty program for our open source projects.
Please report security vulnerabilities associated with the LangChain
open source projects at [huntr](https://huntr.com/bounties/disclose/?target=https%3A%2F%2Fgithub.com%2Flangchain-ai%2Flangchain&validSearch=true).
Before reporting a vulnerability, please review:
1) In-Scope Targets and Out-of-Scope Targets below.
2) The [langchain-ai/langchain](https://docs.langchain.com/oss/python/contributing/code#repository-structure) monorepo structure.
3) The [Best Practices](#best-practices) above to understand what we consider to be a security vulnerability vs. developer responsibility.
### In-Scope Targets
The following packages and repositories are eligible for bug bounties:
* langchain-core
* langchain (see exceptions)
* langchain-community (see exceptions)
* langgraph
* langserve
### Out of Scope Targets
All out of scope targets defined by huntr as well as:
* **langchain-experimental**: This repository is for experimental code and is not
eligible for bug bounties (see [package warning](https://pypi.org/project/langchain-experimental/)), bug reports to it will be marked as interesting or waste of
time and published with no bounty attached.
* **tools**: Tools in either `langchain` or `langchain-community` are not eligible for bug
bounties. This includes the following directories
* `libs/langchain/langchain/tools`
* `libs/community/langchain_community/tools`
* Please review the [Best Practices](#best-practices)
for more details, but generally tools interact with the real world. Developers are
expected to understand the security implications of their code and are responsible
for the security of their tools.
* Code documented with security notices. This will be decided on a case-by-case basis, but likely will not be eligible for a bounty as the code is already
documented with guidelines for developers that should be followed for making their
application secure.
* Any LangSmith related repositories or APIs (see [Reporting LangSmith Vulnerabilities](#reporting-langsmith-vulnerabilities)).
## Reporting LangSmith Vulnerabilities
Please report security vulnerabilities associated with LangSmith by email to `security@langchain.dev`.
* LangSmith site: [https://smith.langchain.com](https://smith.langchain.com)
* SDK client: [https://github.com/langchain-ai/langsmith-sdk](https://github.com/langchain-ai/langsmith-sdk)
### Other Security Concerns
For any other security concerns, please contact us at `security@langchain.dev`.

20
libs/Makefile Normal file
View File

@@ -0,0 +1,20 @@
# Makefile for libs/ directory
# Contains targets that operate across multiple packages
LANGCHAIN_DIRS = core text-splitters langchain langchain_v1 model-profiles
.PHONY: lock check-lock
# Regenerate lockfiles for all core packages
lock:
@for dir in $(LANGCHAIN_DIRS); do \
echo "=== Locking $$dir ==="; \
(cd $$dir && uv lock); \
done
# Verify all lockfiles are up-to-date
check-lock:
@for dir in $(LANGCHAIN_DIRS); do \
echo "=== Checking $$dir ==="; \
(cd $$dir && uv lock --check) || exit 1; \
done

View File

@@ -3,7 +3,7 @@
[![PyPI - Version](https://img.shields.io/pypi/v/langchain-cli?label=%20)](https://pypi.org/project/langchain-cli/#history)
[![PyPI - License](https://img.shields.io/pypi/l/langchain-cli)](https://opensource.org/licenses/MIT)
[![PyPI - Downloads](https://img.shields.io/pepy/dt/langchain-cli)](https://pypistats.org/packages/langchain-cli)
[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/langchainai.svg?style=social&label=Follow%20%40LangChainAI)](https://twitter.com/langchainai)
[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/langchain.svg?style=social&label=Follow%20%40LangChain)](https://x.com/langchain)
## Quick Install

View File

@@ -36,6 +36,9 @@ dev-dependencies = [
[tool.ruff.lint]
select = ["E", "F", "I", "T201"]
[tool.ruff.lint.flake8-tidy-imports]
ban-relative-imports = "all"
[tool.ruff.lint.per-file-ignores]
"docs/**" = [ "ALL",]

View File

@@ -24,7 +24,7 @@ Homepage = "https://docs.langchain.com/"
Documentation = "https://docs.langchain.com/"
Source = "https://github.com/langchain-ai/langchain/tree/master/libs/cli"
Changelog = "https://github.com/langchain-ai/langchain/releases?q=%22langchain-cli%3D%3D1%22"
Twitter = "https://x.com/LangChainAI"
Twitter = "https://x.com/LangChain"
Slack = "https://www.langchain.com/join-community"
Reddit = "https://www.reddit.com/r/LangChain/"
@@ -38,14 +38,16 @@ dev = [
"pytest-watcher>=0.3.4,<1.0.0"
]
lint = [
"ruff>=0.13.1,<0.14",
"mypy>=1.18.1,<1.19"
"ruff>=0.14.11,<0.15.0"
]
test = [
"langchain-core",
"langchain-classic"
]
typing = ["langchain-classic"]
typing = [
"mypy>=1.19.1,<1.20",
"langchain-classic"
]
test_integration = []
[tool.uv.sources]
@@ -64,10 +66,6 @@ ignore = [
"FIX002", # Line contains TODO
"PERF203", # Rarely useful
"PLR09", # Too many something (arg, statements, etc)
"RUF012", # Doesn't play well with Pydantic
"TC001", # Doesn't play well with Pydantic
"TC002", # Doesn't play well with Pydantic
"TC003", # Doesn't play well with Pydantic
"TD002", # Missing author in TODO
"TD003", # Missing issue link in TODO
@@ -76,7 +74,6 @@ ignore = [
]
unfixable = [
"B028", # People should intentionally tune the stacklevel
"PLW1510", # People should intentionally set the check argument
]
flake8-annotations.allow-star-arg-any = true
@@ -89,6 +86,9 @@ pyupgrade.keep-runtime-typing = true
convention = "google"
ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters
[tool.ruff.lint.flake8-tidy-imports]
ban-relative-imports = "all"
[tool.ruff.lint.per-file-ignores]
"tests/**" = [ "D1", "S", "SLF",]
"scripts/**" = [ "INP", "S",]

View File

@@ -4,8 +4,8 @@ from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .file import File
from .folder import Folder
from tests.unit_tests.migrate.cli_runner.file import File
from tests.unit_tests.migrate.cli_runner.folder import Folder
@dataclass

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from .file import File
from tests.unit_tests.migrate.cli_runner.file import File
if TYPE_CHECKING:
from pathlib import Path

308
libs/cli/uv.lock generated
View File

@@ -2,6 +2,15 @@ version = 1
revision = 3
requires-python = ">=3.10.0, <4.0.0"
[[package]]
name = "annotated-doc"
version = "0.0.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
]
[[package]]
name = "annotated-types"
version = "0.7.0"
@@ -143,16 +152,17 @@ wheels = [
[[package]]
name = "fastapi"
version = "0.118.0"
version = "0.128.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/28/3c/2b9345a6504e4055eaa490e0b41c10e338ad61d9aeaae41d97807873cdf2/fastapi-0.118.0.tar.gz", hash = "sha256:5e81654d98c4d2f53790a7d32d25a7353b30c81441be7d0958a26b5d761fa1c8", size = 310536, upload-time = "2025-09-29T03:37:23.126Z" }
sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/54e2bdaad22ca91a59455251998d43094d5c3d3567c52c7c04774b3f43f2/fastapi-0.118.0-py3-none-any.whl", hash = "sha256:705137a61e2ef71019d2445b123aa8845bd97273c395b744d5a7dfe559056855", size = 97694, upload-time = "2025-09-29T03:37:21.338Z" },
{ url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" },
]
[[package]]
@@ -193,6 +203,8 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" },
{ url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" },
{ url = "https://files.pythonhosted.org/packages/a1/8d/88f3ebd2bc96bf7747093696f4335a0a8a4c5acfcf1b757717c0d2474ba3/greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", size = 1137126, upload-time = "2025-08-07T13:18:20.239Z" },
{ url = "https://files.pythonhosted.org/packages/f1/29/74242b7d72385e29bcc5563fba67dad94943d7cd03552bac320d597f29b2/greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7", size = 1544904, upload-time = "2025-11-04T12:42:04.763Z" },
{ url = "https://files.pythonhosted.org/packages/c8/e2/1572b8eeab0f77df5f6729d6ab6b141e4a84ee8eb9bc8c1e7918f94eda6d/greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8", size = 1611228, upload-time = "2025-11-04T12:42:08.423Z" },
{ url = "https://files.pythonhosted.org/packages/d6/6f/b60b0291d9623c496638c582297ead61f43c4b72eef5e9c926ef4565ec13/greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", size = 298654, upload-time = "2025-08-07T13:50:00.469Z" },
{ url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" },
{ url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" },
@@ -202,6 +214,8 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" },
{ url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" },
{ url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" },
{ url = "https://files.pythonhosted.org/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", size = 1548385, upload-time = "2025-11-04T12:42:11.067Z" },
{ url = "https://files.pythonhosted.org/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", size = 1613329, upload-time = "2025-11-04T12:42:12.928Z" },
{ url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" },
{ url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" },
{ url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" },
@@ -211,6 +225,8 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" },
{ url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" },
{ url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" },
{ url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" },
{ url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" },
{ url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" },
{ url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" },
{ url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" },
@@ -220,6 +236,8 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" },
{ url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" },
{ url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" },
{ url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" },
{ url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" },
{ url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" },
{ url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" },
{ url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" },
@@ -227,6 +245,8 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" },
{ url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" },
{ url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" },
{ url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" },
{ url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" },
{ url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" },
]
@@ -341,7 +361,7 @@ wheels = [
[[package]]
name = "langchain-classic"
version = "1.0.0"
version = "1.0.1"
source = { editable = "../langchain" }
dependencies = [
{ name = "async-timeout", marker = "python_full_version < '3.11'" },
@@ -387,15 +407,14 @@ dev = [
{ name = "langchain-core", editable = "../core" },
{ name = "langchain-text-splitters", editable = "../text-splitters" },
{ name = "playwright", specifier = ">=1.28.0,<2.0.0" },
{ name = "setuptools", specifier = ">=67.6.1,<68.0.0" },
{ name = "setuptools", specifier = ">=67.6.1,<79.0.0" },
]
lint = [
{ name = "cffi", marker = "python_full_version < '3.10'", specifier = "<1.17.1" },
{ name = "cffi", marker = "python_full_version >= '3.10'" },
{ name = "ruff", specifier = ">=0.13.1,<0.14.0" },
{ name = "ruff", specifier = ">=0.14.11,<0.15.0" },
]
test = [
{ name = "blockbuster", specifier = ">=1.5.18,<1.6.0" },
{ name = "cffi", marker = "python_full_version < '3.10'", specifier = "<1.17.1" },
{ name = "cffi", marker = "python_full_version >= '3.10'" },
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
@@ -435,7 +454,7 @@ typing = [
{ name = "fastapi", specifier = ">=0.116.1,<1.0.0" },
{ name = "langchain-core", editable = "../core" },
{ name = "langchain-text-splitters", editable = "../text-splitters" },
{ name = "mypy", specifier = ">=1.18.2,<1.19.0" },
{ name = "mypy", specifier = ">=1.19.1,<1.20.0" },
{ name = "mypy-protobuf", specifier = ">=3.0.0,<4.0.0" },
{ name = "numpy", marker = "python_full_version < '3.13'", specifier = ">=1.26.4" },
{ name = "numpy", marker = "python_full_version >= '3.13'", specifier = ">=2.1.0" },
@@ -466,7 +485,6 @@ dev = [
{ name = "pytest-watcher" },
]
lint = [
{ name = "mypy" },
{ name = "ruff" },
]
test = [
@@ -475,6 +493,7 @@ test = [
]
typing = [
{ name = "langchain-classic" },
{ name = "mypy" },
]
[package.metadata]
@@ -492,20 +511,20 @@ dev = [
{ name = "pytest", specifier = ">=7.4.2,<9.0.0" },
{ name = "pytest-watcher", specifier = ">=0.3.4,<1.0.0" },
]
lint = [
{ name = "mypy", specifier = ">=1.18.1,<1.19" },
{ name = "ruff", specifier = ">=0.13.1,<0.14" },
]
lint = [{ name = "ruff", specifier = ">=0.14.11,<0.15.0" }]
test = [
{ name = "langchain-classic", editable = "../langchain" },
{ name = "langchain-core", editable = "../core" },
]
test-integration = []
typing = [{ name = "langchain-classic", editable = "../langchain" }]
typing = [
{ name = "langchain-classic", editable = "../langchain" },
{ name = "mypy", specifier = ">=1.19.1,<1.20" },
]
[[package]]
name = "langchain-core"
version = "1.0.0"
version = "1.2.7"
source = { editable = "../core" }
dependencies = [
{ name = "jsonpatch" },
@@ -515,6 +534,7 @@ dependencies = [
{ name = "pyyaml" },
{ name = "tenacity" },
{ name = "typing-extensions" },
{ name = "uuid-utils" },
]
[package.metadata]
@@ -526,6 +546,7 @@ requires-dist = [
{ name = "pyyaml", specifier = ">=5.3.0,<7.0.0" },
{ name = "tenacity", specifier = ">=8.1.0,!=8.4.0,<10.0.0" },
{ name = "typing-extensions", specifier = ">=4.7.0,<5.0.0" },
{ name = "uuid-utils", specifier = ">=0.12.0,<1.0" },
]
[package.metadata.requires-dev]
@@ -534,7 +555,7 @@ dev = [
{ name = "jupyter", specifier = ">=1.0.0,<2.0.0" },
{ name = "setuptools", specifier = ">=67.6.1,<68.0.0" },
]
lint = [{ name = "ruff", specifier = ">=0.13.1,<0.14.0" }]
lint = [{ name = "ruff", specifier = ">=0.14.11,<0.15.0" }]
test = [
{ name = "blockbuster", specifier = ">=1.5.18,<1.6.0" },
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
@@ -556,14 +577,14 @@ test = [
test-integration = []
typing = [
{ name = "langchain-text-splitters", directory = "../text-splitters" },
{ name = "mypy", specifier = ">=1.18.1,<1.19.0" },
{ name = "mypy", specifier = ">=1.19.1,<1.20.0" },
{ name = "types-pyyaml", specifier = ">=6.0.12.2,<7.0.0.0" },
{ name = "types-requests", specifier = ">=2.28.11.5,<3.0.0.0" },
]
[[package]]
name = "langchain-text-splitters"
version = "1.0.0"
version = "1.1.0"
source = { editable = "../text-splitters" }
dependencies = [
{ name = "langchain-core" },
@@ -579,7 +600,7 @@ dev = [
]
lint = [
{ name = "langchain-core", editable = "../core" },
{ name = "ruff", specifier = ">=0.13.1,<0.14.0" },
{ name = "ruff", specifier = ">=0.14.11,<0.15.0" },
]
test = [
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
@@ -596,7 +617,7 @@ test-integration = [
{ name = "nltk", specifier = ">=3.9.1,<4.0.0" },
{ name = "scipy", marker = "python_full_version == '3.12.*'", specifier = ">=1.7.0,<2.0.0" },
{ name = "scipy", marker = "python_full_version >= '3.13'", specifier = ">=1.14.1,<2.0.0" },
{ name = "sentence-transformers", marker = "python_full_version < '3.14'", specifier = ">=3.0.1,<4.0.0" },
{ name = "sentence-transformers", specifier = ">=3.0.1,<4.0.0" },
{ name = "spacy", marker = "python_full_version < '3.14'", specifier = ">=3.8.7,<4.0.0" },
{ name = "thinc", specifier = ">=8.3.6,<9.0.0" },
{ name = "tiktoken", specifier = ">=0.8.0,<1.0.0" },
@@ -605,14 +626,14 @@ test-integration = [
typing = [
{ name = "beautifulsoup4", specifier = ">=4.13.5,<5.0.0" },
{ name = "lxml-stubs", specifier = ">=0.5.1,<1.0.0" },
{ name = "mypy", specifier = ">=1.18.1,<1.19.0" },
{ name = "mypy", specifier = ">=1.19.1,<1.20.0" },
{ name = "tiktoken", specifier = ">=0.8.0,<1.0.0" },
{ name = "types-requests", specifier = ">=2.31.0.20240218,<3.0.0.0" },
]
[[package]]
name = "langgraph"
version = "1.0.0"
version = "1.0.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
@@ -622,48 +643,48 @@ dependencies = [
{ name = "pydantic" },
{ name = "xxhash" },
]
sdist = { url = "https://files.pythonhosted.org/packages/57/f7/7ae10f1832ab1a6a402f451e54d6dab277e28e7d4e4204e070c7897ca71c/langgraph-1.0.0.tar.gz", hash = "sha256:5f83ed0e9bbcc37635bc49cbc9b3d9306605fa07504f955b7a871ed715f9964c", size = 472835, upload-time = "2025-10-17T20:23:38.263Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/9c/dac99ab1732e9fb2d3b673482ac28f02bee222c0319a3b8f8f73d90727e6/langgraph-1.0.6.tar.gz", hash = "sha256:dd8e754c76d34a07485308d7117221acf63990e7de8f46ddf5fe256b0a22e6c5", size = 495092, upload-time = "2026-01-12T20:33:30.778Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/42/6f6d0fe4eb661b06da8e6c59e58044e9e4221fdbffdcacae864557de961e/langgraph-1.0.0-py3-none-any.whl", hash = "sha256:4d478781832a1bc67e06c3eb571412ec47d7c57a5467d1f3775adf0e9dd4042c", size = 155416, upload-time = "2025-10-17T20:23:36.978Z" },
{ url = "https://files.pythonhosted.org/packages/10/45/9960747781416bed4e531ed0c6b2f2c739bc7b5397d8e92155463735a40e/langgraph-1.0.6-py3-none-any.whl", hash = "sha256:bcfce190974519c72e29f6e5b17f0023914fd6f936bfab8894083215b271eb89", size = 157356, upload-time = "2026-01-12T20:33:29.191Z" },
]
[[package]]
name = "langgraph-checkpoint"
version = "2.1.2"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
{ name = "ormsgpack" },
]
sdist = { url = "https://files.pythonhosted.org/packages/29/83/6404f6ed23a91d7bc63d7df902d144548434237d017820ceaa8d014035f2/langgraph_checkpoint-2.1.2.tar.gz", hash = "sha256:112e9d067a6eff8937caf198421b1ffba8d9207193f14ac6f89930c1260c06f9", size = 142420, upload-time = "2025-10-07T17:45:17.129Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b7/cb/2a6dad2f0a14317580cc122e2a60e7f0ecabb50aaa6dc5b7a6a2c94cead7/langgraph_checkpoint-3.0.0.tar.gz", hash = "sha256:f738695ad938878d8f4775d907d9629e9fcd345b1950196effb08f088c52369e", size = 132132, upload-time = "2025-10-20T18:35:49.132Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c4/f2/06bf5addf8ee664291e1b9ffa1f28fc9d97e59806dc7de5aea9844cbf335/langgraph_checkpoint-2.1.2-py3-none-any.whl", hash = "sha256:911ebffb069fd01775d4b5184c04aaafc2962fcdf50cf49d524cd4367c4d0c60", size = 45763, upload-time = "2025-10-07T17:45:16.19Z" },
{ url = "https://files.pythonhosted.org/packages/85/2a/2efe0b5a72c41e3a936c81c5f5d8693987a1b260287ff1bbebaae1b7b888/langgraph_checkpoint-3.0.0-py3-none-any.whl", hash = "sha256:560beb83e629784ab689212a3d60834fb3196b4bbe1d6ac18e5cad5d85d46010", size = 46060, upload-time = "2025-10-20T18:35:48.255Z" },
]
[[package]]
name = "langgraph-prebuilt"
version = "1.0.0"
version = "1.0.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
{ name = "langgraph-checkpoint" },
]
sdist = { url = "https://files.pythonhosted.org/packages/02/2d/934b1129e217216a0dfaf0f7df0a10cedf2dfafe6cc8e1ee238cafaaa4a7/langgraph_prebuilt-1.0.0.tar.gz", hash = "sha256:eb75dad9aca0137451ca0395aa8541a665b3f60979480b0431d626fd195dcda2", size = 119927, upload-time = "2025-10-17T20:15:21.429Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3c/f5/8c75dace0d729561dce2966e630c5e312193df7e5df41a7e10cd7378c3a7/langgraph_prebuilt-1.0.6.tar.gz", hash = "sha256:c5f6cf0f5a0ac47643d2e26ae6faa38cb28885ecde67911190df9e30c4f72361", size = 162623, upload-time = "2026-01-12T20:31:28.425Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/33/2e/ffa698eedc4c355168a9207ee598b2cc74ede92ce2b55c3469ea06978b6e/langgraph_prebuilt-1.0.0-py3-none-any.whl", hash = "sha256:ceaae4c5cee8c1f9b6468f76c114cafebb748aed0c93483b7c450e5a89de9c61", size = 28455, upload-time = "2025-10-17T20:15:20.043Z" },
{ url = "https://files.pythonhosted.org/packages/26/6c/4045822b0630cfc0f8624c4499ceaf90644142143c063a8dc385a7424fc3/langgraph_prebuilt-1.0.6-py3-none-any.whl", hash = "sha256:9fdc35048ff4ac985a55bd2a019a86d45b8184551504aff6780d096c678b39ae", size = 35322, upload-time = "2026-01-12T20:31:27.161Z" },
]
[[package]]
name = "langgraph-sdk"
version = "0.2.9"
version = "0.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
{ name = "orjson" },
]
sdist = { url = "https://files.pythonhosted.org/packages/23/d8/40e01190a73c564a4744e29a6c902f78d34d43dad9b652a363a92a67059c/langgraph_sdk-0.2.9.tar.gz", hash = "sha256:b3bd04c6be4fa382996cd2be8fbc1e7cc94857d2bc6b6f4599a7f2a245975303", size = 99802, upload-time = "2025-09-20T18:49:14.734Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/0f/ed0634c222eed48a31ba48eab6881f94ad690d65e44fe7ca838240a260c1/langgraph_sdk-0.3.3.tar.gz", hash = "sha256:c34c3dce3b6848755eb61f0c94369d1ba04aceeb1b76015db1ea7362c544fb26", size = 130589, upload-time = "2026-01-13T00:30:43.894Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/66/05/b2d34e16638241e6f27a6946d28160d4b8b641383787646d41a3727e0896/langgraph_sdk-0.2.9-py3-none-any.whl", hash = "sha256:fbf302edadbf0fb343596f91c597794e936ef68eebc0d3e1d358b6f9f72a1429", size = 56752, upload-time = "2025-09-20T18:49:13.346Z" },
{ url = "https://files.pythonhosted.org/packages/6e/be/4ad511bacfdd854afb12974f407cb30010dceb982dc20c55491867b34526/langgraph_sdk-0.3.3-py3-none-any.whl", hash = "sha256:a52ebaf09d91143e55378bb2d0b033ed98f57f48c9ad35c8f81493b88705fc7b", size = 67021, upload-time = "2026-01-13T00:30:42.264Z" },
]
[[package]]
@@ -706,6 +727,79 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3e/8e/e7a43d907a147e1f87eebdd6737483f9feba52a5d4b20f69d0bd6f2fa22f/langsmith-0.4.31-py3-none-any.whl", hash = "sha256:64f340bdead21defe5f4a6ca330c11073e35444989169f669508edf45a19025f", size = 386347, upload-time = "2025-09-25T04:18:16.69Z" },
]
[[package]]
name = "librt"
version = "0.7.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b5/8a/071f6628363d83e803d4783e0cd24fb9c5b798164300fcfaaa47c30659c0/librt-0.7.5.tar.gz", hash = "sha256:de4221a1181fa9c8c4b5f35506ed6f298948f44003d84d2a8b9885d7e01e6cfa", size = 145868, upload-time = "2025-12-25T03:53:16.039Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/f2/3248d8419db99ab80bb36266735d1241f766ad5fd993071211f789b618a5/librt-0.7.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81056e01bba1394f1d92904ec61a4078f66df785316275edbaf51d90da8c6e26", size = 54703, upload-time = "2025-12-25T03:51:48.394Z" },
{ url = "https://files.pythonhosted.org/packages/7b/30/7e179543dbcb1311f84b7e797658ad85cf2d4474c468f5dbafa13f2a98a5/librt-0.7.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d7c72c8756eeb3aefb1b9e3dac7c37a4a25db63640cac0ab6fc18e91a0edf05a", size = 56660, upload-time = "2025-12-25T03:51:49.791Z" },
{ url = "https://files.pythonhosted.org/packages/15/91/3ba03ac1ac1abd66757a134b3bd56d9674928b163d0e686ea065a2bbb92d/librt-0.7.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ddc4a16207f88f9597b397fc1f60781266d13b13de922ff61c206547a29e4bbd", size = 161026, upload-time = "2025-12-25T03:51:51.021Z" },
{ url = "https://files.pythonhosted.org/packages/0d/6e/b8365f547817d37b44c4be2ffa02630be995ef18be52d72698cecc3640c5/librt-0.7.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63055d3dda433ebb314c9f1819942f16a19203c454508fdb2d167613f7017169", size = 169530, upload-time = "2025-12-25T03:51:52.417Z" },
{ url = "https://files.pythonhosted.org/packages/63/6a/8442eb0b6933c651a06e1888f863971f3391cc11338fdaa6ab969f7d1eac/librt-0.7.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f85f9b5db87b0f52e53c68ad2a0c5a53e00afa439bd54a1723742a2b1021276", size = 183272, upload-time = "2025-12-25T03:51:53.713Z" },
{ url = "https://files.pythonhosted.org/packages/90/c4/b1166df6ef8e1f68d309f50bf69e8e750a5ea12fe7e2cf202c771ff359fc/librt-0.7.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c566a4672564c5d54d8ab65cdaae5a87ee14c1564c1a2ddc7a9f5811c750f023", size = 179040, upload-time = "2025-12-25T03:51:55.048Z" },
{ url = "https://files.pythonhosted.org/packages/fc/30/8f3fd9fd975b16c37832d6c248b976d2a0e33f155063781e064f249b37f1/librt-0.7.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fee15c2a190ef389f14928135c6fb2d25cd3fdb7887bfd9a7b444bbdc8c06b96", size = 173506, upload-time = "2025-12-25T03:51:56.407Z" },
{ url = "https://files.pythonhosted.org/packages/75/71/c3d4d5658f9849bf8e07ffba99f892d49a0c9a4001323ed610db72aedc82/librt-0.7.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:584cb3e605ec45ba350962cec853e17be0a25a772f21f09f1e422f7044ae2a7d", size = 193573, upload-time = "2025-12-25T03:51:57.949Z" },
{ url = "https://files.pythonhosted.org/packages/86/7c/c1c8a0116a2eed3d58c8946c589a8f9e1354b9b825cc92eba58bb15f6fb1/librt-0.7.5-cp310-cp310-win32.whl", hash = "sha256:9c08527055fbb03c641c15bbc5b79dd2942fb6a3bd8dabf141dd7e97eeea4904", size = 42603, upload-time = "2025-12-25T03:51:59.215Z" },
{ url = "https://files.pythonhosted.org/packages/1d/00/b52c77ca294247420020b829b70465c6e6f2b9d59ab21d8051aac20432da/librt-0.7.5-cp310-cp310-win_amd64.whl", hash = "sha256:dd810f2d39c526c42ea205e0addad5dc08ef853c625387806a29d07f9d150d9b", size = 48977, upload-time = "2025-12-25T03:52:00.519Z" },
{ url = "https://files.pythonhosted.org/packages/11/89/42b3ccb702a7e5f7a4cf2afc8a0a8f8c5e7d4b4d3a7c3de6357673dddddb/librt-0.7.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f952e1a78c480edee8fb43aa2bf2e84dcd46c917d44f8065b883079d3893e8fc", size = 54705, upload-time = "2025-12-25T03:52:01.433Z" },
{ url = "https://files.pythonhosted.org/packages/bb/90/c16970b509c3c448c365041d326eeef5aeb2abaed81eb3187b26a3cd13f8/librt-0.7.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75965c1f4efb7234ff52a58b729d245a21e87e4b6a26a0ec08052f02b16274e4", size = 56667, upload-time = "2025-12-25T03:52:02.391Z" },
{ url = "https://files.pythonhosted.org/packages/ac/2f/da4bdf6c190503f4663fbb781dfae5564a2b1c3f39a2da8e1ac7536ac7bd/librt-0.7.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:732e0aa0385b59a1b2545159e781c792cc58ce9c134249233a7c7250a44684c4", size = 161705, upload-time = "2025-12-25T03:52:03.395Z" },
{ url = "https://files.pythonhosted.org/packages/fb/88/c5da8e1f5f22b23d56e1fbd87266799dcf32828d47bf69fabc6f9673c6eb/librt-0.7.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdde31759bd8888f3ef0eebda80394a48961328a17c264dce8cc35f4b9cde35d", size = 171029, upload-time = "2025-12-25T03:52:04.798Z" },
{ url = "https://files.pythonhosted.org/packages/38/8a/8dfc00a6f1febc094ed9a55a448fc0b3a591b5dfd83be6cfd76d0910b1f0/librt-0.7.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3146d52465b3b6397d25d513f428cb421c18df65b7378667bb5f1e3cc45805", size = 184704, upload-time = "2025-12-25T03:52:05.887Z" },
{ url = "https://files.pythonhosted.org/packages/ad/57/65dec835ff235f431801064a3b41268f2f5ee0d224dc3bbf46d911af5c1a/librt-0.7.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:29c8d2fae11d4379ea207ba7fc69d43237e42cf8a9f90ec6e05993687e6d648b", size = 180720, upload-time = "2025-12-25T03:52:06.925Z" },
{ url = "https://files.pythonhosted.org/packages/1e/27/92033d169bbcaa0d9a2dd476c179e5171ec22ed574b1b135a3c6104fb7d4/librt-0.7.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb41f04046b4f22b1e7ba5ef513402cd2e3477ec610e5f92d38fe2bba383d419", size = 174538, upload-time = "2025-12-25T03:52:08.075Z" },
{ url = "https://files.pythonhosted.org/packages/44/5c/0127098743575d5340624d8d4ec508d4d5ff0877dcee6f55f54bf03e5ed0/librt-0.7.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8bb7883c1e94ceb87c2bf81385266f032da09cd040e804cc002f2c9d6b842e2f", size = 195240, upload-time = "2025-12-25T03:52:09.427Z" },
{ url = "https://files.pythonhosted.org/packages/47/0f/be028c3e906a8ee6d29a42fd362e6d57d4143057f2bc0c454d489a0f898b/librt-0.7.5-cp311-cp311-win32.whl", hash = "sha256:84d4a6b9efd6124f728558a18e79e7cc5c5d4efc09b2b846c910de7e564f5bad", size = 42941, upload-time = "2025-12-25T03:52:10.527Z" },
{ url = "https://files.pythonhosted.org/packages/ac/3a/2f0ed57f4c3ae3c841780a95dfbea4cd811c6842d9ee66171ce1af606d25/librt-0.7.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab4b0d3bee6f6ff7017e18e576ac7e41a06697d8dea4b8f3ab9e0c8e1300c409", size = 49244, upload-time = "2025-12-25T03:52:11.832Z" },
{ url = "https://files.pythonhosted.org/packages/ee/7c/d7932aedfa5a87771f9e2799e7185ec3a322f4a1f4aa87c234159b75c8c8/librt-0.7.5-cp311-cp311-win_arm64.whl", hash = "sha256:730be847daad773a3c898943cf67fb9845a3961d06fb79672ceb0a8cd8624cfa", size = 42614, upload-time = "2025-12-25T03:52:12.745Z" },
{ url = "https://files.pythonhosted.org/packages/33/9d/cb0a296cee177c0fee7999ada1c1af7eee0e2191372058814a4ca6d2baf0/librt-0.7.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba1077c562a046208a2dc6366227b3eeae8f2c2ab4b41eaf4fd2fa28cece4203", size = 55689, upload-time = "2025-12-25T03:52:14.041Z" },
{ url = "https://files.pythonhosted.org/packages/79/5c/d7de4d4228b74c5b81a3fbada157754bb29f0e1f8c38229c669a7f90422a/librt-0.7.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:654fdc971c76348a73af5240d8e2529265b9a7ba6321e38dd5bae7b0d4ab3abe", size = 57142, upload-time = "2025-12-25T03:52:15.336Z" },
{ url = "https://files.pythonhosted.org/packages/e5/b2/5da779184aae369b69f4ae84225f63741662a0fe422e91616c533895d7a4/librt-0.7.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6b7b58913d475911f6f33e8082f19dd9b120c4f4a5c911d07e395d67b81c6982", size = 165323, upload-time = "2025-12-25T03:52:16.384Z" },
{ url = "https://files.pythonhosted.org/packages/5a/40/6d5abc15ab6cc70e04c4d201bb28baffff4cfb46ab950b8e90935b162d58/librt-0.7.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8e0fd344bad57026a8f4ccfaf406486c2fc991838050c2fef156170edc3b775", size = 174218, upload-time = "2025-12-25T03:52:17.518Z" },
{ url = "https://files.pythonhosted.org/packages/0d/d0/5239a8507e6117a3cb59ce0095bdd258bd2a93d8d4b819a506da06d8d645/librt-0.7.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46aa91813c267c3f60db75d56419b42c0c0b9748ec2c568a0e3588e543fb4233", size = 189007, upload-time = "2025-12-25T03:52:18.585Z" },
{ url = "https://files.pythonhosted.org/packages/1f/a4/8eed1166ffddbb01c25363e4c4e655f4bac298debe9e5a2dcfaf942438a1/librt-0.7.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ddc0ab9dbc5f9ceaf2bf7a367bf01f2697660e908f6534800e88f43590b271db", size = 183962, upload-time = "2025-12-25T03:52:19.723Z" },
{ url = "https://files.pythonhosted.org/packages/a1/83/260e60aab2f5ccba04579c5c46eb3b855e51196fde6e2bcf6742d89140a8/librt-0.7.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7a488908a470451338607650f1c064175094aedebf4a4fa37890682e30ce0b57", size = 177611, upload-time = "2025-12-25T03:52:21.18Z" },
{ url = "https://files.pythonhosted.org/packages/c4/36/6dcfed0df41e9695665462bab59af15b7ed2b9c668d85c7ebadd022cbb76/librt-0.7.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e47fc52602ffc374e69bf1b76536dc99f7f6dd876bd786c8213eaa3598be030a", size = 199273, upload-time = "2025-12-25T03:52:22.25Z" },
{ url = "https://files.pythonhosted.org/packages/a6/b7/157149c8cffae6bc4293a52e0267860cee2398cb270798d94f1c8a69b9ae/librt-0.7.5-cp312-cp312-win32.whl", hash = "sha256:cda8b025875946ffff5a9a7590bf9acde3eb02cb6200f06a2d3e691ef3d9955b", size = 43191, upload-time = "2025-12-25T03:52:23.643Z" },
{ url = "https://files.pythonhosted.org/packages/f8/91/197dfeb8d3bdeb0a5344d0d8b3077f183ba5e76c03f158126f6072730998/librt-0.7.5-cp312-cp312-win_amd64.whl", hash = "sha256:b591c094afd0ffda820e931148c9e48dc31a556dc5b2b9b3cc552fa710d858e4", size = 49462, upload-time = "2025-12-25T03:52:24.637Z" },
{ url = "https://files.pythonhosted.org/packages/03/ea/052a79454cc52081dfaa9a1c4c10a529f7a6a6805b2fac5805fea5b25975/librt-0.7.5-cp312-cp312-win_arm64.whl", hash = "sha256:532ddc6a8a6ca341b1cd7f4d999043e4c71a212b26fe9fd2e7f1e8bb4e873544", size = 42830, upload-time = "2025-12-25T03:52:25.944Z" },
{ url = "https://files.pythonhosted.org/packages/9f/9a/8f61e16de0ff76590af893cfb5b1aa5fa8b13e5e54433d0809c7033f59ed/librt-0.7.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b1795c4b2789b458fa290059062c2f5a297ddb28c31e704d27e161386469691a", size = 55750, upload-time = "2025-12-25T03:52:26.975Z" },
{ url = "https://files.pythonhosted.org/packages/05/7c/a8a883804851a066f301e0bad22b462260b965d5c9e7fe3c5de04e6f91f8/librt-0.7.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2fcbf2e135c11f721193aa5f42ba112bb1046afafbffd407cbc81d8d735c74d0", size = 57170, upload-time = "2025-12-25T03:52:27.948Z" },
{ url = "https://files.pythonhosted.org/packages/d6/5d/b3b47facf5945be294cf8a835b03589f70ee0e791522f99ec6782ed738b3/librt-0.7.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c039bbf79a9a2498404d1ae7e29a6c175e63678d7a54013a97397c40aee026c5", size = 165834, upload-time = "2025-12-25T03:52:29.09Z" },
{ url = "https://files.pythonhosted.org/packages/b4/b6/b26910cd0a4e43e5d02aacaaea0db0d2a52e87660dca08293067ee05601a/librt-0.7.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3919c9407faeeee35430ae135e3a78acd4ecaaaa73767529e2c15ca1d73ba325", size = 174820, upload-time = "2025-12-25T03:52:30.463Z" },
{ url = "https://files.pythonhosted.org/packages/a5/a3/81feddd345d4c869b7a693135a462ae275f964fcbbe793d01ea56a84c2ee/librt-0.7.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26b46620e1e0e45af510d9848ea0915e7040605dd2ae94ebefb6c962cbb6f7ec", size = 189609, upload-time = "2025-12-25T03:52:31.492Z" },
{ url = "https://files.pythonhosted.org/packages/ce/a9/31310796ef4157d1d37648bf4a3b84555319f14cee3e9bad7bdd7bfd9a35/librt-0.7.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9bbb8facc5375476d392990dd6a71f97e4cb42e2ac66f32e860f6e47299d5e89", size = 184589, upload-time = "2025-12-25T03:52:32.59Z" },
{ url = "https://files.pythonhosted.org/packages/32/22/da3900544cb0ac6ab7a2857850158a0a093b86f92b264aa6c4a4f2355ff3/librt-0.7.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e9e9c988b5ffde7be02180f864cbd17c0b0c1231c235748912ab2afa05789c25", size = 178251, upload-time = "2025-12-25T03:52:33.745Z" },
{ url = "https://files.pythonhosted.org/packages/db/77/78e02609846e78b9b8c8e361753b3dbac9a07e6d5b567fe518de9e074ab0/librt-0.7.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:edf6b465306215b19dbe6c3fb63cf374a8f3e1ad77f3b4c16544b83033bbb67b", size = 199852, upload-time = "2025-12-25T03:52:34.826Z" },
{ url = "https://files.pythonhosted.org/packages/2a/25/05706f6b346429c951582f1b3561f4d5e1418d0d7ba1a0c181237cd77b3b/librt-0.7.5-cp313-cp313-win32.whl", hash = "sha256:060bde69c3604f694bd8ae21a780fe8be46bb3dbb863642e8dfc75c931ca8eee", size = 43250, upload-time = "2025-12-25T03:52:35.905Z" },
{ url = "https://files.pythonhosted.org/packages/d9/59/c38677278ac0b9ae1afc611382ef6c9ea87f52ad257bd3d8d65f0eacdc6a/librt-0.7.5-cp313-cp313-win_amd64.whl", hash = "sha256:a82d5a0ee43aeae2116d7292c77cc8038f4841830ade8aa922e098933b468b9e", size = 49421, upload-time = "2025-12-25T03:52:36.895Z" },
{ url = "https://files.pythonhosted.org/packages/c0/47/1d71113df4a81de5fdfbd3d7244e05d3d67e89f25455c3380ca50b92741e/librt-0.7.5-cp313-cp313-win_arm64.whl", hash = "sha256:3c98a8d0ac9e2a7cb8ff8c53e5d6e8d82bfb2839abf144fdeaaa832f2a12aa45", size = 42827, upload-time = "2025-12-25T03:52:37.856Z" },
{ url = "https://files.pythonhosted.org/packages/97/ae/8635b4efdc784220f1378be640d8b1a794332f7f6ea81bb4859bf9d18aa7/librt-0.7.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9937574e6d842f359b8585903d04f5b4ab62277a091a93e02058158074dc52f2", size = 55191, upload-time = "2025-12-25T03:52:38.839Z" },
{ url = "https://files.pythonhosted.org/packages/52/11/ed7ef6955dc2032af37db9b0b31cd5486a138aa792e1bb9e64f0f4950e27/librt-0.7.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5cd3afd71e9bc146203b6c8141921e738364158d4aa7cdb9a874e2505163770f", size = 56894, upload-time = "2025-12-25T03:52:39.805Z" },
{ url = "https://files.pythonhosted.org/packages/24/f1/02921d4a66a1b5dcd0493b89ce76e2762b98c459fe2ad04b67b2ea6fdd39/librt-0.7.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cffa3ef0af29687455161cb446eff059bf27607f95163d6a37e27bcb37180f6", size = 163726, upload-time = "2025-12-25T03:52:40.79Z" },
{ url = "https://files.pythonhosted.org/packages/65/87/27df46d2756fcb7a82fa7f6ca038a0c6064c3e93ba65b0b86fbf6a4f76a2/librt-0.7.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82f3f088482e2229387eadf8215c03f7726d56f69cce8c0c40f0795aebc9b361", size = 172470, upload-time = "2025-12-25T03:52:42.226Z" },
{ url = "https://files.pythonhosted.org/packages/9f/a9/e65a35e5d423639f4f3d8e17301ff13cc41c2ff97677fe9c361c26dbfbb7/librt-0.7.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7aa33153a5bb0bac783d2c57885889b1162823384e8313d47800a0e10d0070e", size = 186807, upload-time = "2025-12-25T03:52:43.688Z" },
{ url = "https://files.pythonhosted.org/packages/d7/b0/ac68aa582a996b1241773bd419823290c42a13dc9f494704a12a17ddd7b6/librt-0.7.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:265729b551a2dd329cc47b323a182fb7961af42abf21e913c9dd7d3331b2f3c2", size = 181810, upload-time = "2025-12-25T03:52:45.095Z" },
{ url = "https://files.pythonhosted.org/packages/e1/c1/03f6717677f20acd2d690813ec2bbe12a2de305f32c61479c53f7b9413bc/librt-0.7.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:168e04663e126416ba712114050f413ac306759a1791d87b7c11d4428ba75760", size = 175599, upload-time = "2025-12-25T03:52:46.177Z" },
{ url = "https://files.pythonhosted.org/packages/01/d7/f976ff4c07c59b69bb5eec7e5886d43243075bbef834428124b073471c86/librt-0.7.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:553dc58987d1d853adda8aeadf4db8e29749f0b11877afcc429a9ad892818ae2", size = 196506, upload-time = "2025-12-25T03:52:47.327Z" },
{ url = "https://files.pythonhosted.org/packages/b7/74/004f068b8888e61b454568b5479f88018fceb14e511ac0609cccee7dd227/librt-0.7.5-cp314-cp314-win32.whl", hash = "sha256:263f4fae9eba277513357c871275b18d14de93fd49bf5e43dc60a97b81ad5eb8", size = 39747, upload-time = "2025-12-25T03:52:48.437Z" },
{ url = "https://files.pythonhosted.org/packages/37/b1/ea3ec8fcf5f0a00df21f08972af77ad799604a306db58587308067d27af8/librt-0.7.5-cp314-cp314-win_amd64.whl", hash = "sha256:85f485b7471571e99fab4f44eeb327dc0e1f814ada575f3fa85e698417d8a54e", size = 45970, upload-time = "2025-12-25T03:52:49.389Z" },
{ url = "https://files.pythonhosted.org/packages/5d/30/5e3fb7ac4614a50fc67e6954926137d50ebc27f36419c9963a94f931f649/librt-0.7.5-cp314-cp314-win_arm64.whl", hash = "sha256:49c596cd18e90e58b7caa4d7ca7606049c1802125fcff96b8af73fa5c3870e4d", size = 39075, upload-time = "2025-12-25T03:52:50.395Z" },
{ url = "https://files.pythonhosted.org/packages/a4/7f/0af0a9306a06c2aabee3a790f5aa560c50ec0a486ab818a572dd3db6c851/librt-0.7.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:54d2aef0b0f5056f130981ad45081b278602ff3657fe16c88529f5058038e802", size = 57375, upload-time = "2025-12-25T03:52:51.439Z" },
{ url = "https://files.pythonhosted.org/packages/57/1f/c85e510baf6572a3d6ef40c742eacedc02973ed2acdb5dba2658751d9af8/librt-0.7.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0b4791202296ad51ac09a3ff58eb49d9da8e3a4009167a6d76ac418a974e5fd4", size = 59234, upload-time = "2025-12-25T03:52:52.687Z" },
{ url = "https://files.pythonhosted.org/packages/49/b1/bb6535e4250cd18b88d6b18257575a0239fa1609ebba925f55f51ae08e8e/librt-0.7.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e860909fea75baef941ee6436e0453612505883b9d0d87924d4fda27865b9a2", size = 183873, upload-time = "2025-12-25T03:52:53.705Z" },
{ url = "https://files.pythonhosted.org/packages/8e/49/ad4a138cca46cdaa7f0e15fa912ce3ccb4cc0d4090bfeb8ccc35766fa6d5/librt-0.7.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f02c4337bf271c4f06637f5ff254fad2238c0b8e32a3a480ebb2fc5e26f754a5", size = 194609, upload-time = "2025-12-25T03:52:54.884Z" },
{ url = "https://files.pythonhosted.org/packages/9c/2d/3b3cb933092d94bb2c1d3c9b503d8775f08d806588c19a91ee4d1495c2a8/librt-0.7.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7f51ffe59f4556243d3cc82d827bde74765f594fa3ceb80ec4de0c13ccd3416", size = 206777, upload-time = "2025-12-25T03:52:55.969Z" },
{ url = "https://files.pythonhosted.org/packages/3a/52/6e7611d3d1347812233dabc44abca4c8065ee97b83c9790d7ecc3f782bc8/librt-0.7.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0b7f080ba30601dfa3e3deed3160352273e1b9bc92e652f51103c3e9298f7899", size = 203208, upload-time = "2025-12-25T03:52:57.036Z" },
{ url = "https://files.pythonhosted.org/packages/27/aa/466ae4654bd2d45903fbf180815d41e3ae8903e5a1861f319f73c960a843/librt-0.7.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fb565b4219abc8ea2402e61c7ba648a62903831059ed3564fa1245cc245d58d7", size = 196698, upload-time = "2025-12-25T03:52:58.481Z" },
{ url = "https://files.pythonhosted.org/packages/97/8f/424f7e4525bb26fe0d3e984d1c0810ced95e53be4fd867ad5916776e18a3/librt-0.7.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a3cfb15961e7333ea6ef033dc574af75153b5c230d5ad25fbcd55198f21e0cf", size = 217194, upload-time = "2025-12-25T03:52:59.575Z" },
{ url = "https://files.pythonhosted.org/packages/9e/33/13a4cb798a171b173f3c94db23adaf13a417130e1493933dc0df0d7fb439/librt-0.7.5-cp314-cp314t-win32.whl", hash = "sha256:118716de5ad6726332db1801bc90fa6d94194cd2e07c1a7822cebf12c496714d", size = 40282, upload-time = "2025-12-25T03:53:01.091Z" },
{ url = "https://files.pythonhosted.org/packages/5f/f1/62b136301796399d65dad73b580f4509bcbd347dff885a450bff08e80cb6/librt-0.7.5-cp314-cp314t-win_amd64.whl", hash = "sha256:3dd58f7ce20360c6ce0c04f7bd9081c7f9c19fc6129a3c705d0c5a35439f201d", size = 46764, upload-time = "2025-12-25T03:53:02.381Z" },
{ url = "https://files.pythonhosted.org/packages/49/cb/940431d9410fda74f941f5cd7f0e5a22c63be7b0c10fa98b2b7022b48cb1/librt-0.7.5-cp314-cp314t-win_arm64.whl", hash = "sha256:08153ea537609d11f774d2bfe84af39d50d5c9ca3a4d061d946e0c9d8bce04a1", size = 39728, upload-time = "2025-12-25T03:53:03.306Z" },
]
[[package]]
name = "markdown-it-py"
version = "4.0.0"
@@ -729,47 +823,48 @@ wheels = [
[[package]]
name = "mypy"
version = "1.18.2"
version = "1.19.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/03/6f/657961a0743cff32e6c0611b63ff1c1970a0b482ace35b069203bf705187/mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c", size = 12807973, upload-time = "2025-09-19T00:10:35.282Z" },
{ url = "https://files.pythonhosted.org/packages/10/e9/420822d4f661f13ca8900f5fa239b40ee3be8b62b32f3357df9a3045a08b/mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e", size = 11896527, upload-time = "2025-09-19T00:10:55.791Z" },
{ url = "https://files.pythonhosted.org/packages/aa/73/a05b2bbaa7005f4642fcfe40fb73f2b4fb6bb44229bd585b5878e9a87ef8/mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b", size = 12507004, upload-time = "2025-09-19T00:11:05.411Z" },
{ url = "https://files.pythonhosted.org/packages/4f/01/f6e4b9f0d031c11ccbd6f17da26564f3a0f3c4155af344006434b0a05a9d/mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66", size = 13245947, upload-time = "2025-09-19T00:10:46.923Z" },
{ url = "https://files.pythonhosted.org/packages/d7/97/19727e7499bfa1ae0773d06afd30ac66a58ed7437d940c70548634b24185/mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428", size = 13499217, upload-time = "2025-09-19T00:09:39.472Z" },
{ url = "https://files.pythonhosted.org/packages/9f/4f/90dc8c15c1441bf31cf0f9918bb077e452618708199e530f4cbd5cede6ff/mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed", size = 9766753, upload-time = "2025-09-19T00:10:49.161Z" },
{ url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" },
{ url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" },
{ url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" },
{ url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" },
{ url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" },
{ url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" },
{ url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" },
{ url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" },
{ url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" },
{ url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" },
{ url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" },
{ url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" },
{ url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" },
{ url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" },
{ url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" },
{ url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" },
{ url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" },
{ url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" },
{ url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" },
{ url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" },
{ url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" },
{ url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" },
{ url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" },
{ url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" },
{ url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" },
{ url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" },
{ url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" },
{ url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" },
{ url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" },
{ url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" },
{ url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" },
{ url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" },
{ url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" },
{ url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" },
{ url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" },
{ url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" },
{ url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" },
{ url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" },
{ url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" },
{ url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" },
{ url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" },
{ url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" },
{ url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" },
{ url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" },
{ url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" },
{ url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" },
{ url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" },
{ url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" },
{ url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" },
{ url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" },
{ url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" },
{ url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" },
{ url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" },
{ url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" },
{ url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" },
]
[[package]]
@@ -1215,28 +1310,28 @@ wheels = [
[[package]]
name = "ruff"
version = "0.13.3"
version = "0.14.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/8e/f9f9ca747fea8e3ac954e3690d4698c9737c23b51731d02df999c150b1c9/ruff-0.13.3.tar.gz", hash = "sha256:5b0ba0db740eefdfbcce4299f49e9eaefc643d4d007749d77d047c2bab19908e", size = 5438533, upload-time = "2025-10-02T19:29:31.582Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d4/77/9a7fe084d268f8855d493e5031ea03fa0af8cc05887f638bf1c4e3363eb8/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417, upload-time = "2026-01-08T19:11:58.322Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/33/8f7163553481466a92656d35dea9331095122bb84cf98210bef597dd2ecd/ruff-0.13.3-py3-none-linux_armv6l.whl", hash = "sha256:311860a4c5e19189c89d035638f500c1e191d283d0cc2f1600c8c80d6dcd430c", size = 12484040, upload-time = "2025-10-02T19:28:49.199Z" },
{ url = "https://files.pythonhosted.org/packages/b0/b5/4a21a4922e5dd6845e91896b0d9ef493574cbe061ef7d00a73c61db531af/ruff-0.13.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2bdad6512fb666b40fcadb65e33add2b040fc18a24997d2e47fee7d66f7fcae2", size = 13122975, upload-time = "2025-10-02T19:28:52.446Z" },
{ url = "https://files.pythonhosted.org/packages/40/90/15649af836d88c9f154e5be87e64ae7d2b1baa5a3ef317cb0c8fafcd882d/ruff-0.13.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fc6fa4637284708d6ed4e5e970d52fc3b76a557d7b4e85a53013d9d201d93286", size = 12346621, upload-time = "2025-10-02T19:28:54.712Z" },
{ url = "https://files.pythonhosted.org/packages/a5/42/bcbccb8141305f9a6d3f72549dd82d1134299177cc7eaf832599700f95a7/ruff-0.13.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c9e6469864f94a98f412f20ea143d547e4c652f45e44f369d7b74ee78185838", size = 12574408, upload-time = "2025-10-02T19:28:56.679Z" },
{ url = "https://files.pythonhosted.org/packages/ce/19/0f3681c941cdcfa2d110ce4515624c07a964dc315d3100d889fcad3bfc9e/ruff-0.13.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5bf62b705f319476c78891e0e97e965b21db468b3c999086de8ffb0d40fd2822", size = 12285330, upload-time = "2025-10-02T19:28:58.79Z" },
{ url = "https://files.pythonhosted.org/packages/10/f8/387976bf00d126b907bbd7725219257feea58650e6b055b29b224d8cb731/ruff-0.13.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cc1abed87ce40cb07ee0667ce99dbc766c9f519eabfd948ed87295d8737c60", size = 13980815, upload-time = "2025-10-02T19:29:01.577Z" },
{ url = "https://files.pythonhosted.org/packages/0c/a6/7c8ec09d62d5a406e2b17d159e4817b63c945a8b9188a771193b7e1cc0b5/ruff-0.13.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4fb75e7c402d504f7a9a259e0442b96403fa4a7310ffe3588d11d7e170d2b1e3", size = 14987733, upload-time = "2025-10-02T19:29:04.036Z" },
{ url = "https://files.pythonhosted.org/packages/97/e5/f403a60a12258e0fd0c2195341cfa170726f254c788673495d86ab5a9a9d/ruff-0.13.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b951f9d9afb39330b2bdd2dd144ce1c1335881c277837ac1b50bfd99985ed3", size = 14439848, upload-time = "2025-10-02T19:29:06.684Z" },
{ url = "https://files.pythonhosted.org/packages/39/49/3de381343e89364c2334c9f3268b0349dc734fc18b2d99a302d0935c8345/ruff-0.13.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6052f8088728898e0a449f0dde8fafc7ed47e4d878168b211977e3e7e854f662", size = 13421890, upload-time = "2025-10-02T19:29:08.767Z" },
{ url = "https://files.pythonhosted.org/packages/ab/b5/c0feca27d45ae74185a6bacc399f5d8920ab82df2d732a17213fb86a2c4c/ruff-0.13.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc742c50f4ba72ce2a3be362bd359aef7d0d302bf7637a6f942eaa763bd292af", size = 13444870, upload-time = "2025-10-02T19:29:11.234Z" },
{ url = "https://files.pythonhosted.org/packages/50/a1/b655298a1f3fda4fdc7340c3f671a4b260b009068fbeb3e4e151e9e3e1bf/ruff-0.13.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8e5640349493b378431637019366bbd73c927e515c9c1babfea3e932f5e68e1d", size = 13691599, upload-time = "2025-10-02T19:29:13.353Z" },
{ url = "https://files.pythonhosted.org/packages/32/b0/a8705065b2dafae007bcae21354e6e2e832e03eb077bb6c8e523c2becb92/ruff-0.13.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6b139f638a80eae7073c691a5dd8d581e0ba319540be97c343d60fb12949c8d0", size = 12421893, upload-time = "2025-10-02T19:29:15.668Z" },
{ url = "https://files.pythonhosted.org/packages/0d/1e/cbe7082588d025cddbb2f23e6dfef08b1a2ef6d6f8328584ad3015b5cebd/ruff-0.13.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6b547def0a40054825de7cfa341039ebdfa51f3d4bfa6a0772940ed351d2746c", size = 12267220, upload-time = "2025-10-02T19:29:17.583Z" },
{ url = "https://files.pythonhosted.org/packages/a5/99/4086f9c43f85e0755996d09bdcb334b6fee9b1eabdf34e7d8b877fadf964/ruff-0.13.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9cc48a3564423915c93573f1981d57d101e617839bef38504f85f3677b3a0a3e", size = 13177818, upload-time = "2025-10-02T19:29:19.943Z" },
{ url = "https://files.pythonhosted.org/packages/9b/de/7b5db7e39947d9dc1c5f9f17b838ad6e680527d45288eeb568e860467010/ruff-0.13.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1a993b17ec03719c502881cb2d5f91771e8742f2ca6de740034433a97c561989", size = 13618715, upload-time = "2025-10-02T19:29:22.527Z" },
{ url = "https://files.pythonhosted.org/packages/28/d3/bb25ee567ce2f61ac52430cf99f446b0e6d49bdfa4188699ad005fdd16aa/ruff-0.13.3-py3-none-win32.whl", hash = "sha256:f14e0d1fe6460f07814d03c6e32e815bff411505178a1f539a38f6097d3e8ee3", size = 12334488, upload-time = "2025-10-02T19:29:24.782Z" },
{ url = "https://files.pythonhosted.org/packages/cf/49/12f5955818a1139eed288753479ba9d996f6ea0b101784bb1fe6977ec128/ruff-0.13.3-py3-none-win_amd64.whl", hash = "sha256:621e2e5812b691d4f244638d693e640f188bacbb9bc793ddd46837cea0503dd2", size = 13455262, upload-time = "2025-10-02T19:29:26.882Z" },
{ url = "https://files.pythonhosted.org/packages/fe/72/7b83242b26627a00e3af70d0394d68f8f02750d642567af12983031777fc/ruff-0.13.3-py3-none-win_arm64.whl", hash = "sha256:9e9e9d699841eaf4c2c798fa783df2fabc680b72059a02ca0ed81c460bc58330", size = 12538484, upload-time = "2025-10-02T19:29:28.951Z" },
{ url = "https://files.pythonhosted.org/packages/f0/a6/a4c40a5aaa7e331f245d2dc1ac8ece306681f52b636b40ef87c88b9f7afd/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208, upload-time = "2026-01-08T19:12:09.218Z" },
{ url = "https://files.pythonhosted.org/packages/5c/5c/360a35cb7204b328b685d3129c08aca24765ff92b5a7efedbdd6c150d555/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075, upload-time = "2026-01-08T19:12:02.549Z" },
{ url = "https://files.pythonhosted.org/packages/1b/9e/0cc2f1be7a7d33cae541824cf3f95b4ff40d03557b575912b5b70273c9ec/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809, upload-time = "2026-01-08T19:12:00.366Z" },
{ url = "https://files.pythonhosted.org/packages/a7/e5/5faab97c15bb75228d9f74637e775d26ac703cc2b4898564c01ab3637c02/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447, upload-time = "2026-01-08T19:12:13.899Z" },
{ url = "https://files.pythonhosted.org/packages/1b/33/e9767f60a2bef779fb5855cab0af76c488e0ce90f7bb7b8a45c8a2ba4178/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560, upload-time = "2026-01-08T19:11:42.55Z" },
{ url = "https://files.pythonhosted.org/packages/eb/84/4c6cf627a21462bb5102f7be2a320b084228ff26e105510cd2255ea868e5/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296, upload-time = "2026-01-08T19:11:30.371Z" },
{ url = "https://files.pythonhosted.org/packages/88/e1/92b5ed7ea66d849f6157e695dc23d5d6d982bd6aa8d077895652c38a7cae/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981, upload-time = "2026-01-08T19:12:04.742Z" },
{ url = "https://files.pythonhosted.org/packages/61/df/c1bd30992615ac17c2fb64b8a7376ca22c04a70555b5d05b8f717163cf9f/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183, upload-time = "2026-01-08T19:11:40.069Z" },
{ url = "https://files.pythonhosted.org/packages/04/e9/fe552902f25013dd28a5428a42347d9ad20c4b534834a325a28305747d64/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453, upload-time = "2026-01-08T19:11:37.555Z" },
{ url = "https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889, upload-time = "2026-01-08T19:12:07.094Z" },
{ url = "https://files.pythonhosted.org/packages/b7/9f/c7fb6ecf554f28709a6a1f2a7f74750d400979e8cd47ed29feeaa1bd4db8/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832, upload-time = "2026-01-08T19:11:55.064Z" },
{ url = "https://files.pythonhosted.org/packages/db/a0/153315310f250f76900a98278cf878c64dfb6d044e184491dd3289796734/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522, upload-time = "2026-01-08T19:11:35.356Z" },
{ url = "https://files.pythonhosted.org/packages/2f/2b/a73a2b6e6d2df1d74bf2b78098be1572191e54bec0e59e29382d13c3adc5/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637, upload-time = "2026-01-08T19:11:47.796Z" },
{ url = "https://files.pythonhosted.org/packages/f0/41/09100590320394401cd3c48fc718a8ba71c7ddb1ffd07e0ad6576b3a3df2/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837, upload-time = "2026-01-08T19:11:32.87Z" },
{ url = "https://files.pythonhosted.org/packages/3b/d8/e035db859d1d3edf909381eb8ff3e89a672d6572e9454093538fe6f164b0/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469, upload-time = "2026-01-08T19:12:11.694Z" },
{ url = "https://files.pythonhosted.org/packages/4e/02/bb3ff8b6e6d02ce9e3740f4c17dfbbfb55f34c789c139e9cd91985f356c7/ruff-0.14.11-py3-none-win32.whl", hash = "sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841", size = 12851094, upload-time = "2026-01-08T19:11:45.163Z" },
{ url = "https://files.pythonhosted.org/packages/58/f1/90ddc533918d3a2ad628bc3044cdfc094949e6d4b929220c3f0eb8a1c998/ruff-0.14.11-py3-none-win_amd64.whl", hash = "sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6", size = 14001379, upload-time = "2026-01-08T19:11:52.591Z" },
{ url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" },
]
[[package]]
@@ -1328,15 +1423,15 @@ wheels = [
[[package]]
name = "starlette"
version = "0.48.0"
version = "0.49.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/3f/507c21db33b66fb027a332f2cb3abbbe924cc3a79ced12f01ed8645955c9/starlette-0.49.1.tar.gz", hash = "sha256:481a43b71e24ed8c43b11ea02f5353d77840e01480881b8cb5a26b8cae64a8cb", size = 2654703, upload-time = "2025-10-28T17:34:10.928Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" },
{ url = "https://files.pythonhosted.org/packages/51/da/545b75d420bb23b5d494b0517757b351963e974e79933f01e05c929f20a6/starlette-0.49.1-py3-none-any.whl", hash = "sha256:d92ce9f07e4a3caa3ac13a79523bd18e3bc0042bb8ff2d759a8e7dd0e1859875", size = 74175, upload-time = "2025-10-28T17:34:09.13Z" },
]
[[package]]
@@ -1434,11 +1529,40 @@ wheels = [
[[package]]
name = "urllib3"
version = "2.5.0"
version = "2.6.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
]
[[package]]
name = "uuid-utils"
version = "0.12.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/0e/512fb221e4970c2f75ca9dae412d320b7d9ddc9f2b15e04ea8e44710396c/uuid_utils-0.12.0.tar.gz", hash = "sha256:252bd3d311b5d6b7f5dfce7a5857e27bb4458f222586bb439463231e5a9cbd64", size = 20889, upload-time = "2025-12-01T17:29:55.494Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/43/de5cd49a57b6293b911b6a9a62fc03e55db9f964da7d5882d9edbee1e9d2/uuid_utils-0.12.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:3b9b30707659292f207b98f294b0e081f6d77e1fbc760ba5b41331a39045f514", size = 603197, upload-time = "2025-12-01T17:29:30.104Z" },
{ url = "https://files.pythonhosted.org/packages/02/fa/5fd1d8c9234e44f0c223910808cde0de43bb69f7df1349e49b1afa7f2baa/uuid_utils-0.12.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:add3d820c7ec14ed37317375bea30249699c5d08ff4ae4dbee9fc9bce3bfbf65", size = 305168, upload-time = "2025-12-01T17:29:31.384Z" },
{ url = "https://files.pythonhosted.org/packages/c8/c6/8633ac9942bf9dc97a897b5154e5dcffa58816ec4dd780b3b12b559ff05c/uuid_utils-0.12.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b8fce83ecb3b16af29c7809669056c4b6e7cc912cab8c6d07361645de12dd79", size = 340580, upload-time = "2025-12-01T17:29:32.362Z" },
{ url = "https://files.pythonhosted.org/packages/f3/88/8a61307b04b4da1c576373003e6d857a04dade52ab035151d62cb84d5cb5/uuid_utils-0.12.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec921769afcb905035d785582b0791d02304a7850fbd6ce924c1a8976380dfc6", size = 346771, upload-time = "2025-12-01T17:29:33.708Z" },
{ url = "https://files.pythonhosted.org/packages/1c/fb/aab2dcf94b991e62aa167457c7825b9b01055b884b888af926562864398c/uuid_utils-0.12.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f3b060330f5899a92d5c723547dc6a95adef42433e9748f14c66859a7396664", size = 474781, upload-time = "2025-12-01T17:29:35.237Z" },
{ url = "https://files.pythonhosted.org/packages/5a/7a/dbd5e49c91d6c86dba57158bbfa0e559e1ddf377bb46dcfd58aea4f0d567/uuid_utils-0.12.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:908dfef7f0bfcf98d406e5dc570c25d2f2473e49b376de41792b6e96c1d5d291", size = 343685, upload-time = "2025-12-01T17:29:36.677Z" },
{ url = "https://files.pythonhosted.org/packages/1a/19/8c4b1d9f450159733b8be421a4e1fb03533709b80ed3546800102d085572/uuid_utils-0.12.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c6a24148926bd0ca63e8a2dabf4cc9dc329a62325b3ad6578ecd60fbf926506", size = 366482, upload-time = "2025-12-01T17:29:37.979Z" },
{ url = "https://files.pythonhosted.org/packages/82/43/c79a6e45687647f80a159c8ba34346f287b065452cc419d07d2212d38420/uuid_utils-0.12.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:64a91e632669f059ef605f1771d28490b1d310c26198e46f754e8846dddf12f4", size = 523132, upload-time = "2025-12-01T17:29:39.293Z" },
{ url = "https://files.pythonhosted.org/packages/5a/a2/b2d75a621260a40c438aa88593827dfea596d18316520a99e839f7a5fb9d/uuid_utils-0.12.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:93c082212470bb4603ca3975916c205a9d7ef1443c0acde8fbd1e0f5b36673c7", size = 614218, upload-time = "2025-12-01T17:29:40.315Z" },
{ url = "https://files.pythonhosted.org/packages/13/6b/ba071101626edd5a6dabf8525c9a1537ff3d885dbc210540574a03901fef/uuid_utils-0.12.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:431b1fb7283ba974811b22abd365f2726f8f821ab33f0f715be389640e18d039", size = 546241, upload-time = "2025-12-01T17:29:41.656Z" },
{ url = "https://files.pythonhosted.org/packages/01/12/9a942b81c0923268e6d85bf98d8f0a61fcbcd5e432fef94fdf4ce2ef8748/uuid_utils-0.12.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd7838c40149100299fa37cbd8bab5ee382372e8e65a148002a37d380df7c8", size = 511842, upload-time = "2025-12-01T17:29:43.107Z" },
{ url = "https://files.pythonhosted.org/packages/a9/a7/c326f5163dd48b79368b87d8a05f5da4668dd228a3f5ca9d79d5fee2fc40/uuid_utils-0.12.0-cp39-abi3-win32.whl", hash = "sha256:487f17c0fee6cbc1d8b90fe811874174a9b1b5683bf2251549e302906a50fed3", size = 179088, upload-time = "2025-12-01T17:29:44.492Z" },
{ url = "https://files.pythonhosted.org/packages/38/92/41c8734dd97213ee1d5ae435cf4499705dc4f2751e3b957fd12376f61784/uuid_utils-0.12.0-cp39-abi3-win_amd64.whl", hash = "sha256:9598e7c9da40357ae8fffc5d6938b1a7017f09a1acbcc95e14af8c65d48c655a", size = 183003, upload-time = "2025-12-01T17:29:45.47Z" },
{ url = "https://files.pythonhosted.org/packages/c9/f9/52ab0359618987331a1f739af837d26168a4b16281c9c3ab46519940c628/uuid_utils-0.12.0-cp39-abi3-win_arm64.whl", hash = "sha256:c9bea7c5b2aa6f57937ebebeee4d4ef2baad10f86f1b97b58a3f6f34c14b4e84", size = 182975, upload-time = "2025-12-01T17:29:46.444Z" },
{ url = "https://files.pythonhosted.org/packages/ef/f7/6c55b7722cede3b424df02ed5cddb25c19543abda2f95fa4cfc34a892ae5/uuid_utils-0.12.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e2209d361f2996966ab7114f49919eb6aaeabc6041672abbbbf4fdbb8ec1acc0", size = 593065, upload-time = "2025-12-01T17:29:47.507Z" },
{ url = "https://files.pythonhosted.org/packages/b8/40/ce5fe8e9137dbd5570e0016c2584fca43ad81b11a1cef809a1a1b4952ab7/uuid_utils-0.12.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d9636bcdbd6cfcad2b549c352b669412d0d1eb09be72044a2f13e498974863cd", size = 300047, upload-time = "2025-12-01T17:29:48.596Z" },
{ url = "https://files.pythonhosted.org/packages/fb/9b/31c5d0736d7b118f302c50214e581f40e904305d8872eb0f0c921d50e138/uuid_utils-0.12.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cd8543a3419251fb78e703ce3b15fdfafe1b7c542cf40caf0775e01db7e7674", size = 335165, upload-time = "2025-12-01T17:29:49.755Z" },
{ url = "https://files.pythonhosted.org/packages/f6/5c/d80b4d08691c9d7446d0ad58fd41503081a662cfd2c7640faf68c64d8098/uuid_utils-0.12.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e98db2d8977c052cb307ae1cb5cc37a21715e8d415dbc65863b039397495a013", size = 341437, upload-time = "2025-12-01T17:29:51.112Z" },
{ url = "https://files.pythonhosted.org/packages/f6/b3/9dccdc6f3c22f6ef5bd381ae559173f8a1ae185ae89ed1f39f499d9d8b02/uuid_utils-0.12.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8f2bdf5e4ffeb259ef6d15edae92aed60a1d6f07cbfab465d836f6b12b48da8", size = 469123, upload-time = "2025-12-01T17:29:52.389Z" },
{ url = "https://files.pythonhosted.org/packages/fd/90/6c35ef65fbc49f8189729839b793a4a74a7dd8c5aa5eb56caa93f8c97732/uuid_utils-0.12.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c3ec53c0cb15e1835870c139317cc5ec06e35aa22843e3ed7d9c74f23f23898", size = 335892, upload-time = "2025-12-01T17:29:53.44Z" },
{ url = "https://files.pythonhosted.org/packages/6b/c7/e3f3ce05c5af2bf86a0938d22165affe635f4dcbfd5687b1dacc042d3e0e/uuid_utils-0.12.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:84e5c0eba209356f7f389946a3a47b2cc2effd711b3fc7c7f155ad9f7d45e8a3", size = 360693, upload-time = "2025-12-01T17:29:54.558Z" },
]
[[package]]

View File

@@ -1,4 +1,4 @@
.PHONY: all format lint test tests test_watch integration_tests help extended_tests
.PHONY: all format lint test tests test_watch integration_tests help extended_tests check_version
# Default target executed when no arguments are given to make.
all: help
@@ -31,6 +31,9 @@ test_profile:
check_imports: $(shell find langchain_core -name '*.py')
uv run --group test python ./scripts/check_imports.py $^
check_version:
uv run python ./scripts/check_version.py
extended_tests:
uv run --group test pytest --only-extended --disable-socket --allow-unix-socket $(TEST_FILE)
@@ -69,6 +72,7 @@ help:
@echo '----'
@echo 'format - run code formatters'
@echo 'lint - run linters'
@echo 'check_version - validate version consistency'
@echo 'test - run unit tests'
@echo 'tests - run unit tests'
@echo 'test TEST_FILE=<test_file> - run all tests in file'

View File

@@ -3,7 +3,7 @@
[![PyPI - Version](https://img.shields.io/pypi/v/langchain-core?label=%20)](https://pypi.org/project/langchain-core/#history)
[![PyPI - License](https://img.shields.io/pypi/l/langchain-core)](https://opensource.org/licenses/MIT)
[![PyPI - Downloads](https://img.shields.io/pepy/dt/langchain-core)](https://pypistats.org/packages/langchain-core)
[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/langchainai.svg?style=social&label=Follow%20%40LangChainAI)](https://twitter.com/langchainai)
[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/langchain.svg?style=social&label=Follow%20%40LangChain)](https://x.com/langchain)
Looking for the JS/TS version? Check out [LangChain.js](https://github.com/langchain-ai/langchainjs).

View File

@@ -13,20 +13,20 @@ from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from .beta_decorator import (
from langchain_core._api.beta_decorator import (
LangChainBetaWarning,
beta,
suppress_langchain_beta_warning,
surface_langchain_beta_warnings,
)
from .deprecation import (
from langchain_core._api.deprecation import (
LangChainDeprecationWarning,
deprecated,
suppress_langchain_deprecation_warning,
surface_langchain_deprecation_warnings,
warn_deprecated,
)
from .path import as_import_path, get_relative_path
from langchain_core._api.path import as_import_path, get_relative_path
__all__ = (
"LangChainBetaWarning",
@@ -58,6 +58,20 @@ _dynamic_imports = {
def __getattr__(attr_name: str) -> object:
"""Dynamically import and return an attribute from a submodule.
This function enables lazy loading of API functions from submodules, reducing
initial import time and circular dependency issues.
Args:
attr_name: Name of the attribute to import.
Returns:
The imported attribute object.
Raises:
AttributeError: If the attribute is not a valid dynamic import.
"""
module_name = _dynamic_imports.get(attr_name)
result = import_attr(attr_name, module_name, __spec__.parent)
globals()[attr_name] = result
@@ -65,4 +79,9 @@ def __getattr__(attr_name: str) -> object:
def __dir__() -> list[str]:
"""Return a list of available attributes for this module.
Returns:
List of attribute names that can be imported from this module.
"""
return list(__all__)

View File

@@ -125,7 +125,7 @@ def beta(
_name = _name or obj.__qualname__
old_doc = obj.__doc__
def finalize(wrapper: Callable[..., Any], new_doc: str) -> T: # noqa: ARG001
def finalize(_: Callable[..., Any], new_doc: str, /) -> T:
"""Finalize the annotation of a class."""
# Can't set new_doc on some extension objects.
with contextlib.suppress(AttributeError):
@@ -168,7 +168,7 @@ def beta(
emit_warning()
obj.fdel(instance)
def finalize(_wrapper: Callable[..., Any], new_doc: str) -> Any:
def finalize(_: Callable[..., Any], new_doc: str, /) -> Any:
"""Finalize the property."""
return property(fget=_fget, fset=_fset, fdel=_fdel, doc=new_doc)
@@ -181,7 +181,7 @@ def beta(
wrapped = obj
old_doc = wrapped.__doc__
def finalize(wrapper: Callable[..., Any], new_doc: str) -> T:
def finalize(wrapper: Callable[..., Any], new_doc: str, /) -> T:
"""Wrap the wrapped function using the wrapper and update the docstring.
Args:

View File

@@ -28,6 +28,27 @@ from pydantic.v1.fields import FieldInfo as FieldInfoV1
from langchain_core._api.internal import is_caller_internal
def _build_deprecation_message(
*,
alternative: str = "",
alternative_import: str = "",
) -> str:
"""Build a simple deprecation message for `__deprecated__` attribute.
Args:
alternative: An alternative API name.
alternative_import: A fully qualified import path for the alternative.
Returns:
A deprecation message string for IDE/type checker display.
"""
if alternative_import:
return f"Use {alternative_import} instead."
if alternative:
return f"Use {alternative} instead."
return "Deprecated."
class LangChainDeprecationWarning(DeprecationWarning):
"""A class for issuing deprecation warnings for LangChain users."""
@@ -81,60 +102,57 @@ def deprecated(
) -> Callable[[T], T]:
"""Decorator to mark a function, a class, or a property as deprecated.
When deprecating a classmethod, a staticmethod, or a property, the
`@deprecated` decorator should go *under* `@classmethod` and
`@staticmethod` (i.e., `deprecated` should directly decorate the
underlying callable), but *over* `@property`.
When deprecating a classmethod, a staticmethod, or a property, the `@deprecated`
decorator should go *under* `@classmethod` and `@staticmethod` (i.e., `deprecated`
should directly decorate the underlying callable), but *over* `@property`.
When deprecating a class `C` intended to be used as a base class in a
multiple inheritance hierarchy, `C` *must* define an `__init__` method
(if `C` instead inherited its `__init__` from its own base class, then
`@deprecated` would mess up `__init__` inheritance when installing its
own (deprecation-emitting) `C.__init__`).
When deprecating a class `C` intended to be used as a base class in a multiple
inheritance hierarchy, `C` *must* define an `__init__` method (if `C` instead
inherited its `__init__` from its own base class, then `@deprecated` would mess up
`__init__` inheritance when installing its own (deprecation-emitting) `C.__init__`).
Parameters are the same as for `warn_deprecated`, except that *obj_type*
defaults to 'class' if decorating a class, 'attribute' if decorating a
property, and 'function' otherwise.
Parameters are the same as for `warn_deprecated`, except that *obj_type* defaults to
'class' if decorating a class, 'attribute' if decorating a property, and 'function'
otherwise.
Args:
since:
The release at which this API became deprecated.
message:
Override the default deprecation message. The %(since)s,
%(name)s, %(alternative)s, %(obj_type)s, %(addendum)s,
and %(removal)s format specifiers will be replaced by the
since: The release at which this API became deprecated.
message: Override the default deprecation message.
The `%(since)s`, `%(name)s`, `%(alternative)s`, `%(obj_type)s`,
`%(addendum)s`, and `%(removal)s` format specifiers will be replaced by the
values of the respective arguments passed to this function.
name:
The name of the deprecated object.
alternative:
An alternative API that the user may use in place of the
deprecated API. The deprecation warning will tell the user
about this alternative if provided.
alternative_import:
An alternative import that the user may use instead.
pending:
If `True`, uses a `PendingDeprecationWarning` instead of a
DeprecationWarning. Cannot be used together with removal.
obj_type:
The object type being deprecated.
addendum:
Additional text appended directly to the final message.
removal:
The expected removal version. With the default (an empty
string), a removal version is automatically computed from
since. Set to other Falsy values to not schedule a removal
date. Cannot be used together with pending.
package:
The package of the deprecated object.
name: The name of the deprecated object.
alternative: An alternative API that the user may use in place of the deprecated
API.
The deprecation warning will tell the user about this alternative if
provided.
alternative_import: An alternative import that the user may use instead.
pending: If `True`, uses a `PendingDeprecationWarning` instead of a
`DeprecationWarning`.
Cannot be used together with removal.
obj_type: The object type being deprecated.
addendum: Additional text appended directly to the final message.
removal: The expected removal version.
With the default (an empty string), a removal version is automatically
computed from since. Set to other Falsy values to not schedule a removal
date.
Cannot be used together with pending.
package: The package of the deprecated object.
Returns:
A decorator to mark a function or class as deprecated.
```python
@deprecated("1.4.0")
def the_function_to_deprecate():
pass
```
Example:
```python
@deprecated("1.4.0")
def the_function_to_deprecate():
pass
```
"""
_validate_deprecation_params(
removal, alternative, alternative_import, pending=pending
@@ -204,7 +222,7 @@ def deprecated(
_name = _name or obj.__qualname__
old_doc = obj.__doc__
def finalize(wrapper: Callable[..., Any], new_doc: str) -> T: # noqa: ARG001
def finalize(_: Callable[..., Any], new_doc: str, /) -> T:
"""Finalize the deprecation of a class."""
# Can't set new_doc on some extension objects.
with contextlib.suppress(AttributeError):
@@ -223,6 +241,11 @@ def deprecated(
obj.__init__ = functools.wraps(obj.__init__)( # type: ignore[misc]
warn_if_direct_instance
)
# Set __deprecated__ for PEP 702 (IDE/type checker support)
obj.__deprecated__ = _build_deprecation_message( # type: ignore[attr-defined]
alternative=alternative,
alternative_import=alternative_import,
)
return obj
elif isinstance(obj, FieldInfoV1):
@@ -234,7 +257,7 @@ def deprecated(
raise ValueError(msg)
old_doc = obj.description
def finalize(wrapper: Callable[..., Any], new_doc: str) -> T: # noqa: ARG001
def finalize(_: Callable[..., Any], new_doc: str, /) -> T:
return cast(
"T",
FieldInfoV1(
@@ -255,7 +278,7 @@ def deprecated(
raise ValueError(msg)
old_doc = obj.description
def finalize(wrapper: Callable[..., Any], new_doc: str) -> T: # noqa: ARG001
def finalize(_: Callable[..., Any], new_doc: str, /) -> T:
return cast(
"T",
FieldInfo(
@@ -313,14 +336,17 @@ def deprecated(
if _name == "<lambda>":
_name = set_name
def finalize(wrapper: Callable[..., Any], new_doc: str) -> T: # noqa: ARG001
def finalize(_: Callable[..., Any], new_doc: str, /) -> T:
"""Finalize the property."""
return cast(
"T",
_DeprecatedProperty(
fget=obj.fget, fset=obj.fset, fdel=obj.fdel, doc=new_doc
),
prop = _DeprecatedProperty(
fget=obj.fget, fset=obj.fset, fdel=obj.fdel, doc=new_doc
)
# Set __deprecated__ for PEP 702 (IDE/type checker support)
prop.__deprecated__ = _build_deprecation_message( # type: ignore[attr-defined]
alternative=alternative,
alternative_import=alternative_import,
)
return cast("T", prop)
else:
_name = _name or cast("type | Callable", obj).__qualname__
@@ -331,7 +357,7 @@ def deprecated(
wrapped = obj
old_doc = wrapped.__doc__
def finalize(wrapper: Callable[..., Any], new_doc: str) -> T:
def finalize(wrapper: Callable[..., Any], new_doc: str, /) -> T:
"""Wrap the wrapped function using the wrapper and update the docstring.
Args:
@@ -343,6 +369,11 @@ def deprecated(
"""
wrapper = functools.wraps(wrapped)(wrapper)
wrapper.__doc__ = new_doc
# Set __deprecated__ for PEP 702 (IDE/type checker support)
wrapper.__deprecated__ = _build_deprecation_message( # type: ignore[attr-defined]
alternative=alternative,
alternative_import=alternative_import,
)
return cast("T", wrapper)
old_doc = inspect.cleandoc(old_doc or "").strip("\n")
@@ -398,7 +429,7 @@ def deprecated(
@contextlib.contextmanager
def suppress_langchain_deprecation_warning() -> Generator[None, None, None]:
"""Context manager to suppress LangChainDeprecationWarning."""
"""Context manager to suppress `LangChainDeprecationWarning`."""
with warnings.catch_warnings():
warnings.simplefilter("ignore", LangChainDeprecationWarning)
warnings.simplefilter("ignore", LangChainPendingDeprecationWarning)
@@ -421,35 +452,33 @@ def warn_deprecated(
"""Display a standardized deprecation.
Args:
since:
The release at which this API became deprecated.
message:
Override the default deprecation message. The %(since)s,
%(name)s, %(alternative)s, %(obj_type)s, %(addendum)s,
and %(removal)s format specifiers will be replaced by the
since: The release at which this API became deprecated.
message: Override the default deprecation message.
The `%(since)s`, `%(name)s`, `%(alternative)s`, `%(obj_type)s`,
`%(addendum)s`, and `%(removal)s` format specifiers will be replaced by the
values of the respective arguments passed to this function.
name:
The name of the deprecated object.
alternative:
An alternative API that the user may use in place of the
deprecated API. The deprecation warning will tell the user
about this alternative if provided.
alternative_import:
An alternative import that the user may use instead.
pending:
If `True`, uses a `PendingDeprecationWarning` instead of a
DeprecationWarning. Cannot be used together with removal.
obj_type:
The object type being deprecated.
addendum:
Additional text appended directly to the final message.
removal:
The expected removal version. With the default (an empty
string), a removal version is automatically computed from
since. Set to other Falsy values to not schedule a removal
date. Cannot be used together with pending.
package:
The package of the deprecated object.
name: The name of the deprecated object.
alternative: An alternative API that the user may use in place of the
deprecated API.
The deprecation warning will tell the user about this alternative if
provided.
alternative_import: An alternative import that the user may use instead.
pending: If `True`, uses a `PendingDeprecationWarning` instead of a
`DeprecationWarning`.
Cannot be used together with removal.
obj_type: The object type being deprecated.
addendum: Additional text appended directly to the final message.
removal: The expected removal version.
With the default (an empty string), a removal version is automatically
computed from since. Set to other Falsy values to not schedule a removal
date.
Cannot be used together with pending.
package: The package of the deprecated object.
"""
if not pending:
if not removal:
@@ -534,8 +563,8 @@ def rename_parameter(
"""Decorator indicating that parameter *old* of *func* is renamed to *new*.
The actual implementation of *func* should use *new*, not *old*. If *old* is passed
to *func*, a DeprecationWarning is emitted, and its value is used, even if *new* is
also passed by keyword.
to *func*, a `DeprecationWarning` is emitted, and its value is used, even if *new*
is also passed by keyword.
Args:
since: The version in which the parameter was renamed.

View File

@@ -1,4 +1,5 @@
import inspect
from typing import cast
def is_caller_internal(depth: int = 2) -> bool:
@@ -16,7 +17,7 @@ def is_caller_internal(depth: int = 2) -> bool:
return False
# Directly access the module name from the frame's global variables
module_globals = frame.f_globals
caller_module_name = module_globals.get("__name__", "")
caller_module_name = cast("str", module_globals.get("__name__", ""))
return caller_module_name.startswith("langchain")
finally:
del frame

View File

@@ -3,6 +3,7 @@
Distinct from provider-based [prompt caching](https://docs.langchain.com/oss/python/langchain/models#prompt-caching).
!!! warning "Beta feature"
This is a beta feature. Please be wary of deploying experimental code to production
unless you've taken appropriate precautions.

View File

@@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
class RetrieverManagerMixin:
"""Mixin for Retriever callbacks."""
"""Mixin for `Retriever` callbacks."""
def on_retriever_error(
self,
@@ -31,12 +31,12 @@ class RetrieverManagerMixin:
parent_run_id: UUID | None = None,
**kwargs: Any,
) -> Any:
"""Run when Retriever errors.
"""Run when `Retriever` errors.
Args:
error: The error that occurred.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
**kwargs: Additional keyword arguments.
"""
@@ -48,12 +48,12 @@ class RetrieverManagerMixin:
parent_run_id: UUID | None = None,
**kwargs: Any,
) -> Any:
"""Run when Retriever ends running.
"""Run when `Retriever` ends running.
Args:
documents: The documents retrieved.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
**kwargs: Additional keyword arguments.
"""
@@ -68,6 +68,7 @@ class LLMManagerMixin:
chunk: GenerationChunk | ChatGenerationChunk | None = None,
run_id: UUID,
parent_run_id: UUID | None = None,
tags: list[str] | None = None,
**kwargs: Any,
) -> Any:
"""Run on new output token. Only available when streaming is enabled.
@@ -77,8 +78,9 @@ class LLMManagerMixin:
Args:
token: The new token.
chunk: The new generated chunk, containing content and other information.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
**kwargs: Additional keyword arguments.
"""
@@ -88,14 +90,16 @@ class LLMManagerMixin:
*,
run_id: UUID,
parent_run_id: UUID | None = None,
tags: list[str] | None = None,
**kwargs: Any,
) -> Any:
"""Run when LLM ends running.
Args:
response: The response which was generated.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
**kwargs: Additional keyword arguments.
"""
@@ -105,14 +109,16 @@ class LLMManagerMixin:
*,
run_id: UUID,
parent_run_id: UUID | None = None,
tags: list[str] | None = None,
**kwargs: Any,
) -> Any:
"""Run when LLM errors.
Args:
error: The error that occurred.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
**kwargs: Additional keyword arguments.
"""
@@ -132,8 +138,8 @@ class ChainManagerMixin:
Args:
outputs: The outputs of the chain.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
**kwargs: Additional keyword arguments.
"""
@@ -149,8 +155,8 @@ class ChainManagerMixin:
Args:
error: The error that occurred.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
**kwargs: Additional keyword arguments.
"""
@@ -166,8 +172,8 @@ class ChainManagerMixin:
Args:
action: The agent action.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
**kwargs: Additional keyword arguments.
"""
@@ -183,8 +189,8 @@ class ChainManagerMixin:
Args:
finish: The agent finish.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
**kwargs: Additional keyword arguments.
"""
@@ -204,8 +210,8 @@ class ToolManagerMixin:
Args:
output: The output of the tool.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
**kwargs: Additional keyword arguments.
"""
@@ -221,8 +227,8 @@ class ToolManagerMixin:
Args:
error: The error that occurred.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
**kwargs: Additional keyword arguments.
"""
@@ -251,8 +257,8 @@ class CallbackManagerMixin:
Args:
serialized: The serialized LLM.
prompts: The prompts.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
metadata: The metadata.
**kwargs: Additional keyword arguments.
@@ -278,8 +284,8 @@ class CallbackManagerMixin:
Args:
serialized: The serialized chat model.
messages: The messages.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
metadata: The metadata.
**kwargs: Additional keyword arguments.
@@ -300,13 +306,13 @@ class CallbackManagerMixin:
metadata: dict[str, Any] | None = None,
**kwargs: Any,
) -> Any:
"""Run when the Retriever starts running.
"""Run when the `Retriever` starts running.
Args:
serialized: The serialized Retriever.
serialized: The serialized `Retriever`.
query: The query.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
metadata: The metadata.
**kwargs: Additional keyword arguments.
@@ -328,8 +334,8 @@ class CallbackManagerMixin:
Args:
serialized: The serialized chain.
inputs: The inputs.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
metadata: The metadata.
**kwargs: Additional keyword arguments.
@@ -352,8 +358,8 @@ class CallbackManagerMixin:
Args:
serialized: The serialized chain.
input_str: The input string.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
metadata: The metadata.
inputs: The inputs.
@@ -376,8 +382,8 @@ class RunManagerMixin:
Args:
text: The text.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
**kwargs: Additional keyword arguments.
"""
@@ -393,8 +399,8 @@ class RunManagerMixin:
Args:
retry_state: The retry state.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
**kwargs: Additional keyword arguments.
"""
@@ -412,13 +418,12 @@ class RunManagerMixin:
Args:
name: The name of the custom event.
data: The data for the custom event. Format will match
the format specified by the user.
data: The data for the custom event. Format will match the format specified
by the user.
run_id: The ID of the run.
tags: The tags associated with the custom event
(includes inherited tags).
metadata: The metadata associated with the custom event
(includes inherited metadata).
tags: The tags associated with the custom event (includes inherited tags).
metadata: The metadata associated with the custom event (includes inherited
metadata).
"""
@@ -430,7 +435,7 @@ class BaseCallbackHandler(
CallbackManagerMixin,
RunManagerMixin,
):
"""Base callback handler for LangChain."""
"""Base callback handler."""
raise_error: bool = False
"""Whether to raise an error if an exception occurs."""
@@ -475,7 +480,7 @@ class BaseCallbackHandler(
class AsyncCallbackHandler(BaseCallbackHandler):
"""Async callback handler for LangChain."""
"""Base async callback handler."""
async def on_llm_start(
self,
@@ -498,8 +503,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
Args:
serialized: The serialized LLM.
prompts: The prompts.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
metadata: The metadata.
**kwargs: Additional keyword arguments.
@@ -525,8 +530,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
Args:
serialized: The serialized chat model.
messages: The messages.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
metadata: The metadata.
**kwargs: Additional keyword arguments.
@@ -553,8 +558,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
Args:
token: The new token.
chunk: The new generated chunk, containing content and other information.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
**kwargs: Additional keyword arguments.
"""
@@ -572,8 +577,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
Args:
response: The response which was generated.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
**kwargs: Additional keyword arguments.
"""
@@ -591,10 +596,11 @@ class AsyncCallbackHandler(BaseCallbackHandler):
Args:
error: The error that occurred.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
**kwargs: Additional keyword arguments.
- response (LLMResult): The response which was generated before
the error occurred.
"""
@@ -615,8 +621,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
Args:
serialized: The serialized chain.
inputs: The inputs.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
metadata: The metadata.
**kwargs: Additional keyword arguments.
@@ -635,8 +641,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
Args:
outputs: The outputs of the chain.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
**kwargs: Additional keyword arguments.
"""
@@ -654,8 +660,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
Args:
error: The error that occurred.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
**kwargs: Additional keyword arguments.
"""
@@ -677,8 +683,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
Args:
serialized: The serialized tool.
input_str: The input string.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
metadata: The metadata.
inputs: The inputs.
@@ -698,8 +704,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
Args:
output: The output of the tool.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
**kwargs: Additional keyword arguments.
"""
@@ -717,8 +723,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
Args:
error: The error that occurred.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
**kwargs: Additional keyword arguments.
"""
@@ -736,8 +742,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
Args:
text: The text.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
**kwargs: Additional keyword arguments.
"""
@@ -754,8 +760,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
Args:
retry_state: The retry state.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
**kwargs: Additional keyword arguments.
"""
@@ -772,8 +778,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
Args:
action: The agent action.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
**kwargs: Additional keyword arguments.
"""
@@ -791,8 +797,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
Args:
finish: The agent finish.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
**kwargs: Additional keyword arguments.
"""
@@ -813,8 +819,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
Args:
serialized: The serialized retriever.
query: The query.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
metadata: The metadata.
**kwargs: Additional keyword arguments.
@@ -833,8 +839,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
Args:
documents: The documents retrieved.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
**kwargs: Additional keyword arguments.
"""
@@ -852,8 +858,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
Args:
error: The error that occurred.
run_id: The run ID. This is the ID of the current run.
parent_run_id: The parent run ID. This is the ID of the parent run.
run_id: The ID of the current run.
parent_run_id: The ID of the parent run.
tags: The tags.
**kwargs: Additional keyword arguments.
"""
@@ -883,7 +889,7 @@ class AsyncCallbackHandler(BaseCallbackHandler):
class BaseCallbackManager(CallbackManagerMixin):
"""Base callback manager for LangChain."""
"""Base callback manager."""
def __init__(
self,
@@ -932,8 +938,9 @@ class BaseCallbackManager(CallbackManagerMixin):
def merge(self, other: BaseCallbackManager) -> Self:
"""Merge the callback manager with another callback manager.
May be overwritten in subclasses. Primarily used internally
within merge_configs.
May be overwritten in subclasses.
Primarily used internally within `merge_configs`.
Returns:
The merged callback manager of the same type as the current object.
@@ -960,28 +967,29 @@ class BaseCallbackManager(CallbackManagerMixin):
# ['tag2', 'tag1']
```
""" # noqa: E501
manager = self.__class__(
# Combine handlers and inheritable_handlers separately, using sets
# to deduplicate (order not preserved)
combined_handlers = list(set(self.handlers) | set(other.handlers))
combined_inheritable = list(
set(self.inheritable_handlers) | set(other.inheritable_handlers)
)
return self.__class__(
parent_run_id=self.parent_run_id or other.parent_run_id,
handlers=[],
inheritable_handlers=[],
handlers=combined_handlers,
inheritable_handlers=combined_inheritable,
tags=list(set(self.tags + other.tags)),
inheritable_tags=list(set(self.inheritable_tags + other.inheritable_tags)),
metadata={
**self.metadata,
**other.metadata,
},
inheritable_metadata={
**self.inheritable_metadata,
**other.inheritable_metadata,
},
)
handlers = self.handlers + other.handlers
inheritable_handlers = self.inheritable_handlers + other.inheritable_handlers
for handler in handlers:
manager.add_handler(handler)
for handler in inheritable_handlers:
manager.add_handler(handler, inherit=True)
return manager
@property
def is_async(self) -> bool:
"""Whether the callback manager is async."""

View File

@@ -12,7 +12,6 @@ from concurrent.futures import ThreadPoolExecutor
from contextlib import asynccontextmanager, contextmanager
from contextvars import copy_context
from typing import TYPE_CHECKING, Any, TypeVar, cast
from uuid import UUID
from langsmith.run_helpers import get_tracing_context
from typing_extensions import Self, override
@@ -44,6 +43,7 @@ from langchain_core.utils.uuid import uuid7
if TYPE_CHECKING:
from collections.abc import AsyncGenerator, Coroutine, Generator, Sequence
from uuid import UUID
from tenacity import RetryCallState

View File

@@ -95,7 +95,7 @@ def get_usage_metadata_callback(
"""Get usage metadata callback.
Get context manager for tracking usage metadata across chat model calls using
`AIMessage.usage_metadata`.
[`AIMessage.usage_metadata`][langchain.messages.AIMessage.usage_metadata].
Args:
name: The name of the context variable.

View File

@@ -11,6 +11,7 @@ from typing_extensions import override
from langchain_core.document_loaders.base import BaseLoader
from langchain_core.documents import Document
from langchain_core.tracers._compat import pydantic_to_dict
class LangSmithLoader(BaseLoader):
@@ -118,14 +119,14 @@ class LangSmithLoader(BaseLoader):
for key in self.content_key:
content = content[key]
content_str = self.format_content(content)
metadata = example.dict()
metadata = pydantic_to_dict(example)
# Stringify datetime and UUID types.
for k in ("dataset_id", "created_at", "modified_at", "source_run_id", "id"):
metadata[k] = str(metadata[k]) if metadata[k] else metadata[k]
yield Document(content_str, metadata=metadata)
def _stringify(x: str | dict) -> str:
def _stringify(x: str | dict[str, Any]) -> str:
if isinstance(x, str):
return x
try:

View File

@@ -30,9 +30,9 @@ from typing import TYPE_CHECKING
from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from .base import Document
from .compressor import BaseDocumentCompressor
from .transformers import BaseDocumentTransformer
from langchain_core.documents.base import Document
from langchain_core.documents.compressor import BaseDocumentCompressor
from langchain_core.documents.transformers import BaseDocumentTransformer
__all__ = ("BaseDocumentCompressor", "BaseDocumentTransformer", "Document")

View File

@@ -11,7 +11,7 @@ from langchain_core.prompts.prompt import PromptTemplate
def _get_length_based(text: str) -> int:
return len(re.split("\n| ", text))
return len(re.split(r"\n| ", text))
class LengthBasedExampleSelector(BaseExampleSelector, BaseModel):

View File

@@ -242,6 +242,17 @@ def _delete(
vector_store: VectorStore | DocumentIndex,
ids: list[str],
) -> None:
"""Delete documents from a vector store or document index by their IDs.
Args:
vector_store: The vector store or document index to delete from.
ids: List of document IDs to delete.
Raises:
IndexingException: If the delete operation fails.
TypeError: If the `vector_store` is neither a `VectorStore` nor a
`DocumentIndex`.
"""
if isinstance(vector_store, VectorStore):
delete_ok = vector_store.delete(ids)
if delete_ok is not None and delete_ok is False:

View File

@@ -1,7 +1,7 @@
"""Language models.
"""Core language model abstractions.
LangChain has two main classes to work with language models: chat models and
"old-fashioned" LLMs.
"old-fashioned" LLMs (string-in, string-out).
**Chat models**
@@ -11,14 +11,16 @@ as outputs (as opposed to using plain text).
Chat models support the assignment of distinct roles to conversation messages, helping
to distinguish messages from the AI, users, and instructions such as system messages.
The key abstraction for chat models is `BaseChatModel`. Implementations should inherit
from this class.
The key abstraction for chat models is
[`BaseChatModel`][langchain_core.language_models.BaseChatModel]. Implementations should
inherit from this class.
See existing [chat model integrations](https://docs.langchain.com/oss/python/integrations/chat).
**LLMs**
**LLMs (legacy)**
Language models that takes a string as input and returns a string.
These are traditionally older models (newer models generally are chat models).
Although the underlying models are string in, string out, the LangChain wrappers also

View File

@@ -12,13 +12,14 @@ from typing import (
Literal,
TypeAlias,
TypeVar,
cast,
)
from pydantic import BaseModel, ConfigDict, Field, field_validator
from typing_extensions import TypedDict, override
from langchain_core.caches import BaseCache
from langchain_core.callbacks import Callbacks
from langchain_core.caches import BaseCache # noqa: TC001
from langchain_core.callbacks import Callbacks # noqa: TC001
from langchain_core.globals import get_verbose
from langchain_core.messages import (
AIMessage,
@@ -86,13 +87,28 @@ def get_tokenizer() -> Any:
return GPT2TokenizerFast.from_pretrained("gpt2")
_GPT2_TOKENIZER_WARNED = False
def _get_token_ids_default_method(text: str) -> list[int]:
"""Encode the text into token IDs."""
# get the cached tokenizer
"""Encode the text into token IDs using the fallback GPT-2 tokenizer."""
global _GPT2_TOKENIZER_WARNED # noqa: PLW0603
if not _GPT2_TOKENIZER_WARNED:
warnings.warn(
"Using fallback GPT-2 tokenizer for token counting. "
"Token counts may be inaccurate for non-GPT-2 models. "
"For accurate counts, use a model-specific method if available.",
stacklevel=3,
)
_GPT2_TOKENIZER_WARNED = True
tokenizer = get_tokenizer()
# tokenize the text using the GPT-2 tokenizer
return tokenizer.encode(text)
# Pass verbose=False to suppress the "Token indices sequence length is longer than
# the specified maximum sequence length" warning from HuggingFace. This warning is
# about GPT-2's 1024 token context limit, but we're only using the tokenizer for
# counting, not for model input.
return cast("list[int]", tokenizer.encode(text, verbose=False))
LanguageModelInput = PromptValue | str | Sequence[MessageLikeRepresentation]

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
import asyncio
import inspect
import json
import typing
from abc import ABC, abstractmethod
from collections.abc import AsyncIterator, Callable, Iterator, Sequence
from functools import cached_property
@@ -74,6 +73,7 @@ from langchain_core.utils.pydantic import TypeBaseModel, is_basemodel_subclass
from langchain_core.utils.utils import LC_ID_PREFIX, from_env
if TYPE_CHECKING:
import builtins
import uuid
from langchain_core.output_parsers.base import OutputParserLike
@@ -341,6 +341,7 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
"""Profile detailing model capabilities.
!!! warning "Beta feature"
This is a beta feature. The format of model profiles is subject to change.
If not specified, automatically loaded from the provider package on initialization
@@ -358,7 +359,10 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
@cached_property
def _serialized(self) -> dict[str, Any]:
return dumpd(self)
# self is always a Serializable object in this case, thus the result is
# guaranteed to be a dict since dumps uses the default callback, which uses
# obj.to_json which always returns TypedDict subclasses
return cast("dict[str, Any]", dumpd(self))
# --- Runnable methods ---
@@ -461,7 +465,7 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
# Check if a runtime streaming flag has been passed in.
if "stream" in kwargs:
return kwargs["stream"]
return bool(kwargs["stream"])
if "streaming" in self.model_fields_set:
streaming_value = getattr(self, "streaming", None)
@@ -547,7 +551,7 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
):
if block["type"] != index_type:
index_type = block["type"]
index = index + 1
index += 1
if "index" not in block:
block["index"] = index
run_manager.on_llm_new_token(
@@ -679,7 +683,7 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
):
if block["type"] != index_type:
index_type = block["type"]
index = index + 1
index += 1
if "index" not in block:
block["index"] = index
await run_manager.on_llm_new_token(
@@ -730,7 +734,7 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
# --- Custom methods ---
def _combine_llm_outputs(self, llm_outputs: list[dict | None]) -> dict: # noqa: ARG002
def _combine_llm_outputs(self, _llm_outputs: list[dict | None], /) -> dict:
return {}
def _convert_cached_generations(self, cache_val: list) -> list[ChatGeneration]:
@@ -1144,7 +1148,15 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
if check_cache:
if llm_cache:
llm_string = self._get_llm_string(stop=stop, **kwargs)
prompt = dumps(messages)
normalized_messages = [
(
msg.model_copy(update={"id": None})
if getattr(msg, "id", None) is not None
else msg
)
for msg in messages
]
prompt = dumps(normalized_messages)
cache_val = llm_cache.lookup(prompt, llm_string)
if isinstance(cache_val, list):
converted_generations = self._convert_cached_generations(cache_val)
@@ -1187,7 +1199,7 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
):
if block["type"] != index_type:
index_type = block["type"]
index = index + 1
index += 1
if "index" not in block:
block["index"] = index
if run_manager:
@@ -1262,7 +1274,15 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
if check_cache:
if llm_cache:
llm_string = self._get_llm_string(stop=stop, **kwargs)
prompt = dumps(messages)
normalized_messages = [
(
msg.model_copy(update={"id": None})
if getattr(msg, "id", None) is not None
else msg
)
for msg in messages
]
prompt = dumps(normalized_messages)
cache_val = await llm_cache.alookup(prompt, llm_string)
if isinstance(cache_val, list):
converted_generations = self._convert_cached_generations(cache_val)
@@ -1305,7 +1325,7 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
):
if block["type"] != index_type:
index_type = block["type"]
index = index + 1
index += 1
if "index" not in block:
block["index"] = index
if run_manager:
@@ -1500,9 +1520,7 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
def bind_tools(
self,
tools: Sequence[
typing.Dict[str, Any] | type | Callable | BaseTool # noqa: UP006
],
tools: Sequence[builtins.dict[str, Any] | type | Callable | BaseTool],
*,
tool_choice: str | None = None,
**kwargs: Any,
@@ -1521,11 +1539,11 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
def with_structured_output(
self,
schema: typing.Dict | type, # noqa: UP006
schema: builtins.dict[str, Any] | type,
*,
include_raw: bool = False,
**kwargs: Any,
) -> Runnable[LanguageModelInput, typing.Dict | BaseModel]: # noqa: UP006
) -> Runnable[LanguageModelInput, builtins.dict[str, Any] | BaseModel]:
"""Model wrapper that returns outputs formatted to match the given schema.
Args:
@@ -1578,86 +1596,86 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
depends on the `schema` as described above.
- `'parsing_error'`: `BaseException | None`
Example: Pydantic schema (`include_raw=False`):
???+ example "Pydantic schema (`include_raw=False`)"
```python
from pydantic import BaseModel
```python
from pydantic import BaseModel
class AnswerWithJustification(BaseModel):
'''An answer to the user question along with justification for the answer.'''
class AnswerWithJustification(BaseModel):
'''An answer to the user question along with justification for the answer.'''
answer: str
justification: str
answer: str
justification: str
model = ChatModel(model="model-name", temperature=0)
structured_model = model.with_structured_output(AnswerWithJustification)
model = ChatModel(model="model-name", temperature=0)
structured_model = model.with_structured_output(AnswerWithJustification)
structured_model.invoke(
"What weighs more a pound of bricks or a pound of feathers"
)
structured_model.invoke(
"What weighs more a pound of bricks or a pound of feathers"
)
# -> AnswerWithJustification(
# answer='They weigh the same',
# justification='Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ.'
# )
```
# -> AnswerWithJustification(
# answer='They weigh the same',
# justification='Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ.'
# )
```
Example: Pydantic schema (`include_raw=True`):
??? example "Pydantic schema (`include_raw=True`)"
```python
from pydantic import BaseModel
```python
from pydantic import BaseModel
class AnswerWithJustification(BaseModel):
'''An answer to the user question along with justification for the answer.'''
class AnswerWithJustification(BaseModel):
'''An answer to the user question along with justification for the answer.'''
answer: str
justification: str
answer: str
justification: str
model = ChatModel(model="model-name", temperature=0)
structured_model = model.with_structured_output(
AnswerWithJustification, include_raw=True
)
model = ChatModel(model="model-name", temperature=0)
structured_model = model.with_structured_output(
AnswerWithJustification, include_raw=True
)
structured_model.invoke(
"What weighs more a pound of bricks or a pound of feathers"
)
# -> {
# 'raw': AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_Ao02pnFYXD6GN1yzc0uXPsvF', 'function': {'arguments': '{"answer":"They weigh the same.","justification":"Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ."}', 'name': 'AnswerWithJustification'}, 'type': 'function'}]}),
# 'parsed': AnswerWithJustification(answer='They weigh the same.', justification='Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ.'),
# 'parsing_error': None
# }
```
structured_model.invoke(
"What weighs more a pound of bricks or a pound of feathers"
)
# -> {
# 'raw': AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_Ao02pnFYXD6GN1yzc0uXPsvF', 'function': {'arguments': '{"answer":"They weigh the same.","justification":"Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ."}', 'name': 'AnswerWithJustification'}, 'type': 'function'}]}),
# 'parsed': AnswerWithJustification(answer='They weigh the same.', justification='Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume or density of the objects may differ.'),
# 'parsing_error': None
# }
```
Example: Dictionary schema (`include_raw=False`):
??? example "Dictionary schema (`include_raw=False`)"
```python
from pydantic import BaseModel
from langchain_core.utils.function_calling import convert_to_openai_tool
```python
from pydantic import BaseModel
from langchain_core.utils.function_calling import convert_to_openai_tool
class AnswerWithJustification(BaseModel):
'''An answer to the user question along with justification for the answer.'''
class AnswerWithJustification(BaseModel):
'''An answer to the user question along with justification for the answer.'''
answer: str
justification: str
answer: str
justification: str
dict_schema = convert_to_openai_tool(AnswerWithJustification)
model = ChatModel(model="model-name", temperature=0)
structured_model = model.with_structured_output(dict_schema)
dict_schema = convert_to_openai_tool(AnswerWithJustification)
model = ChatModel(model="model-name", temperature=0)
structured_model = model.with_structured_output(dict_schema)
structured_model.invoke(
"What weighs more a pound of bricks or a pound of feathers"
)
# -> {
# 'answer': 'They weigh the same',
# 'justification': 'Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume and density of the two substances differ.'
# }
```
structured_model.invoke(
"What weighs more a pound of bricks or a pound of feathers"
)
# -> {
# 'answer': 'They weigh the same',
# 'justification': 'Both a pound of bricks and a pound of feathers weigh one pound. The weight is the same, but the volume and density of the two substances differ.'
# }
```
!!! warning "Behavior changed in `langchain-core` 0.2.26"

View File

@@ -61,6 +61,8 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
_background_tasks: set[asyncio.Task] = set()
@functools.lru_cache
def _log_error_once(msg: str) -> None:
@@ -100,9 +102,9 @@ def create_base_retry_decorator(
asyncio.run(coro)
else:
if loop.is_running():
# TODO: Fix RUF006 - this task should have a reference
# and be awaited somewhere
loop.create_task(coro) # noqa: RUF006
task = loop.create_task(coro)
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)
else:
asyncio.run(coro)
except Exception as e:
@@ -299,7 +301,10 @@ class BaseLLM(BaseLanguageModel[str], ABC):
@functools.cached_property
def _serialized(self) -> dict[str, Any]:
return dumpd(self)
# self is always a Serializable object in this case, thus the result is
# guaranteed to be a dict since dumps uses the default callback, which uses
# obj.to_json which always returns TypedDict subclasses
return cast("dict[str, Any]", dumpd(self))
# --- Runnable methods ---

View File

@@ -7,6 +7,7 @@ class ModelProfile(TypedDict, total=False):
"""Model profile.
!!! warning "Beta feature"
This is a beta feature. The format of model profiles is subject to change.
Provides information about chat model capabilities, such as context window sizes

View File

@@ -6,7 +6,7 @@ from langchain_core._import_utils import import_attr
if TYPE_CHECKING:
from langchain_core.load.dump import dumpd, dumps
from langchain_core.load.load import loads
from langchain_core.load.load import InitValidator, loads
from langchain_core.load.serializable import Serializable
# Unfortunately, we have to eagerly import load from langchain_core/load/load.py
@@ -15,11 +15,19 @@ if TYPE_CHECKING:
# the `from langchain_core.load.load import load` absolute import should also work.
from langchain_core.load.load import load
__all__ = ("Serializable", "dumpd", "dumps", "load", "loads")
__all__ = (
"InitValidator",
"Serializable",
"dumpd",
"dumps",
"load",
"loads",
)
_dynamic_imports = {
"dumpd": "dump",
"dumps": "dump",
"InitValidator": "load",
"loads": "load",
"Serializable": "serializable",
}

View File

@@ -0,0 +1,174 @@
"""Validation utilities for LangChain serialization.
Provides escape-based protection against injection attacks in serialized objects. The
approach uses an allowlist design: only dicts explicitly produced by
`Serializable.to_json()` are treated as LC objects during deserialization.
## How escaping works
During serialization, plain dicts (user data) that contain an `'lc'` key are wrapped:
```python
{"lc": 1, ...} # user data that looks like LC object
# becomes:
{"__lc_escaped__": {"lc": 1, ...}}
```
During deserialization, escaped dicts are unwrapped and returned as plain dicts,
NOT instantiated as LC objects.
"""
from typing import Any
from langchain_core.load.serializable import (
Serializable,
to_json_not_implemented,
)
_LC_ESCAPED_KEY = "__lc_escaped__"
"""Sentinel key used to mark escaped user dicts during serialization.
When a plain dict contains 'lc' key (which could be confused with LC objects),
we wrap it as {"__lc_escaped__": {...original...}}.
"""
def _needs_escaping(obj: dict[str, Any]) -> bool:
"""Check if a dict needs escaping to prevent confusion with LC objects.
A dict needs escaping if:
1. It has an `'lc'` key (could be confused with LC serialization format)
2. It has only the escape key (would be mistaken for an escaped dict)
"""
return "lc" in obj or (len(obj) == 1 and _LC_ESCAPED_KEY in obj)
def _escape_dict(obj: dict[str, Any]) -> dict[str, Any]:
"""Wrap a dict in the escape marker.
Example:
```python
{"key": "value"} # becomes {"__lc_escaped__": {"key": "value"}}
```
"""
return {_LC_ESCAPED_KEY: obj}
def _is_escaped_dict(obj: dict[str, Any]) -> bool:
"""Check if a dict is an escaped user dict.
Example:
```python
{"__lc_escaped__": {...}} # is an escaped dict
```
"""
return len(obj) == 1 and _LC_ESCAPED_KEY in obj
def _serialize_value(obj: Any) -> Any:
"""Serialize a value with escaping of user dicts.
Called recursively on kwarg values to escape any plain dicts that could be confused
with LC objects.
Args:
obj: The value to serialize.
Returns:
The serialized value with user dicts escaped as needed.
"""
if isinstance(obj, Serializable):
# This is an LC object - serialize it properly (not escaped)
return _serialize_lc_object(obj)
if isinstance(obj, dict):
if not all(isinstance(k, (str, int, float, bool, type(None))) for k in obj):
# if keys are not json serializable
return to_json_not_implemented(obj)
# Check if dict needs escaping BEFORE recursing into values.
# If it needs escaping, wrap it as-is - the contents are user data that
# will be returned as-is during deserialization (no instantiation).
# This prevents re-escaping of already-escaped nested content.
if _needs_escaping(obj):
return _escape_dict(obj)
# Safe dict (no 'lc' key) - recurse into values
return {k: _serialize_value(v) for k, v in obj.items()}
if isinstance(obj, (list, tuple)):
return [_serialize_value(item) for item in obj]
if isinstance(obj, (str, int, float, bool, type(None))):
return obj
# Non-JSON-serializable object (datetime, custom objects, etc.)
return to_json_not_implemented(obj)
def _is_lc_secret(obj: Any) -> bool:
"""Check if an object is a LangChain secret marker."""
expected_num_keys = 3
return (
isinstance(obj, dict)
and obj.get("lc") == 1
and obj.get("type") == "secret"
and "id" in obj
and len(obj) == expected_num_keys
)
def _serialize_lc_object(obj: Any) -> dict[str, Any]:
"""Serialize a `Serializable` object with escaping of user data in kwargs.
Args:
obj: The `Serializable` object to serialize.
Returns:
The serialized dict with user data in kwargs escaped as needed.
Note:
Kwargs values are processed with `_serialize_value` to escape user data (like
metadata) that contains `'lc'` keys. Secret fields (from `lc_secrets`) are
skipped because `to_json()` replaces their values with secret markers.
"""
if not isinstance(obj, Serializable):
msg = f"Expected Serializable, got {type(obj)}"
raise TypeError(msg)
serialized: dict[str, Any] = dict(obj.to_json())
# Process kwargs to escape user data that could be confused with LC objects
# Skip secret fields - to_json() already converted them to secret markers
if serialized.get("type") == "constructor" and "kwargs" in serialized:
serialized["kwargs"] = {
k: v if _is_lc_secret(v) else _serialize_value(v)
for k, v in serialized["kwargs"].items()
}
return serialized
def _unescape_value(obj: Any) -> Any:
"""Unescape a value, processing escape markers in dict values and lists.
When an escaped dict is encountered (`{"__lc_escaped__": ...}`), it's
unwrapped and the contents are returned AS-IS (no further processing).
The contents represent user data that should not be modified.
For regular dicts and lists, we recurse to find any nested escape markers.
Args:
obj: The value to unescape.
Returns:
The unescaped value.
"""
if isinstance(obj, dict):
if _is_escaped_dict(obj):
# Unwrap and return the user data as-is (no further unescaping).
# The contents are user data that may contain more escape keys,
# but those are part of the user's actual data.
return obj[_LC_ESCAPED_KEY]
# Regular dict - recurse into values to find nested escape markers
return {k: _unescape_value(v) for k, v in obj.items()}
if isinstance(obj, list):
return [_unescape_value(item) for item in obj]
return obj

View File

@@ -1,10 +1,26 @@
"""Dump objects to json."""
"""Serialize LangChain objects to JSON.
Provides `dumps` (to JSON string) and `dumpd` (to dict) for serializing
`Serializable` objects.
## Escaping
During serialization, plain dicts (user data) that contain an `'lc'` key are escaped
by wrapping them: `{"__lc_escaped__": {...original...}}`. This prevents injection
attacks where malicious data could trick the deserializer into instantiating
arbitrary classes. The escape marker is removed during deserialization.
This is an allowlist approach: only dicts explicitly produced by
`Serializable.to_json()` are treated as LC objects; everything else is escaped if it
could be confused with the LC format.
"""
import json
from typing import Any
from pydantic import BaseModel
from langchain_core.load._validation import _serialize_value
from langchain_core.load.serializable import Serializable, to_json_not_implemented
from langchain_core.messages import AIMessage
from langchain_core.outputs import ChatGeneration
@@ -25,6 +41,20 @@ def default(obj: Any) -> Any:
def _dump_pydantic_models(obj: Any) -> Any:
"""Convert nested Pydantic models to dicts for JSON serialization.
Handles the special case where a `ChatGeneration` contains an `AIMessage`
with a parsed Pydantic model in `additional_kwargs["parsed"]`. Since
Pydantic models aren't directly JSON serializable, this converts them to
dicts.
Args:
obj: The object to process.
Returns:
A copy of the object with nested Pydantic models converted to dicts, or
the original object unchanged if no conversion was needed.
"""
if (
isinstance(obj, ChatGeneration)
and isinstance(obj.message, AIMessage)
@@ -40,10 +70,17 @@ def _dump_pydantic_models(obj: Any) -> Any:
def dumps(obj: Any, *, pretty: bool = False, **kwargs: Any) -> str:
"""Return a JSON string representation of an object.
Note:
Plain dicts containing an `'lc'` key are automatically escaped to prevent
confusion with LC serialization format. The escape marker is removed during
deserialization.
Args:
obj: The object to dump.
pretty: Whether to pretty print the json. If `True`, the json will be
indented with 2 spaces (if no indent is provided as part of `kwargs`).
pretty: Whether to pretty print the json.
If `True`, the json will be indented by either 2 spaces or the amount
provided in the `indent` kwarg.
**kwargs: Additional arguments to pass to `json.dumps`
Returns:
@@ -55,28 +92,29 @@ def dumps(obj: Any, *, pretty: bool = False, **kwargs: Any) -> str:
if "default" in kwargs:
msg = "`default` should not be passed to dumps"
raise ValueError(msg)
try:
obj = _dump_pydantic_models(obj)
if pretty:
indent = kwargs.pop("indent", 2)
return json.dumps(obj, default=default, indent=indent, **kwargs)
return json.dumps(obj, default=default, **kwargs)
except TypeError:
if pretty:
indent = kwargs.pop("indent", 2)
return json.dumps(to_json_not_implemented(obj), indent=indent, **kwargs)
return json.dumps(to_json_not_implemented(obj), **kwargs)
obj = _dump_pydantic_models(obj)
serialized = _serialize_value(obj)
if pretty:
indent = kwargs.pop("indent", 2)
return json.dumps(serialized, indent=indent, **kwargs)
return json.dumps(serialized, **kwargs)
def dumpd(obj: Any) -> Any:
"""Return a dict representation of an object.
Note:
Plain dicts containing an `'lc'` key are automatically escaped to prevent
confusion with LC serialization format. The escape marker is removed during
deserialization.
Args:
obj: The object to dump.
Returns:
Dictionary that can be serialized to json using `json.dumps`.
"""
# Unfortunately this function is not as efficient as it could be because it first
# dumps the object to a json string and then loads it back into a dictionary.
return json.loads(dumps(obj))
obj = _dump_pydantic_models(obj)
return _serialize_value(obj)

View File

@@ -1,11 +1,83 @@
"""Load LangChain objects from JSON strings or objects."""
"""Load LangChain objects from JSON strings or objects.
## How it works
Each `Serializable` LangChain object has a unique identifier (its "class path"), which
is a list of strings representing the module path and class name. For example:
- `AIMessage` -> `["langchain_core", "messages", "ai", "AIMessage"]`
- `ChatPromptTemplate` -> `["langchain_core", "prompts", "chat", "ChatPromptTemplate"]`
When deserializing, the class path from the JSON `'id'` field is checked against an
allowlist. If the class is not in the allowlist, deserialization raises a `ValueError`.
## Security model
The `allowed_objects` parameter controls which classes can be deserialized:
- **`'core'` (default)**: Allow classes defined in the serialization mappings for
langchain_core.
- **`'all'`**: Allow classes defined in the serialization mappings. This
includes core LangChain types (messages, prompts, documents, etc.) and trusted
partner integrations. See `langchain_core.load.mapping` for the full list.
- **Explicit list of classes**: Only those specific classes are allowed.
For simple data types like messages and documents, the default allowlist is safe to use.
These classes do not perform side effects during initialization.
!!! note "Side effects in allowed classes"
Deserialization calls `__init__` on allowed classes. If those classes perform side
effects during initialization (network calls, file operations, etc.), those side
effects will occur. The allowlist prevents instantiation of classes outside the
allowlist, but does not sandbox the allowed classes themselves.
Import paths are also validated against trusted namespaces before any module is
imported.
### Injection protection (escape-based)
During serialization, plain dicts that contain an `'lc'` key are escaped by wrapping
them: `{"__lc_escaped__": {...}}`. During deserialization, escaped dicts are unwrapped
and returned as plain dicts, NOT instantiated as LC objects.
This is an allowlist approach: only dicts explicitly produced by
`Serializable.to_json()` (which are NOT escaped) are treated as LC objects;
everything else is user data.
Even if an attacker's payload includes `__lc_escaped__` wrappers, it will be unwrapped
to plain dicts and NOT instantiated as malicious objects.
## Examples
```python
from langchain_core.load import load
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import AIMessage, HumanMessage
# Use default allowlist (classes from mappings) - recommended
obj = load(data)
# Allow only specific classes (most restrictive)
obj = load(
data,
allowed_objects=[
ChatPromptTemplate,
AIMessage,
HumanMessage,
],
)
```
"""
import importlib
import json
import os
from typing import Any
from collections.abc import Callable, Iterable
from typing import Any, Literal, cast
from langchain_core._api import beta
from langchain_core.load._validation import _is_escaped_dict, _unescape_value
from langchain_core.load.mapping import (
_JS_SERIALIZABLE_MAPPING,
_OG_SERIALIZABLE_MAPPING,
@@ -44,34 +116,209 @@ ALL_SERIALIZABLE_MAPPINGS = {
**_JS_SERIALIZABLE_MAPPING,
}
# Cache for the default allowed class paths computed from mappings
# Maps mode ("all" or "core") to the cached set of paths
_default_class_paths_cache: dict[str, set[tuple[str, ...]]] = {}
def _get_default_allowed_class_paths(
allowed_object_mode: Literal["all", "core"],
) -> set[tuple[str, ...]]:
"""Get the default allowed class paths from the serialization mappings.
This uses the mappings as the source of truth for what classes are allowed
by default. Both the legacy paths (keys) and current paths (values) are included.
Args:
allowed_object_mode: either `'all'` or `'core'`.
Returns:
Set of class path tuples that are allowed by default.
"""
if allowed_object_mode in _default_class_paths_cache:
return _default_class_paths_cache[allowed_object_mode]
allowed_paths: set[tuple[str, ...]] = set()
for key, value in ALL_SERIALIZABLE_MAPPINGS.items():
if allowed_object_mode == "core" and value[0] != "langchain_core":
continue
allowed_paths.add(key)
allowed_paths.add(value)
_default_class_paths_cache[allowed_object_mode] = allowed_paths
return _default_class_paths_cache[allowed_object_mode]
def _block_jinja2_templates(
class_path: tuple[str, ...],
kwargs: dict[str, Any],
) -> None:
"""Block jinja2 templates during deserialization for security.
Jinja2 templates can execute arbitrary code, so they are blocked by default when
deserializing objects with `template_format='jinja2'`.
Note:
We intentionally do NOT check the `class_path` here to keep this simple and
future-proof. If any new class is added that accepts `template_format='jinja2'`,
it will be automatically blocked without needing to update this function.
Args:
class_path: The class path tuple being deserialized (unused).
kwargs: The kwargs dict for the class constructor.
Raises:
ValueError: If `template_format` is `'jinja2'`.
"""
_ = class_path # Unused - see docstring for rationale. Kept to satisfy signature.
if kwargs.get("template_format") == "jinja2":
msg = (
"Jinja2 templates are not allowed during deserialization for security "
"reasons. Use 'f-string' template format instead, or explicitly allow "
"jinja2 by providing a custom init_validator."
)
raise ValueError(msg)
def default_init_validator(
class_path: tuple[str, ...],
kwargs: dict[str, Any],
) -> None:
"""Default init validator that blocks jinja2 templates.
This is the default validator used by `load()` and `loads()` when no custom
validator is provided.
Args:
class_path: The class path tuple being deserialized.
kwargs: The kwargs dict for the class constructor.
Raises:
ValueError: If template_format is `'jinja2'`.
"""
_block_jinja2_templates(class_path, kwargs)
AllowedObject = type[Serializable]
"""Type alias for classes that can be included in the `allowed_objects` parameter.
Must be a `Serializable` subclass (the class itself, not an instance).
"""
InitValidator = Callable[[tuple[str, ...], dict[str, Any]], None]
"""Type alias for a callable that validates kwargs during deserialization.
The callable receives:
- `class_path`: A tuple of strings identifying the class being instantiated
(e.g., `('langchain', 'schema', 'messages', 'AIMessage')`).
- `kwargs`: The kwargs dict that will be passed to the constructor.
The validator should raise an exception if the object should not be deserialized.
"""
def _compute_allowed_class_paths(
allowed_objects: Iterable[AllowedObject],
import_mappings: dict[tuple[str, ...], tuple[str, ...]],
) -> set[tuple[str, ...]]:
"""Return allowed class paths from an explicit list of classes.
A class path is a tuple of strings identifying a serializable class, derived from
`Serializable.lc_id()`. For example: `('langchain_core', 'messages', 'AIMessage')`.
Args:
allowed_objects: Iterable of `Serializable` subclasses to allow.
import_mappings: Mapping of legacy class paths to current class paths.
Returns:
Set of allowed class paths.
Example:
```python
# Allow a specific class
_compute_allowed_class_paths([MyPrompt], {}) ->
{("langchain_core", "prompts", "MyPrompt")}
# Include legacy paths that map to the same class
import_mappings = {("old", "Prompt"): ("langchain_core", "prompts", "MyPrompt")}
_compute_allowed_class_paths([MyPrompt], import_mappings) ->
{("langchain_core", "prompts", "MyPrompt"), ("old", "Prompt")}
```
"""
allowed_objects_list = list(allowed_objects)
allowed_class_paths: set[tuple[str, ...]] = set()
for allowed_obj in allowed_objects_list:
if not isinstance(allowed_obj, type) or not issubclass(
allowed_obj, Serializable
):
msg = "allowed_objects must contain Serializable subclasses."
raise TypeError(msg)
class_path = tuple(allowed_obj.lc_id())
allowed_class_paths.add(class_path)
# Add legacy paths that map to the same class.
for mapping_key, mapping_value in import_mappings.items():
if tuple(mapping_value) == class_path:
allowed_class_paths.add(mapping_key)
return allowed_class_paths
class Reviver:
"""Reviver for JSON objects."""
"""Reviver for JSON objects.
Used as the `object_hook` for `json.loads` to reconstruct LangChain objects from
their serialized JSON representation.
Only classes in the allowlist can be instantiated.
"""
def __init__(
self,
allowed_objects: Iterable[AllowedObject] | Literal["all", "core"] = "core",
secrets_map: dict[str, str] | None = None,
valid_namespaces: list[str] | None = None,
secrets_from_env: bool = True, # noqa: FBT001,FBT002
secrets_from_env: bool = False, # noqa: FBT001,FBT002
additional_import_mappings: dict[tuple[str, ...], tuple[str, ...]]
| None = None,
*,
ignore_unserializable_fields: bool = False,
init_validator: InitValidator | None = default_init_validator,
) -> None:
"""Initialize the reviver.
Args:
secrets_map: A map of secrets to load.
allowed_objects: Allowlist of classes that can be deserialized.
- `'core'` (default): Allow classes defined in the serialization
mappings for `langchain_core`.
- `'all'`: Allow classes defined in the serialization mappings.
This includes core LangChain types (messages, prompts, documents,
etc.) and trusted partner integrations. See
`langchain_core.load.mapping` for the full list.
- Explicit list of classes: Only those specific classes are allowed.
secrets_map: A map of secrets to load.
If a secret is not found in the map, it will be loaded from the
environment if `secrets_from_env` is `True`.
valid_namespaces: A list of additional namespaces (modules)
to allow to be deserialized.
valid_namespaces: Additional namespaces (modules) to allow during
deserialization, beyond the default trusted namespaces.
secrets_from_env: Whether to load secrets from the environment.
additional_import_mappings: A dictionary of additional namespace mappings
additional_import_mappings: A dictionary of additional namespace mappings.
You can use this to override default mappings or add new mappings.
When `allowed_objects` is `None` (using defaults), paths from these
mappings are also added to the allowed class paths.
ignore_unserializable_fields: Whether to ignore unserializable fields.
init_validator: Optional callable to validate kwargs before instantiation.
If provided, this function is called with `(class_path, kwargs)` where
`class_path` is the class path tuple and `kwargs` is the kwargs dict.
The validator should raise an exception if the object should not be
deserialized, otherwise return `None`.
Defaults to `default_init_validator` which blocks jinja2 templates.
"""
self.secrets_from_env = secrets_from_env
self.secrets_map = secrets_map or {}
@@ -90,7 +337,26 @@ class Reviver:
if self.additional_import_mappings
else ALL_SERIALIZABLE_MAPPINGS
)
# Compute allowed class paths:
# - "all" -> use default paths from mappings (+ additional_import_mappings)
# - Explicit list -> compute from those classes
if allowed_objects in ("all", "core"):
self.allowed_class_paths: set[tuple[str, ...]] | None = (
_get_default_allowed_class_paths(
cast("Literal['all', 'core']", allowed_objects)
).copy()
)
# Add paths from additional_import_mappings to the defaults
if self.additional_import_mappings:
for key, value in self.additional_import_mappings.items():
self.allowed_class_paths.add(key)
self.allowed_class_paths.add(value)
else:
self.allowed_class_paths = _compute_allowed_class_paths(
cast("Iterable[AllowedObject]", allowed_objects), self.import_mappings
)
self.ignore_unserializable_fields = ignore_unserializable_fields
self.init_validator = init_validator
def __call__(self, value: dict[str, Any]) -> Any:
"""Revive the value.
@@ -141,6 +407,20 @@ class Reviver:
[*namespace, name] = value["id"]
mapping_key = tuple(value["id"])
if (
self.allowed_class_paths is not None
and mapping_key not in self.allowed_class_paths
):
msg = (
f"Deserialization of {mapping_key!r} is not allowed. "
"The default (allowed_objects='core') only permits core "
"langchain-core classes. To allow trusted partner integrations, "
"use allowed_objects='all'. Alternatively, pass an explicit list "
"of allowed classes via allowed_objects=[...]. "
"See langchain_core.load.mapping for the full allowlist."
)
raise ValueError(msg)
if (
namespace[0] not in self.valid_namespaces
# The root namespace ["langchain"] is not a valid identifier.
@@ -148,13 +428,11 @@ class Reviver:
):
msg = f"Invalid namespace: {value}"
raise ValueError(msg)
# Has explicit import path.
# Determine explicit import path
if mapping_key in self.import_mappings:
import_path = self.import_mappings[mapping_key]
# Split into module and name
import_dir, name = import_path[:-1], import_path[-1]
# Import module
mod = importlib.import_module(".".join(import_dir))
elif namespace[0] in DISALLOW_LOAD_FROM_PATH:
msg = (
"Trying to deserialize something that cannot "
@@ -162,9 +440,16 @@ class Reviver:
f"{mapping_key}."
)
raise ValueError(msg)
# Otherwise, treat namespace as path.
else:
mod = importlib.import_module(".".join(namespace))
# Otherwise, treat namespace as path.
import_dir = namespace
# Validate import path is in trusted namespaces before importing
if import_dir[0] not in self.valid_namespaces:
msg = f"Invalid namespace: {value}"
raise ValueError(msg)
mod = importlib.import_module(".".join(import_dir))
cls = getattr(mod, name)
@@ -176,6 +461,10 @@ class Reviver:
# We don't need to recurse on kwargs
# as json.loads will do that for us.
kwargs = value.get("kwargs", {})
if self.init_validator is not None:
self.init_validator(mapping_key, kwargs)
return cls(**kwargs)
return value
@@ -185,42 +474,81 @@ class Reviver:
def loads(
text: str,
*,
allowed_objects: Iterable[AllowedObject] | Literal["all", "core"] = "core",
secrets_map: dict[str, str] | None = None,
valid_namespaces: list[str] | None = None,
secrets_from_env: bool = True,
secrets_from_env: bool = False,
additional_import_mappings: dict[tuple[str, ...], tuple[str, ...]] | None = None,
ignore_unserializable_fields: bool = False,
init_validator: InitValidator | None = default_init_validator,
) -> Any:
"""Revive a LangChain class from a JSON string.
Equivalent to `load(json.loads(text))`.
Only classes in the allowlist can be instantiated. The default allowlist includes
core LangChain types (messages, prompts, documents, etc.). See
`langchain_core.load.mapping` for the full list.
!!! warning "Beta feature"
This is a beta feature. Please be wary of deploying experimental code to
production unless you've taken appropriate precautions.
Args:
text: The string to load.
allowed_objects: Allowlist of classes that can be deserialized.
- `'core'` (default): Allow classes defined in the serialization mappings
for `langchain_core`.
- `'all'`: Allow classes defined in the serialization mappings.
This includes core LangChain types (messages, prompts, documents, etc.)
and trusted partner integrations. See `langchain_core.load.mapping` for
the full list.
- Explicit list of classes: Only those specific classes are allowed.
- `[]`: Disallow all deserialization (will raise on any object).
secrets_map: A map of secrets to load.
If a secret is not found in the map, it will be loaded from the environment
if `secrets_from_env` is `True`.
valid_namespaces: A list of additional namespaces (modules)
to allow to be deserialized.
valid_namespaces: Additional namespaces (modules) to allow during
deserialization, beyond the default trusted namespaces.
secrets_from_env: Whether to load secrets from the environment.
additional_import_mappings: A dictionary of additional namespace mappings
additional_import_mappings: A dictionary of additional namespace mappings.
You can use this to override default mappings or add new mappings.
When `allowed_objects` is `None` (using defaults), paths from these
mappings are also added to the allowed class paths.
ignore_unserializable_fields: Whether to ignore unserializable fields.
init_validator: Optional callable to validate kwargs before instantiation.
If provided, this function is called with `(class_path, kwargs)` where
`class_path` is the class path tuple and `kwargs` is the kwargs dict.
The validator should raise an exception if the object should not be
deserialized, otherwise return `None`.
Defaults to `default_init_validator` which blocks jinja2 templates.
Returns:
Revived LangChain objects.
Raises:
ValueError: If an object's class path is not in the `allowed_objects` allowlist.
"""
return json.loads(
text,
object_hook=Reviver(
secrets_map,
valid_namespaces,
secrets_from_env,
additional_import_mappings,
ignore_unserializable_fields=ignore_unserializable_fields,
),
# Parse JSON and delegate to load() for proper escape handling
raw_obj = json.loads(text)
return load(
raw_obj,
allowed_objects=allowed_objects,
secrets_map=secrets_map,
valid_namespaces=valid_namespaces,
secrets_from_env=secrets_from_env,
additional_import_mappings=additional_import_mappings,
ignore_unserializable_fields=ignore_unserializable_fields,
init_validator=init_validator,
)
@@ -228,45 +556,112 @@ def loads(
def load(
obj: Any,
*,
allowed_objects: Iterable[AllowedObject] | Literal["all", "core"] = "core",
secrets_map: dict[str, str] | None = None,
valid_namespaces: list[str] | None = None,
secrets_from_env: bool = True,
secrets_from_env: bool = False,
additional_import_mappings: dict[tuple[str, ...], tuple[str, ...]] | None = None,
ignore_unserializable_fields: bool = False,
init_validator: InitValidator | None = default_init_validator,
) -> Any:
"""Revive a LangChain class from a JSON object.
Use this if you already have a parsed JSON object,
eg. from `json.load` or `orjson.loads`.
Use this if you already have a parsed JSON object, eg. from `json.load` or
`orjson.loads`.
Only classes in the allowlist can be instantiated. The default allowlist includes
core LangChain types (messages, prompts, documents, etc.). See
`langchain_core.load.mapping` for the full list.
!!! warning "Beta feature"
This is a beta feature. Please be wary of deploying experimental code to
production unless you've taken appropriate precautions.
Args:
obj: The object to load.
allowed_objects: Allowlist of classes that can be deserialized.
- `'core'` (default): Allow classes defined in the serialization mappings
for `langchain_core`.
- `'all'`: Allow classes defined in the serialization mappings.
This includes core LangChain types (messages, prompts, documents, etc.)
and trusted partner integrations. See `langchain_core.load.mapping` for
the full list.
- Explicit list of classes: Only those specific classes are allowed.
- `[]`: Disallow all deserialization (will raise on any object).
secrets_map: A map of secrets to load.
If a secret is not found in the map, it will be loaded from the environment
if `secrets_from_env` is `True`.
valid_namespaces: A list of additional namespaces (modules)
to allow to be deserialized.
valid_namespaces: Additional namespaces (modules) to allow during
deserialization, beyond the default trusted namespaces.
secrets_from_env: Whether to load secrets from the environment.
additional_import_mappings: A dictionary of additional namespace mappings
additional_import_mappings: A dictionary of additional namespace mappings.
You can use this to override default mappings or add new mappings.
When `allowed_objects` is `None` (using defaults), paths from these
mappings are also added to the allowed class paths.
ignore_unserializable_fields: Whether to ignore unserializable fields.
init_validator: Optional callable to validate kwargs before instantiation.
If provided, this function is called with `(class_path, kwargs)` where
`class_path` is the class path tuple and `kwargs` is the kwargs dict.
The validator should raise an exception if the object should not be
deserialized, otherwise return `None`.
Defaults to `default_init_validator` which blocks jinja2 templates.
Returns:
Revived LangChain objects.
Raises:
ValueError: If an object's class path is not in the `allowed_objects` allowlist.
Example:
```python
from langchain_core.load import load, dumpd
from langchain_core.messages import AIMessage
msg = AIMessage(content="Hello")
data = dumpd(msg)
# Deserialize using default allowlist
loaded = load(data)
# Or with explicit allowlist
loaded = load(data, allowed_objects=[AIMessage])
# Or extend defaults with additional mappings
loaded = load(
data,
additional_import_mappings={
("my_pkg", "MyClass"): ("my_pkg", "module", "MyClass"),
},
)
```
"""
reviver = Reviver(
allowed_objects,
secrets_map,
valid_namespaces,
secrets_from_env,
additional_import_mappings,
ignore_unserializable_fields=ignore_unserializable_fields,
init_validator=init_validator,
)
def _load(obj: Any) -> Any:
if isinstance(obj, dict):
# Need to revive leaf nodes before reviving this node
# Check for escaped dict FIRST (before recursing).
# Escaped dicts are user data that should NOT be processed as LC objects.
if _is_escaped_dict(obj):
return _unescape_value(obj)
# Not escaped - recurse into children then apply reviver
loaded_obj = {k: _load(v) for k, v in obj.items()}
return reviver(loaded_obj)
if isinstance(obj, list):

View File

@@ -1,21 +1,19 @@
"""Serialization mapping.
This file contains a mapping between the lc_namespace path for a given
subclass that implements from Serializable to the namespace
This file contains a mapping between the `lc_namespace` path for a given
subclass that implements from `Serializable` to the namespace
where that class is actually located.
This mapping helps maintain the ability to serialize and deserialize
well-known LangChain objects even if they are moved around in the codebase
across different LangChain versions.
For example,
For example, the code for the `AIMessage` class is located in
`langchain_core.messages.ai.AIMessage`. This message is associated with the
`lc_namespace` of `["langchain", "schema", "messages", "AIMessage"]`,
because this code was originally in `langchain.schema.messages.AIMessage`.
The code for AIMessage class is located in langchain_core.messages.ai.AIMessage,
This message is associated with the lc_namespace
["langchain", "schema", "messages", "AIMessage"],
because this code was originally in langchain.schema.messages.AIMessage.
The mapping allows us to deserialize an AIMessage created with an older
The mapping allows us to deserialize an `AIMessage` created with an older
version of LangChain where the code was in a different location.
"""
@@ -275,6 +273,11 @@ SERIALIZABLE_MAPPING: dict[tuple[str, ...], tuple[str, ...]] = {
"chat_models",
"ChatGroq",
),
("langchain_xai", "chat_models", "ChatXAI"): (
"langchain_xai",
"chat_models",
"ChatXAI",
),
("langchain", "chat_models", "fireworks", "ChatFireworks"): (
"langchain_fireworks",
"chat_models",
@@ -529,16 +532,6 @@ SERIALIZABLE_MAPPING: dict[tuple[str, ...], tuple[str, ...]] = {
"structured",
"StructuredPrompt",
),
("langchain_sambanova", "chat_models", "ChatSambaNovaCloud"): (
"langchain_sambanova",
"chat_models",
"ChatSambaNovaCloud",
),
("langchain_sambanova", "chat_models", "ChatSambaStudio"): (
"langchain_sambanova",
"chat_models",
"ChatSambaStudio",
),
("langchain_core", "prompts", "message", "_DictMessagePromptTemplate"): (
"langchain_core",
"prompts",

View File

@@ -92,11 +92,12 @@ class Serializable(BaseModel, ABC):
It relies on the following methods and properties:
- `is_lc_serializable`: Is this class serializable?
- [`is_lc_serializable`][langchain_core.load.serializable.Serializable.is_lc_serializable]: Is this class serializable?
By design, even if a class inherits from `Serializable`, it is not serializable
by default. This is to prevent accidental serialization of objects that should
not be serialized.
- `get_lc_namespace`: Get the namespace of the LangChain object.
- [`get_lc_namespace`][langchain_core.load.serializable.Serializable.get_lc_namespace]: Get the namespace of the LangChain object.
During deserialization, this namespace is used to identify
the correct class to instantiate.
@@ -105,10 +106,10 @@ class Serializable(BaseModel, ABC):
During deserialization an additional mapping is handle classes that have moved
or been renamed across package versions.
- `lc_secrets`: A map of constructor argument names to secret ids.
- `lc_attributes`: List of additional attribute names that should be included
- [`lc_secrets`][langchain_core.load.serializable.Serializable.lc_secrets]: A map of constructor argument names to secret ids.
- [`lc_attributes`][langchain_core.load.serializable.Serializable.lc_attributes]: List of additional attribute names that should be included
as part of the serialized representation.
"""
""" # noqa: E501
# Remove default BaseModel init docstring.
def __init__(self, *args: Any, **kwargs: Any) -> None:
@@ -132,8 +133,9 @@ class Serializable(BaseModel, ABC):
def get_lc_namespace(cls) -> list[str]:
"""Get the namespace of the LangChain object.
For example, if the class is `langchain.llms.openai.OpenAI`, then the
namespace is `["langchain", "llms", "openai"]`
For example, if the class is
[`langchain.llms.openai.OpenAI`][langchain_openai.OpenAI], then the namespace is
`["langchain", "llms", "openai"]`
Returns:
The namespace.

View File

@@ -1,12 +1,13 @@
"""AI message."""
import itertools
import json
import logging
import operator
from collections.abc import Sequence
from typing import Any, Literal, cast, overload
from pydantic import model_validator
from pydantic import Field, model_validator
from typing_extensions import NotRequired, Self, TypedDict, override
from langchain_core.messages import content as types
@@ -51,22 +52,22 @@ class InputTokenDetails(TypedDict, total=False):
May also hold extra provider-specific keys.
!!! version-added "Added in `langchain-core` 0.3.9"
"""
audio: int
"""Audio input tokens."""
cache_creation: int
"""Input tokens that were cached and there was a cache miss.
Since there was a cache miss, the cache was created from these tokens.
"""
cache_read: int
"""Input tokens that were cached and there was a cache hit.
Since there was a cache hit, the tokens were read from the cache. More precisely,
the model state given these tokens was read from the cache.
"""
@@ -91,12 +92,12 @@ class OutputTokenDetails(TypedDict, total=False):
audio: int
"""Audio output tokens."""
reasoning: int
"""Reasoning output tokens.
Tokens generated by the model in a chain of thought process (i.e. by OpenAI's o1
models) that are not returned as part of model output.
"""
@@ -136,15 +137,19 @@ class UsageMetadata(TypedDict):
input_tokens: int
"""Count of input (or prompt) tokens. Sum of all input token types."""
output_tokens: int
"""Count of output (or completion) tokens. Sum of all output token types."""
total_tokens: int
"""Total token count. Sum of `input_tokens` + `output_tokens`."""
input_token_details: NotRequired[InputTokenDetails]
"""Breakdown of input token counts.
Does *not* need to sum to full input token count. Does *not* need to have all keys.
"""
output_token_details: NotRequired[OutputTokenDetails]
"""Breakdown of output token counts.
@@ -162,10 +167,12 @@ class AIMessage(BaseMessage):
(e.g., tool calls, usage metadata) added by the LangChain framework.
"""
tool_calls: list[ToolCall] = []
tool_calls: list[ToolCall] = Field(default_factory=list)
"""If present, tool calls associated with the message."""
invalid_tool_calls: list[InvalidToolCall] = []
invalid_tool_calls: list[InvalidToolCall] = Field(default_factory=list)
"""If present, tool calls with parsing errors associated with the message."""
usage_metadata: UsageMetadata | None = None
"""If present, usage metadata for a message, such as token counts.
@@ -320,7 +327,7 @@ class AIMessage(BaseMessage):
if tool_calls := values.get("tool_calls"):
values["tool_calls"] = [
create_tool_call(
**{k: v for k, v in tc.items() if k not in ("type", "extras")}
**{k: v for k, v in tc.items() if k not in {"type", "extras"}}
)
for tc in tool_calls
]
@@ -388,7 +395,7 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
type: Literal["AIMessageChunk"] = "AIMessageChunk" # type: ignore[assignment]
"""The type of the message (used for deserialization)."""
tool_call_chunks: list[ToolCallChunk] = []
tool_call_chunks: list[ToolCallChunk] = Field(default_factory=list)
"""If provided, tool call chunks associated with the message."""
chunk_position: Literal["last"] | None = None
@@ -399,8 +406,8 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
"""
@property
@override
def lc_attributes(self) -> dict:
"""Attributes to be serialized, even if they are derived from other initialization args.""" # noqa: E501
return {
"tool_calls": self.tool_calls,
"invalid_tool_calls": self.invalid_tool_calls,
@@ -436,7 +443,7 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
blocks = [
block
for block in blocks
if block["type"] not in ("tool_call", "invalid_tool_call")
if block["type"] not in {"tool_call", "invalid_tool_call"}
]
for tool_call_chunk in self.tool_call_chunks:
tc: types.ToolCallChunk = {
@@ -557,7 +564,11 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
@model_validator(mode="after")
def init_server_tool_calls(self) -> Self:
"""Parse `server_tool_call_chunks`."""
"""Initialize server tool calls.
Parse `server_tool_call_chunks` from
[`ServerToolCallChunk`][langchain.messages.ServerToolCallChunk] objects.
"""
if (
self.chunk_position == "last"
and self.response_metadata.get("output_version") == "v1"
@@ -567,7 +578,7 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
if (
isinstance(block, dict)
and block.get("type")
in ("server_tool_call", "server_tool_call_chunk")
in {"server_tool_call", "server_tool_call_chunk"}
and (args_str := block.get("args"))
and isinstance(args_str, str)
):
@@ -645,29 +656,28 @@ def add_ai_message_chunks(
else:
usage_metadata = None
# Ranks are defined by the order of preference. Higher is better:
# 2. Provider-assigned IDs (non lc_* and non lc_run-*)
# 1. lc_run-* IDs
# 0. lc_* and other remaining IDs
best_rank = -1
chunk_id = None
candidates = [left.id] + [o.id for o in others]
# first pass: pick the first provider-assigned id (non-run-* and non-lc_*)
candidates = itertools.chain([left.id], (o.id for o in others))
for id_ in candidates:
if (
id_
and not id_.startswith(LC_ID_PREFIX)
and not id_.startswith(LC_AUTO_PREFIX)
):
if not id_:
continue
if not id_.startswith(LC_ID_PREFIX) and not id_.startswith(LC_AUTO_PREFIX):
chunk_id = id_
# Highest rank, return instantly
break
else:
# second pass: prefer lc_run-* IDs over lc_* IDs
for id_ in candidates:
if id_ and id_.startswith(LC_ID_PREFIX):
chunk_id = id_
break
else:
# third pass: take any remaining ID (auto-generated lc_* IDs)
for id_ in candidates:
if id_:
chunk_id = id_
break
rank = 1 if id_.startswith(LC_ID_PREFIX) else 0
if rank > best_rank:
best_rank = rank
chunk_id = id_
chunk_position: Literal["last"] | None = (
"last" if any(x.chunk_position == "last" for x in [left, *others]) else None

View File

@@ -8,6 +8,7 @@ from pydantic import ConfigDict, Field
from langchain_core._api.deprecation import warn_deprecated
from langchain_core.load.serializable import Serializable
from langchain_core.messages import content as types
from langchain_core.utils import get_bolded_text
from langchain_core.utils._merge import merge_dicts, merge_lists
from langchain_core.utils.interactive_env import is_interactive_env
@@ -17,7 +18,6 @@ if TYPE_CHECKING:
from typing_extensions import Self
from langchain_core.messages import content as types
from langchain_core.prompts.chat import ChatPromptTemplate
@@ -204,7 +204,6 @@ class BaseMessage(Serializable):
"""
# Needed here to avoid circular import, as these classes import BaseMessages
from langchain_core.messages import content as types # noqa: PLC0415
from langchain_core.messages.block_translators.anthropic import ( # noqa: PLC0415
_convert_to_v1_from_anthropic_input,
)
@@ -266,6 +265,9 @@ class BaseMessage(Serializable):
Can be used as both property (`message.text`) and method (`message.text()`).
Handles both string and list content types (e.g. for content blocks). Only
extracts blocks with `type: 'text'`; other block types are ignored.
!!! deprecated
As of `langchain-core` 1.0.0, calling `.text()` as a method is deprecated.
Use `.text` as a property instead. This method will be removed in 2.0.0.
@@ -277,7 +279,7 @@ class BaseMessage(Serializable):
if isinstance(self.content, str):
text_value = self.content
else:
# must be a list
# Must be a list
blocks = [
block
for block in self.content
@@ -302,7 +304,7 @@ class BaseMessage(Serializable):
from langchain_core.prompts.chat import ChatPromptTemplate # noqa: PLC0415
prompt = ChatPromptTemplate(messages=[self])
return prompt + other
return prompt.__add__(other)
def pretty_repr(
self,
@@ -391,12 +393,12 @@ class BaseMessageChunk(BaseMessage):
Raises:
TypeError: If the other object is not a message chunk.
For example,
`AIMessageChunk(content="Hello") + AIMessageChunk(content=" World")`
will give `AIMessageChunk(content="Hello World")`
Example:
```txt
AIMessageChunk(content="Hello", ...)
+ AIMessageChunk(content=" World", ...)
= AIMessageChunk(content="Hello World", ...)
```
"""
if isinstance(other, BaseMessageChunk):
# If both are (subclasses of) BaseMessageChunk,

View File

@@ -159,12 +159,12 @@ def _convert_citation_to_v1(citation: dict[str, Any]) -> types.Annotation:
return url_citation
if citation_type in (
if citation_type in {
"char_location",
"content_block_location",
"page_location",
"search_result_location",
):
}:
document_citation: types.Citation = {
"type": "citation",
"cited_text": citation["cited_text"],
@@ -173,8 +173,6 @@ def _convert_citation_to_v1(citation: dict[str, Any]) -> types.Annotation:
document_citation["title"] = citation["document_title"]
elif title := citation.get("title"):
document_citation["title"] = title
else:
pass
known_fields = {
"type",
"cited_text",
@@ -245,11 +243,20 @@ def _convert_to_v1_from_anthropic(message: AIMessage) -> list[types.ContentBlock
and message.chunk_position != "last"
):
# Isolated chunk
tool_call_chunk: types.ToolCallChunk = (
message.tool_call_chunks[0].copy() # type: ignore[assignment]
chunk = message.tool_call_chunks[0]
tool_call_chunk = types.ToolCallChunk(
name=chunk.get("name"),
id=chunk.get("id"),
args=chunk.get("args"),
type="tool_call_chunk",
)
if "type" not in tool_call_chunk:
tool_call_chunk["type"] = "tool_call_chunk"
if "caller" in block:
tool_call_chunk["extras"] = {"caller": block["caller"]}
index = chunk.get("index")
if index is not None:
tool_call_chunk["index"] = index
yield tool_call_chunk
else:
tool_call_block: types.ToolCall | None = None
@@ -271,8 +278,6 @@ def _convert_to_v1_from_anthropic(message: AIMessage) -> list[types.ContentBlock
"id": tc.get("id"),
}
break
else:
pass
if not tool_call_block:
tool_call_block = {
"type": "tool_call",
@@ -282,17 +287,27 @@ def _convert_to_v1_from_anthropic(message: AIMessage) -> list[types.ContentBlock
}
if "index" in block:
tool_call_block["index"] = block["index"]
if "caller" in block:
if "extras" not in tool_call_block:
tool_call_block["extras"] = {}
tool_call_block["extras"]["caller"] = block["caller"]
yield tool_call_block
elif block_type == "input_json_delta" and isinstance(
message, AIMessageChunk
):
if len(message.tool_call_chunks) == 1:
tool_call_chunk = (
message.tool_call_chunks[0].copy() # type: ignore[assignment]
chunk = message.tool_call_chunks[0]
tool_call_chunk = types.ToolCallChunk(
name=chunk.get("name"),
id=chunk.get("id"),
args=chunk.get("args"),
type="tool_call_chunk",
)
if "type" not in tool_call_chunk:
tool_call_chunk["type"] = "tool_call_chunk"
index = chunk.get("index")
if index is not None:
tool_call_chunk["index"] = index
yield tool_call_chunk
else:
@@ -446,12 +461,26 @@ def _convert_to_v1_from_anthropic(message: AIMessage) -> list[types.ContentBlock
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
"""Derive standard content blocks from a message with Anthropic content."""
"""Derive standard content blocks from a message with Anthropic content.
Args:
message: The message to translate.
Returns:
The derived content blocks.
"""
return _convert_to_v1_from_anthropic(message)
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
"""Derive standard content blocks from a message chunk with Anthropic content."""
"""Derive standard content blocks from a message chunk with Anthropic content.
Args:
message: The message chunk to translate.
Returns:
The derived content blocks.
"""
return _convert_to_v1_from_anthropic(message)

View File

@@ -65,14 +65,28 @@ def _convert_to_v1_from_bedrock_chunk(
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
"""Derive standard content blocks from a message with Bedrock content."""
"""Derive standard content blocks from a message with Bedrock content.
Args:
message: The message to translate.
Returns:
The derived content blocks.
"""
if "claude" not in message.response_metadata.get("model_name", "").lower():
raise NotImplementedError # fall back to best-effort parsing
return _convert_to_v1_from_bedrock(message)
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
"""Derive standard content blocks from a message chunk with Bedrock content."""
"""Derive standard content blocks from a message chunk with Bedrock content.
Args:
message: The message chunk to translate.
Returns:
The derived content blocks.
"""
# TODO: add model_name to all Bedrock chunks and update core merging logic
# to not append during aggregation. Then raise NotImplementedError here if
# not an Anthropic model to fall back to best-effort parsing.

View File

@@ -209,11 +209,16 @@ def _convert_to_v1_from_converse(message: AIMessage) -> list[types.ContentBlock]
and message.chunk_position != "last"
):
# Isolated chunk
tool_call_chunk: types.ToolCallChunk = (
message.tool_call_chunks[0].copy() # type: ignore[assignment]
chunk = message.tool_call_chunks[0]
tool_call_chunk = types.ToolCallChunk(
name=chunk.get("name"),
id=chunk.get("id"),
args=chunk.get("args"),
type="tool_call_chunk",
)
if "type" not in tool_call_chunk:
tool_call_chunk["type"] = "tool_call_chunk"
index = chunk.get("index")
if index is not None:
tool_call_chunk["index"] = index
yield tool_call_chunk
else:
tool_call_block: types.ToolCall | None = None
@@ -235,8 +240,6 @@ def _convert_to_v1_from_converse(message: AIMessage) -> list[types.ContentBlock]
"id": tc.get("id"),
}
break
else:
pass
if not tool_call_block:
tool_call_block = {
"type": "tool_call",
@@ -253,11 +256,16 @@ def _convert_to_v1_from_converse(message: AIMessage) -> list[types.ContentBlock]
and isinstance(message, AIMessageChunk)
and len(message.tool_call_chunks) == 1
):
tool_call_chunk = (
message.tool_call_chunks[0].copy() # type: ignore[assignment]
chunk = message.tool_call_chunks[0]
tool_call_chunk = types.ToolCallChunk(
name=chunk.get("name"),
id=chunk.get("id"),
args=chunk.get("args"),
type="tool_call_chunk",
)
if "type" not in tool_call_chunk:
tool_call_chunk["type"] = "tool_call_chunk"
index = chunk.get("index")
if index is not None:
tool_call_chunk["index"] = index
yield tool_call_chunk
else:
@@ -273,12 +281,26 @@ def _convert_to_v1_from_converse(message: AIMessage) -> list[types.ContentBlock]
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
"""Derive standard content blocks from a message with Bedrock Converse content."""
"""Derive standard content blocks from a message with Bedrock Converse content.
Args:
message: The message to translate.
Returns:
The derived content blocks.
"""
return _convert_to_v1_from_converse(message)
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
"""Derive standard content blocks from a chunk with Bedrock Converse content."""
"""Derive standard content blocks from a chunk with Bedrock Converse content.
Args:
message: The message chunk to translate.
Returns:
The derived content blocks.
"""
return _convert_to_v1_from_converse(message)

View File

@@ -9,6 +9,13 @@ from langchain_core.messages import AIMessage, AIMessageChunk
from langchain_core.messages import content as types
from langchain_core.messages.content import Citation, create_citation
try:
import filetype # type: ignore[import-not-found]
_HAS_FILETYPE = True
except ImportError:
_HAS_FILETYPE = False
def _bytes_to_b64_str(bytes_: bytes) -> str:
"""Convert bytes to base64 encoded string."""
@@ -76,21 +83,36 @@ def translate_grounding_metadata_to_citations(
for chunk_index in chunk_indices:
if chunk_index < len(grounding_chunks):
chunk = grounding_chunks[chunk_index]
web_info = chunk.get("web", {})
# Handle web and maps grounding
web_info = chunk.get("web") or {}
maps_info = chunk.get("maps") or {}
# Extract citation info depending on source
url = maps_info.get("uri") or web_info.get("uri")
title = maps_info.get("title") or web_info.get("title")
# Note: confidence_scores is a legacy field from Gemini 2.0 and earlier
# that indicated confidence (0.0-1.0) for each grounding chunk.
#
# In Gemini 2.5+, this field is always None/empty and should be ignored.
extras_metadata = {
"web_search_queries": web_search_queries,
"grounding_chunk_index": chunk_index,
"confidence_scores": support.get("confidence_scores") or [],
}
# Add maps-specific metadata if present
if maps_info.get("placeId"):
extras_metadata["place_id"] = maps_info["placeId"]
citation = create_citation(
url=web_info.get("uri"),
title=web_info.get("title"),
url=url,
title=title,
start_index=start_index,
end_index=end_index,
cited_text=cited_text,
extras={
"google_ai_metadata": {
"web_search_queries": web_search_queries,
"grounding_chunk_index": chunk_index,
"confidence_scores": support.get("confidence_scores", []),
}
},
google_ai_metadata=extras_metadata,
)
citations.append(citation)
@@ -376,9 +398,7 @@ def _convert_to_v1_from_genai(message: AIMessage) -> list[types.ContentBlock]:
"base64": url,
}
try:
import filetype # type: ignore[import-not-found] # noqa: PLC0415
if _HAS_FILETYPE:
# Guess MIME type based on file bytes
mime_type = None
kind = filetype.guess(decoded_bytes)
@@ -386,9 +406,6 @@ def _convert_to_v1_from_genai(message: AIMessage) -> list[types.ContentBlock]:
mime_type = kind.mime
if mime_type:
image_url_b64_block["mime_type"] = mime_type
except ImportError:
# filetype library not available, skip type detection
pass
converted_blocks.append(
cast("types.ImageContentBlock", image_url_b64_block)
@@ -396,7 +413,10 @@ def _convert_to_v1_from_genai(message: AIMessage) -> list[types.ContentBlock]:
except Exception:
# Not valid base64, treat as non-standard
converted_blocks.append(
{"type": "non_standard", "value": item}
{
"type": "non_standard",
"value": item,
}
)
else:
# This likely won't be reached according to previous implementations
@@ -508,12 +528,26 @@ def _convert_to_v1_from_genai(message: AIMessage) -> list[types.ContentBlock]:
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
"""Derive standard content blocks from a message with Google (GenAI) content."""
"""Derive standard content blocks from a message with Google (GenAI) content.
Args:
message: The message to translate.
Returns:
The derived content blocks.
"""
return _convert_to_v1_from_genai(message)
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
"""Derive standard content blocks from a chunk with Google (GenAI) content."""
"""Derive standard content blocks from a chunk with Google (GenAI) content.
Args:
message: The message chunk to translate.
Returns:
The derived content blocks.
"""
return _convert_to_v1_from_genai(message)

View File

@@ -105,26 +105,40 @@ def _convert_to_v1_from_groq(message: AIMessage) -> list[types.ContentBlock]:
if isinstance(message.content, str) and message.content:
content_blocks.append({"type": "text", "text": message.content})
for tool_call in message.tool_calls:
content_blocks.append( # noqa: PERF401
{
"type": "tool_call",
"name": tool_call["name"],
"args": tool_call["args"],
"id": tool_call.get("id"),
}
)
content_blocks.extend(
{
"type": "tool_call",
"name": tool_call["name"],
"args": tool_call["args"],
"id": tool_call.get("id"),
}
for tool_call in message.tool_calls
)
return content_blocks
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
"""Derive standard content blocks from a message with groq content."""
"""Derive standard content blocks from a message with groq content.
Args:
message: The message to translate.
Returns:
The derived content blocks.
"""
return _convert_to_v1_from_groq(message)
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
"""Derive standard content blocks from a message chunk with groq content."""
"""Derive standard content blocks from a message chunk with groq content.
Args:
message: The message chunk to translate.
Returns:
The derived content blocks.
"""
return _convert_to_v1_from_groq(message)

View File

@@ -10,16 +10,28 @@ from langchain_core.language_models._utils import (
_parse_data_uri,
is_openai_data_block,
)
from langchain_core.messages import AIMessageChunk
from langchain_core.messages import content as types
if TYPE_CHECKING:
from collections.abc import Iterable
from langchain_core.messages import AIMessage, AIMessageChunk
from langchain_core.messages import AIMessage
def convert_to_openai_image_block(block: dict[str, Any]) -> dict:
"""Convert `ImageContentBlock` to format expected by OpenAI Chat Completions."""
"""Convert `ImageContentBlock` to format expected by OpenAI Chat Completions.
Args:
block: The image content block to convert.
Raises:
ValueError: If required keys are missing.
ValueError: If source type is unsupported.
Returns:
The formatted image content block.
"""
if "url" in block:
return {
"type": "image_url",
@@ -50,6 +62,18 @@ def convert_to_openai_data_block(
"Standard data content block" can include old-style LangChain v0 blocks
(URLContentBlock, Base64ContentBlock, IDContentBlock) or new ones.
Args:
block: The content block to convert.
api: The OpenAI API being targeted. Either "chat/completions" or "responses".
Raises:
ValueError: If required keys are missing.
ValueError: If file URLs are used with Chat Completions API.
ValueError: If block type is unsupported.
Returns:
The formatted content block.
"""
if block["type"] == "image":
chat_completions_block = convert_to_openai_image_block(block)
@@ -169,8 +193,6 @@ def _convert_to_v1_from_chat_completions_input(
Returns:
Updated list with OpenAI blocks converted to v1 format.
"""
from langchain_core.messages import content as types # noqa: PLC0415
converted_blocks = []
unpacked_blocks: list[dict[str, Any]] = [
cast("dict[str, Any]", block)
@@ -248,7 +270,7 @@ def _convert_from_v1_to_chat_completions(message: AIMessage) -> AIMessage:
if block_type == "text":
# Strip annotations
new_content.append({"type": "text", "text": block["text"]})
elif block_type in ("reasoning", "tool_call"):
elif block_type in {"reasoning", "tool_call"}:
pass
else:
new_content.append(block)
@@ -265,8 +287,6 @@ _FUNCTION_CALL_IDS_MAP_KEY = "__openai_function_call_ids__"
def _convert_from_v03_ai_message(message: AIMessage) -> AIMessage:
"""Convert v0 AIMessage into `output_version="responses/v1"` format."""
from langchain_core.messages import AIMessageChunk # noqa: PLC0415
# Only update ChatOpenAI v0.3 AIMessages
is_chatopenai_v03 = (
isinstance(message.content, list)
@@ -683,8 +703,6 @@ def _convert_to_v1_from_responses(message: AIMessage) -> list[types.ContentBlock
) = None
call_id = block.get("call_id", "")
from langchain_core.messages import AIMessageChunk # noqa: PLC0415
if (
isinstance(message, AIMessageChunk)
and len(message.tool_call_chunks) == 1
@@ -706,8 +724,6 @@ def _convert_to_v1_from_responses(message: AIMessage) -> list[types.ContentBlock
if invalid_tool_call.get("id") == call_id:
tool_call_block = invalid_tool_call.copy()
break
else:
pass
if tool_call_block:
if "id" in block:
if "extras" not in tool_call_block:
@@ -735,7 +751,7 @@ def _convert_to_v1_from_responses(message: AIMessage) -> list[types.ContentBlock
k: v for k, v in block["action"].items() if k != "sources"
}
for key in block:
if key not in ("type", "id", "action", "status", "index"):
if key not in {"type", "id", "action", "status", "index"}:
web_search_call[key] = block[key]
yield cast("types.ServerToolCall", web_search_call)
@@ -761,8 +777,6 @@ def _convert_to_v1_from_responses(message: AIMessage) -> list[types.ContentBlock
web_search_result["status"] = "success"
elif status:
web_search_result["extras"] = {"status": status}
else:
pass
if "index" in block and isinstance(block["index"], int):
web_search_result["index"] = f"lc_wsr_{block['index'] + 1}"
yield cast("types.ServerToolResult", web_search_result)
@@ -778,14 +792,14 @@ def _convert_to_v1_from_responses(message: AIMessage) -> list[types.ContentBlock
file_search_call["index"] = f"lc_fsc_{block['index']}"
for key in block:
if key not in (
if key not in {
"type",
"id",
"queries",
"results",
"status",
"index",
):
}:
file_search_call[key] = block[key]
yield cast("types.ServerToolCall", file_search_call)
@@ -804,8 +818,6 @@ def _convert_to_v1_from_responses(message: AIMessage) -> list[types.ContentBlock
file_search_result["status"] = "success"
elif status:
file_search_result["extras"] = {"status": status}
else:
pass
if "index" in block and isinstance(block["index"], int):
file_search_result["index"] = f"lc_fsr_{block['index'] + 1}"
yield cast("types.ServerToolResult", file_search_result)
@@ -849,8 +861,6 @@ def _convert_to_v1_from_responses(message: AIMessage) -> list[types.ContentBlock
code_interpreter_result["status"] = "success"
elif status:
code_interpreter_result["extras"] = {"status": status}
else:
pass
if "index" in block and isinstance(block["index"], int):
code_interpreter_result["index"] = f"lc_cir_{block['index'] + 1}"
@@ -981,7 +991,14 @@ def _convert_to_v1_from_responses(message: AIMessage) -> list[types.ContentBlock
def translate_content(message: AIMessage) -> list[types.ContentBlock]:
"""Derive standard content blocks from a message with OpenAI content."""
"""Derive standard content blocks from a message with OpenAI content.
Args:
message: The message to translate.
Returns:
The derived content blocks.
"""
if isinstance(message.content, str):
return _convert_to_v1_from_chat_completions(message)
message = _convert_from_v03_ai_message(message)
@@ -989,7 +1006,14 @@ def translate_content(message: AIMessage) -> list[types.ContentBlock]:
def translate_content_chunk(message: AIMessageChunk) -> list[types.ContentBlock]:
"""Derive standard content blocks from a message chunk with OpenAI content."""
"""Derive standard content blocks from a message chunk with OpenAI content.
Args:
message: The message chunk to translate.
Returns:
The derived content blocks.
"""
if isinstance(message.content, str):
return _convert_to_v1_from_chat_completions_chunk(message)
message = _convert_from_v03_ai_message(message) # type: ignore[assignment]

View File

@@ -1,18 +1,14 @@
"""Standard, multimodal content blocks for Large Language Model I/O.
!!! warning
This module is under active development. The API is unstable and subject to
change in future releases.
This module provides standardized data structures for representing inputs to and
outputs from LLMs. The core abstraction is the **Content Block**, a `TypedDict`.
This module provides standardized data structures for representing inputs to and outputs
from LLMs. The core abstraction is the **Content Block**, a `TypedDict`.
**Rationale**
Different LLM providers use distinct and incompatible API schemas. This module
provides a unified, provider-agnostic format to facilitate these interactions. A
message to or from a model is simply a list of content blocks, allowing for the natural
interleaving of text, images, and other content in a single ordered sequence.
Different LLM providers use distinct and incompatible API schemas. This module provides
a unified, provider-agnostic format to facilitate these interactions. A message to or
from a model is simply a list of content blocks, allowing for the natural interleaving
of text, images, and other content in a single ordered sequence.
An adapter for a specific provider is responsible for translating this standard list of
blocks into the format required by its API.
@@ -25,16 +21,27 @@ without losing the benefits of type checking and validation.
Furthermore, provider-specific fields **within** a standard block are fully supported
by default in the `extras` field of each block. This allows for additional metadata
to be included without breaking the standard structure.
to be included without breaking the standard structure. For example, Google's thought
signature:
```python
AIMessage(
content=[
{
"type": "text",
"text": "J'adore la programmation.",
"extras": {"signature": "EpoWCpc..."}, # Thought signature
}
], ...
)
```
!!! warning
Do not heavily rely on the `extras` field for provider-specific data! This field
is subject to deprecation in future releases as we move towards PEP 728.
!!! note
Following widespread adoption of [PEP 728](https://peps.python.org/pep-0728/), we
will add `extra_items=Any` as a param to Content Blocks. This will signify to type
checkers that additional provider-specific fields are allowed outside of the
intend to add `extra_items=Any` as a param to Content Blocks. This will signify to
type checkers that additional provider-specific fields are allowed outside of the
`extras` field, and that will become the new standard approach to adding
provider-specific metadata.
@@ -72,30 +79,10 @@ to be included without breaking the standard structure.
openai_data = my_block["openai_metadata"] # Type: Any
```
PEP 728 is enabled with `# type: ignore[call-arg]` comments to suppress
warnings from type checkers that don't yet support it. The functionality works
correctly in Python 3.13+ and will be fully supported as the ecosystem catches
up.
**Key Block Types**
The module defines several types of content blocks, including:
- `TextContentBlock`: Standard text output.
- `Citation`: For annotations that link text output to a source document.
- `ToolCall`: For function calling.
- `ReasoningContentBlock`: To capture a model's thought process.
- Multimodal data:
- `ImageContentBlock`
- `AudioContentBlock`
- `VideoContentBlock`
- `PlainTextContentBlock` (e.g. .txt or .md files)
- `FileContentBlock` (e.g. PDFs, etc.)
**Example Usage**
```python
# Direct construction:
# Direct construction
from langchain_core.messages.content import TextContentBlock, ImageContentBlock
multimodal_message: AIMessage(
@@ -109,7 +96,7 @@ multimodal_message: AIMessage(
]
)
# Using factories:
# Using factories
from langchain_core.messages.content import create_text_block, create_image_block
multimodal_message: AIMessage(
@@ -124,6 +111,7 @@ multimodal_message: AIMessage(
```
Factory functions offer benefits such as:
- Automatic ID generation (when not provided)
- No need to manually specify the `type` field
"""
@@ -139,30 +127,30 @@ class Citation(TypedDict):
"""Annotation for citing data from a document.
!!! note
`start`/`end` indices refer to the **response text**,
not the source text. This means that the indices are relative to the model's
response, not the original document (as specified in the `url`).
!!! note "Factory function"
`create_citation` may also be used as a factory to create a `Citation`.
Benefits include:
* Automatic ID generation (when not provided)
* Required arguments strictly validated at creation time
"""
type: Literal["citation"]
"""Type of the content block. Used for discrimination."""
id: NotRequired[str]
"""Content block identifier.
"""Unique identifier for this content block.
Either:
- Generated by the provider (e.g., OpenAI's file ID)
- Generated by the provider
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
"""
url: NotRequired[str]
@@ -200,13 +188,12 @@ class NonStandardAnnotation(TypedDict):
"""Type of the content block. Used for discrimination."""
id: NotRequired[str]
"""Content block identifier.
"""Unique identifier for this content block.
Either:
- Generated by the provider (e.g., OpenAI's file ID)
- Generated by the provider
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
"""
value: dict[str, Any]
@@ -224,25 +211,24 @@ class TextContentBlock(TypedDict):
from a language model or the text of a user message.
!!! note "Factory function"
`create_text_block` may also be used as a factory to create a
`TextContentBlock`. Benefits include:
* Automatic ID generation (when not provided)
* Required arguments strictly validated at creation time
"""
type: Literal["text"]
"""Type of the content block. Used for discrimination."""
id: NotRequired[str]
"""Content block identifier.
"""Unique identifier for this content block.
Either:
- Generated by the provider (e.g., OpenAI's file ID)
- Generated by the provider
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
"""
text: str
@@ -270,12 +256,12 @@ class ToolCall(TypedDict):
and an identifier of "123".
!!! note "Factory function"
`create_tool_call` may also be used as a factory to create a
`ToolCall`. Benefits include:
* Automatic ID generation (when not provided)
* Required arguments strictly validated at creation time
"""
type: Literal["tool_call"]
@@ -286,7 +272,6 @@ class ToolCall(TypedDict):
An identifier is needed to associate a tool call request with a tool
call result in events when multiple concurrent tool calls are made.
"""
# TODO: Consider making this NotRequired[str] in the future.
@@ -332,8 +317,8 @@ class ToolCallChunk(TypedDict):
An identifier is needed to associate a tool call request with a tool
call result in events when multiple concurrent tool calls are made.
"""
# TODO: Consider making this NotRequired[str] in the future.
name: str | None
"""The name of the tool to be called."""
@@ -353,7 +338,6 @@ class InvalidToolCall(TypedDict):
Here we add an `error` key to surface errors made during generation
(e.g., invalid JSON arguments.)
"""
# TODO: Consider making fields NotRequired[str] in the future.
@@ -366,8 +350,8 @@ class InvalidToolCall(TypedDict):
An identifier is needed to associate a tool call request with a tool
call result in events when multiple concurrent tool calls are made.
"""
# TODO: Consider making this NotRequired[str] in the future.
name: str | None
"""The name of the tool to be called."""
@@ -423,7 +407,13 @@ class ServerToolCallChunk(TypedDict):
"""JSON substring of the arguments to the tool call."""
id: NotRequired[str]
"""An identifier associated with the tool call."""
"""Unique identifier for this server tool call chunk.
Either:
- Generated by the provider
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
"""
index: NotRequired[int | str]
"""Index of block in aggregate response. Used during streaming."""
@@ -439,7 +429,13 @@ class ServerToolResult(TypedDict):
"""Used for discrimination."""
id: NotRequired[str]
"""An identifier associated with the server tool result."""
"""Unique identifier for this server tool result.
Either:
- Generated by the provider
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
"""
tool_call_id: str
"""ID of the corresponding server tool call."""
@@ -461,25 +457,24 @@ class ReasoningContentBlock(TypedDict):
"""Reasoning output from a LLM.
!!! note "Factory function"
`create_reasoning_block` may also be used as a factory to create a
`ReasoningContentBlock`. Benefits include:
* Automatic ID generation (when not provided)
* Required arguments strictly validated at creation time
"""
type: Literal["reasoning"]
"""Type of the content block. Used for discrimination."""
id: NotRequired[str]
"""Content block identifier.
"""Unique identifier for this content block.
Either:
- Generated by the provider (e.g., OpenAI's file ID)
- Generated by the provider
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
"""
reasoning: NotRequired[str]
@@ -487,7 +482,6 @@ class ReasoningContentBlock(TypedDict):
Either the thought summary or the raw reasoning text itself. This is often parsed
from `<think>` tags in the model's response.
"""
index: NotRequired[int | str]
@@ -504,35 +498,38 @@ class ImageContentBlock(TypedDict):
"""Image data.
!!! note "Factory function"
`create_image_block` may also be used as a factory to create a
`create_image_block` may also be used as a factory to create an
`ImageContentBlock`. Benefits include:
* Automatic ID generation (when not provided)
* Required arguments strictly validated at creation time
"""
type: Literal["image"]
"""Type of the content block. Used for discrimination."""
id: NotRequired[str]
"""Content block identifier.
"""Unique identifier for this content block.
Either:
- Generated by the provider (e.g., OpenAI's file ID)
- Generated by the provider
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
"""
file_id: NotRequired[str]
"""ID of the image file, e.g., from a file storage system."""
"""Reference to the image in an external file storage system.
For example, OpenAI or Anthropic's Files API.
"""
mime_type: NotRequired[str]
"""MIME type of the image. Required for base64.
"""MIME type of the image.
Required for base64 data.
[Examples from IANA](https://www.iana.org/assignments/media-types/media-types.xhtml#image)
"""
index: NotRequired[int | str]
@@ -552,35 +549,38 @@ class VideoContentBlock(TypedDict):
"""Video data.
!!! note "Factory function"
`create_video_block` may also be used as a factory to create a
`VideoContentBlock`. Benefits include:
* Automatic ID generation (when not provided)
* Required arguments strictly validated at creation time
"""
type: Literal["video"]
"""Type of the content block. Used for discrimination."""
id: NotRequired[str]
"""Content block identifier.
"""Unique identifier for this content block.
Either:
- Generated by the provider (e.g., OpenAI's file ID)
- Generated by the provider
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
"""
file_id: NotRequired[str]
"""ID of the video file, e.g., from a file storage system."""
"""Reference to the video in an external file storage system.
For example, OpenAI or Anthropic's Files API.
"""
mime_type: NotRequired[str]
"""MIME type of the video. Required for base64.
"""MIME type of the video.
Required for base64 data.
[Examples from IANA](https://www.iana.org/assignments/media-types/media-types.xhtml#video)
"""
index: NotRequired[int | str]
@@ -600,34 +600,38 @@ class AudioContentBlock(TypedDict):
"""Audio data.
!!! note "Factory function"
`create_audio_block` may also be used as a factory to create an
`AudioContentBlock`. Benefits include:
* Automatic ID generation (when not provided)
* Required arguments strictly validated at creation time
"""
type: Literal["audio"]
"""Type of the content block. Used for discrimination."""
id: NotRequired[str]
"""Content block identifier.
"""Unique identifier for this content block.
Either:
- Generated by the provider (e.g., OpenAI's file ID)
- Generated by the provider
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
"""
file_id: NotRequired[str]
"""ID of the audio file, e.g., from a file storage system."""
"""Reference to the audio file in an external file storage system.
For example, OpenAI or Anthropic's Files API.
"""
mime_type: NotRequired[str]
"""MIME type of the audio. Required for base64.
"""MIME type of the audio.
Required for base64 data.
[Examples from IANA](https://www.iana.org/assignments/media-types/media-types.xhtml#audio)
"""
index: NotRequired[int | str]
@@ -647,42 +651,49 @@ class PlainTextContentBlock(TypedDict):
"""Plaintext data (e.g., from a `.txt` or `.md` document).
!!! note
A `PlainTextContentBlock` existed in `langchain-core<1.0.0`. Although the
name has carried over, the structure has changed significantly. The only shared
keys between the old and new versions are `type` and `text`, though the
`type` value has changed from `'text'` to `'text-plain'`.
!!! note
Title and context are optional fields that may be passed to the model. See
Anthropic [example](https://platform.claude.com/docs/en/build-with-claude/citations#citable-vs-non-citable-content).
!!! note "Factory function"
`create_plaintext_block` may also be used as a factory to create a
`PlainTextContentBlock`. Benefits include:
* Automatic ID generation (when not provided)
* Required arguments strictly validated at creation time
"""
type: Literal["text-plain"]
"""Type of the content block. Used for discrimination."""
id: NotRequired[str]
"""Content block identifier.
"""Unique identifier for this content block.
Either:
- Generated by the provider (e.g., OpenAI's file ID)
- Generated by the provider
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
"""
file_id: NotRequired[str]
"""ID of the plaintext file, e.g., from a file storage system."""
"""Reference to the plaintext file in an external file storage system.
For example, OpenAI or Anthropic's Files API.
"""
mime_type: Literal["text/plain"]
"""MIME type of the file. Required for base64."""
"""MIME type of the file.
Required for base64 data.
"""
index: NotRequired[int | str]
"""Index of block in aggregate response. Used during streaming."""
@@ -717,35 +728,44 @@ class FileContentBlock(TypedDict):
`PlainTextContentBlock`).
!!! note "Factory function"
`create_file_block` may also be used as a factory to create a
`FileContentBlock`. Benefits include:
* Automatic ID generation (when not provided)
* Required arguments strictly validated at creation time
"""
type: Literal["file"]
"""Type of the content block. Used for discrimination."""
id: NotRequired[str]
"""Content block identifier.
"""Unique identifier for this content block.
Used for tracking and referencing specific blocks (e.g., during streaming).
Not to be confused with `file_id`, which references an external file in a
storage system.
Either:
- Generated by the provider (e.g., OpenAI's file ID)
- Generated by the provider
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
"""
file_id: NotRequired[str]
"""ID of the file, e.g., from a file storage system."""
"""Reference to the file in an external file storage system.
For example, a file ID from OpenAI's Files API or another cloud storage provider.
This is distinct from `id`, which identifies the content block itself.
"""
mime_type: NotRequired[str]
"""MIME type of the file. Required for base64.
"""MIME type of the file.
Required for base64 data.
[Examples from IANA](https://www.iana.org/assignments/media-types/media-types.xhtml)
"""
index: NotRequired[int | str]
@@ -780,25 +800,24 @@ class NonStandardContentBlock(TypedDict):
`value` field.
!!! note "Factory function"
`create_non_standard_block` may also be used as a factory to create a
`NonStandardContentBlock`. Benefits include:
* Automatic ID generation (when not provided)
* Required arguments strictly validated at creation time
"""
type: Literal["non_standard"]
"""Type of the content block. Used for discrimination."""
id: NotRequired[str]
"""Content block identifier.
"""Unique identifier for this content block.
Either:
- Generated by the provider (e.g., OpenAI's file ID)
- Generated by the provider
- Generated by LangChain upon creation (`UUID4` prefixed with `'lc_'`))
"""
value: dict[str, Any]
@@ -855,7 +874,7 @@ KNOWN_BLOCK_TYPES = {
"non_standard",
# citation and non_standard_annotation intentionally omitted
}
"""These are block types known to `langchain-core>=1.0.0`.
"""These are block types known to `langchain-core >= 1.0.0`.
If a block has a type not in this set, it is considered to be provider-specific.
"""
@@ -895,7 +914,6 @@ def is_data_content_block(block: dict) -> bool:
Returns:
`True` if the content block is a data content block, `False` otherwise.
"""
if block.get("type") not in _get_data_content_block_types():
return False
@@ -940,17 +958,21 @@ def create_text_block(
Args:
text: The text content of the block.
id: Content block identifier. Generated automatically if not provided.
id: Content block identifier.
Generated automatically if not provided.
annotations: `Citation`s and other annotations for the text.
index: Index of block in aggregate response. Used during streaming.
index: Index of block in aggregate response.
Used during streaming.
Returns:
A properly formatted `TextContentBlock`.
!!! note
The `id` is generated automatically if not provided, using a UUID4 format
prefixed with `'lc_'` to indicate it is a LangChain-generated ID.
"""
block = TextContentBlock(
type="text",
@@ -985,9 +1007,15 @@ def create_image_block(
url: URL of the image.
base64: Base64-encoded image data.
file_id: ID of the image file from a file storage system.
mime_type: MIME type of the image. Required for base64 data.
id: Content block identifier. Generated automatically if not provided.
index: Index of block in aggregate response. Used during streaming.
mime_type: MIME type of the image.
Required for base64 data.
id: Content block identifier.
Generated automatically if not provided.
index: Index of block in aggregate response.
Used during streaming.
Returns:
A properly formatted `ImageContentBlock`.
@@ -997,9 +1025,9 @@ def create_image_block(
`mime_type`.
!!! note
The `id` is generated automatically if not provided, using a UUID4 format
prefixed with `'lc_'` to indicate it is a LangChain-generated ID.
"""
if not any([url, base64, file_id]):
msg = "Must provide one of: url, base64, or file_id"
@@ -1041,9 +1069,15 @@ def create_video_block(
url: URL of the video.
base64: Base64-encoded video data.
file_id: ID of the video file from a file storage system.
mime_type: MIME type of the video. Required for base64 data.
id: Content block identifier. Generated automatically if not provided.
index: Index of block in aggregate response. Used during streaming.
mime_type: MIME type of the video.
Required for base64 data.
id: Content block identifier.
Generated automatically if not provided.
index: Index of block in aggregate response.
Used during streaming.
Returns:
A properly formatted `VideoContentBlock`.
@@ -1053,9 +1087,9 @@ def create_video_block(
`mime_type`.
!!! note
The `id` is generated automatically if not provided, using a UUID4 format
prefixed with `'lc_'` to indicate it is a LangChain-generated ID.
"""
if not any([url, base64, file_id]):
msg = "Must provide one of: url, base64, or file_id"
@@ -1101,9 +1135,15 @@ def create_audio_block(
url: URL of the audio.
base64: Base64-encoded audio data.
file_id: ID of the audio file from a file storage system.
mime_type: MIME type of the audio. Required for base64 data.
id: Content block identifier. Generated automatically if not provided.
index: Index of block in aggregate response. Used during streaming.
mime_type: MIME type of the audio.
Required for base64 data.
id: Content block identifier.
Generated automatically if not provided.
index: Index of block in aggregate response.
Used during streaming.
Returns:
A properly formatted `AudioContentBlock`.
@@ -1113,9 +1153,9 @@ def create_audio_block(
`mime_type`.
!!! note
The `id` is generated automatically if not provided, using a UUID4 format
prefixed with `'lc_'` to indicate it is a LangChain-generated ID.
"""
if not any([url, base64, file_id]):
msg = "Must provide one of: url, base64, or file_id"
@@ -1161,9 +1201,15 @@ def create_file_block(
url: URL of the file.
base64: Base64-encoded file data.
file_id: ID of the file from a file storage system.
mime_type: MIME type of the file. Required for base64 data.
id: Content block identifier. Generated automatically if not provided.
index: Index of block in aggregate response. Used during streaming.
mime_type: MIME type of the file.
Required for base64 data.
id: Content block identifier.
Generated automatically if not provided.
index: Index of block in aggregate response.
Used during streaming.
Returns:
A properly formatted `FileContentBlock`.
@@ -1173,9 +1219,9 @@ def create_file_block(
`mime_type`.
!!! note
The `id` is generated automatically if not provided, using a UUID4 format
prefixed with `'lc_'` to indicate it is a LangChain-generated ID.
"""
if not any([url, base64, file_id]):
msg = "Must provide one of: url, base64, or file_id"
@@ -1225,16 +1271,20 @@ def create_plaintext_block(
file_id: ID of the plaintext file from a file storage system.
title: Title of the text data.
context: Context or description of the text content.
id: Content block identifier. Generated automatically if not provided.
index: Index of block in aggregate response. Used during streaming.
id: Content block identifier.
Generated automatically if not provided.
index: Index of block in aggregate response.
Used during streaming.
Returns:
A properly formatted `PlainTextContentBlock`.
!!! note
The `id` is generated automatically if not provided, using a UUID4 format
prefixed with `'lc_'` to indicate it is a LangChain-generated ID.
"""
block = PlainTextContentBlock(
type="text-plain",
@@ -1277,16 +1327,20 @@ def create_tool_call(
Args:
name: The name of the tool to be called.
args: The arguments to the tool call.
id: An identifier for the tool call. Generated automatically if not provided.
index: Index of block in aggregate response. Used during streaming.
id: An identifier for the tool call.
Generated automatically if not provided.
index: Index of block in aggregate response.
Used during streaming.
Returns:
A properly formatted `ToolCall`.
!!! note
The `id` is generated automatically if not provided, using a UUID4 format
prefixed with `'lc_'` to indicate it is a LangChain-generated ID.
"""
block = ToolCall(
type="tool_call",
@@ -1315,16 +1369,20 @@ def create_reasoning_block(
Args:
reasoning: The reasoning text or thought summary.
id: Content block identifier. Generated automatically if not provided.
index: Index of block in aggregate response. Used during streaming.
id: Content block identifier.
Generated automatically if not provided.
index: Index of block in aggregate response.
Used during streaming.
Returns:
A properly formatted `ReasoningContentBlock`.
!!! note
The `id` is generated automatically if not provided, using a UUID4 format
prefixed with `'lc_'` to indicate it is a LangChain-generated ID.
"""
block = ReasoningContentBlock(
type="reasoning",
@@ -1360,15 +1418,17 @@ def create_citation(
start_index: Start index in the response text where citation applies.
end_index: End index in the response text where citation applies.
cited_text: Excerpt of source text being cited.
id: Content block identifier. Generated automatically if not provided.
id: Content block identifier.
Generated automatically if not provided.
Returns:
A properly formatted `Citation`.
!!! note
The `id` is generated automatically if not provided, using a UUID4 format
prefixed with `'lc_'` to indicate it is a LangChain-generated ID.
"""
block = Citation(type="citation", id=ensure_id(id))
@@ -1400,16 +1460,20 @@ def create_non_standard_block(
Args:
value: Provider-specific content data.
id: Content block identifier. Generated automatically if not provided.
index: Index of block in aggregate response. Used during streaming.
id: Content block identifier.
Generated automatically if not provided.
index: Index of block in aggregate response.
Used during streaming.
Returns:
A properly formatted `NonStandardContentBlock`.
!!! note
The `id` is generated automatically if not provided, using a UUID4 format
prefixed with `'lc_'` to indicate it is a LangChain-generated ID.
"""
block = NonStandardContentBlock(
type="non_standard",

View File

@@ -29,38 +29,39 @@ class ToolMessage(BaseMessage, ToolOutputMixin):
`ToolMessage` objects contain the result of a tool invocation. Typically, the result
is encoded inside the `content` field.
Example: A `ToolMessage` representing a result of `42` from a tool call with id
`tool_call_id` is used to associate the tool call request with the tool call
response. Useful in situations where a chat model is able to request multiple tool
calls in parallel.
```python
from langchain_core.messages import ToolMessage
Example:
A `ToolMessage` representing a result of `42` from a tool call with id
ToolMessage(content="42", tool_call_id="call_Jja7J89XsjrOLA5r!MEOW!SL")
```
```python
from langchain_core.messages import ToolMessage
Example: A `ToolMessage` where only part of the tool output is sent to the model
and the full output is passed in to artifact.
ToolMessage(content="42", tool_call_id="call_Jja7J89XsjrOLA5r!MEOW!SL")
```
```python
from langchain_core.messages import ToolMessage
Example:
A `ToolMessage` where only part of the tool output is sent to the model
and the full output is passed in to artifact.
tool_output = {
"stdout": "From the graph we can see that the correlation between "
"x and y is ...",
"stderr": None,
"artifacts": {"type": "image", "base64_data": "/9j/4gIcSU..."},
}
```python
from langchain_core.messages import ToolMessage
ToolMessage(
content=tool_output["stdout"],
artifact=tool_output,
tool_call_id="call_Jja7J89XsjrOLA5r!MEOW!SL",
)
```
The `tool_call_id` field is used to associate the tool call request with the
tool call response. Useful in situations where a chat model is able
to request multiple tool calls in parallel.
tool_output = {
"stdout": "From the graph we can see that the correlation between "
"x and y is ...",
"stderr": None,
"artifacts": {"type": "image", "base64_data": "/9j/4gIcSU..."},
}
ToolMessage(
content=tool_output["stdout"],
artifact=tool_output,
tool_call_id="call_Jja7J89XsjrOLA5r!MEOW!SL",
)
```
"""
tool_call_id: str
@@ -213,20 +214,29 @@ class ToolCall(TypedDict):
This represents a request to call the tool named `'foo'` with arguments
`{"a": 1}` and an identifier of `'123'`.
!!! note "Factory function"
`tool_call` may also be used as a factory to create a `ToolCall`. Benefits
include:
* Required arguments strictly validated at creation time
"""
name: str
"""The name of the tool to be called."""
args: dict[str, Any]
"""The arguments to the tool call."""
"""The arguments to the tool call as a dictionary."""
id: str | None
"""An identifier associated with the tool call.
An identifier is needed to associate a tool call request with a tool
call result in events when multiple concurrent tool calls are made.
"""
type: NotRequired[Literal["tool_call"]]
"""Used for discrimination."""
def tool_call(
@@ -239,7 +249,7 @@ def tool_call(
Args:
name: The name of the tool to be called.
args: The arguments to the tool call.
args: The arguments to the tool call as a dictionary.
id: An identifier associated with the tool call.
Returns:
@@ -251,9 +261,9 @@ def tool_call(
class ToolCallChunk(TypedDict):
"""A chunk of a tool call (yielded when streaming).
When merging `ToolCallChunk`s (e.g., via `AIMessageChunk.__add__`),
all string attributes are concatenated. Chunks are only merged if their
values of `index` are equal and not None.
When merging `ToolCallChunk` objects (e.g., via `AIMessageChunk.__add__`), all
string attributes are concatenated. Chunks are only merged if their values of
`index` are equal and not `None`.
Example:
```python
@@ -269,13 +279,25 @@ class ToolCallChunk(TypedDict):
name: str | None
"""The name of the tool to be called."""
args: str | None
"""The arguments to the tool call."""
"""The arguments to the tool call as a JSON-parseable string."""
id: str | None
"""An identifier associated with the tool call."""
"""An identifier associated with the tool call.
An identifier is needed to associate a tool call request with a tool
call result in events when multiple concurrent tool calls are made.
"""
index: int | None
"""The index of the tool call in a sequence."""
"""The index of the tool call in a sequence.
Used for merging chunks.
"""
type: NotRequired[Literal["tool_call_chunk"]]
"""Used for discrimination."""
def tool_call_chunk(
@@ -289,7 +311,7 @@ def tool_call_chunk(
Args:
name: The name of the tool to be called.
args: The arguments to the tool call.
args: The arguments to the tool call as a JSON string.
id: An identifier associated with the tool call.
index: The index of the tool call in a sequence.
@@ -312,7 +334,7 @@ def invalid_tool_call(
Args:
name: The name of the tool to be called.
args: The arguments to the tool call.
args: The arguments to the tool call as a JSON string.
id: An identifier associated with the tool call.
error: An error message associated with the tool call.

View File

@@ -15,15 +15,20 @@ import json
import logging
import math
from collections.abc import Callable, Iterable, Sequence
from functools import partial
from functools import partial, wraps
from typing import (
TYPE_CHECKING,
Annotated,
Any,
Concatenate,
Literal,
ParamSpec,
Protocol,
TypeVar,
cast,
overload,
)
from xml.sax.saxutils import escape, quoteattr
from pydantic import Discriminator, Field, Tag
@@ -61,14 +66,19 @@ logger = logging.getLogger(__name__)
def _get_type(v: Any) -> str:
"""Get the type associated with the object for serialization purposes."""
if isinstance(v, dict) and "type" in v:
return v["type"]
if hasattr(v, "type"):
return v.type
msg = (
f"Expected either a dictionary with a 'type' key or an object "
f"with a 'type' attribute. Instead got type {type(v)}."
)
raise TypeError(msg)
result = v["type"]
elif hasattr(v, "type"):
result = v.type
else:
msg = (
f"Expected either a dictionary with a 'type' key or an object "
f"with a 'type' attribute. Instead got type {type(v)}."
)
raise TypeError(msg)
if not isinstance(result, str):
msg = f"Expected 'type' to be a str, got {type(result).__name__}"
raise TypeError(msg)
return result
AnyMessage = Annotated[
@@ -89,8 +99,199 @@ AnyMessage = Annotated[
"""A type representing any defined `Message` or `MessageChunk` type."""
def _has_base64_data(block: dict) -> bool:
"""Check if a content block contains base64 encoded data.
Args:
block: A content block dictionary.
Returns:
Whether the block contains base64 data.
"""
# Check for explicit base64 field (standard content blocks)
if block.get("base64"):
return True
# Check for data: URL in url field
url = block.get("url", "")
if isinstance(url, str) and url.startswith("data:"):
return True
# Check for OpenAI-style image_url with data: URL
image_url = block.get("image_url", {})
if isinstance(image_url, dict):
url = image_url.get("url", "")
if isinstance(url, str) and url.startswith("data:"):
return True
return False
_XML_CONTENT_BLOCK_MAX_LEN = 500
def _truncate(text: str, max_len: int = _XML_CONTENT_BLOCK_MAX_LEN) -> str:
"""Truncate text to `max_len` characters, adding ellipsis if truncated."""
if len(text) <= max_len:
return text
return text[:max_len] + "..."
def _format_content_block_xml(block: dict) -> str | None:
"""Format a content block as XML.
Args:
block: A LangChain content block.
Returns:
XML string representation of the block, or `None` if the block should be
skipped.
Note:
Plain text document content, server tool call arguments, and server tool
result outputs are truncated to 500 characters.
"""
block_type = block.get("type", "")
# Skip blocks with base64 encoded data
if _has_base64_data(block):
return None
# Text blocks
if block_type == "text":
text = block.get("text", "")
return escape(text) if text else None
# Reasoning blocks
if block_type == "reasoning":
reasoning = block.get("reasoning", "")
if reasoning:
return f"<reasoning>{escape(reasoning)}</reasoning>"
return None
# Image blocks (URL only, base64 already filtered)
if block_type == "image":
url = block.get("url")
file_id = block.get("file_id")
if url:
return f"<image url={quoteattr(url)} />"
if file_id:
return f"<image file_id={quoteattr(file_id)} />"
return None
# OpenAI-style image_url blocks
if block_type == "image_url":
image_url = block.get("image_url", {})
if isinstance(image_url, dict):
url = image_url.get("url", "")
if url and not url.startswith("data:"):
return f"<image url={quoteattr(url)} />"
return None
# Audio blocks (URL only)
if block_type == "audio":
url = block.get("url")
file_id = block.get("file_id")
if url:
return f"<audio url={quoteattr(url)} />"
if file_id:
return f"<audio file_id={quoteattr(file_id)} />"
return None
# Video blocks (URL only)
if block_type == "video":
url = block.get("url")
file_id = block.get("file_id")
if url:
return f"<video url={quoteattr(url)} />"
if file_id:
return f"<video file_id={quoteattr(file_id)} />"
return None
# Plain text document blocks
if block_type == "text-plain":
text = block.get("text", "")
return escape(_truncate(text)) if text else None
# Server tool call blocks (from AI messages)
if block_type == "server_tool_call":
tc_id = quoteattr(str(block.get("id") or ""))
tc_name = quoteattr(str(block.get("name") or ""))
tc_args_json = json.dumps(block.get("args", {}), ensure_ascii=False)
tc_args = escape(_truncate(tc_args_json))
return (
f"<server_tool_call id={tc_id} name={tc_name}>{tc_args}</server_tool_call>"
)
# Server tool result blocks
if block_type == "server_tool_result":
tool_call_id = quoteattr(str(block.get("tool_call_id") or ""))
status = quoteattr(str(block.get("status") or ""))
output = block.get("output")
if output:
output_json = json.dumps(output, ensure_ascii=False)
output_str = escape(_truncate(output_json))
else:
output_str = ""
return (
f"<server_tool_result tool_call_id={tool_call_id} status={status}>"
f"{output_str}</server_tool_result>"
)
# Unknown block type - skip silently
return None
def _get_message_type_str(
m: BaseMessage,
human_prefix: str,
ai_prefix: str,
system_prefix: str,
function_prefix: str,
tool_prefix: str,
) -> str:
"""Get the type string for XML message element.
Args:
m: The message to get the type string for.
human_prefix: The prefix to use for `HumanMessage`.
ai_prefix: The prefix to use for `AIMessage`.
system_prefix: The prefix to use for `SystemMessage`.
function_prefix: The prefix to use for `FunctionMessage`.
tool_prefix: The prefix to use for `ToolMessage`.
Returns:
The type string for the message element.
Raises:
ValueError: If an unsupported message type is encountered.
"""
if isinstance(m, HumanMessage):
return human_prefix.lower()
if isinstance(m, AIMessage):
return ai_prefix.lower()
if isinstance(m, SystemMessage):
return system_prefix.lower()
if isinstance(m, FunctionMessage):
return function_prefix.lower()
if isinstance(m, ToolMessage):
return tool_prefix.lower()
if isinstance(m, ChatMessage):
return m.role
msg = f"Got unsupported message type: {m}"
raise ValueError(msg)
def get_buffer_string(
messages: Sequence[BaseMessage], human_prefix: str = "Human", ai_prefix: str = "AI"
messages: Sequence[BaseMessage],
human_prefix: str = "Human",
ai_prefix: str = "AI",
*,
system_prefix: str = "System",
function_prefix: str = "Function",
tool_prefix: str = "Tool",
message_separator: str = "\n",
format: Literal["prefix", "xml"] = "prefix", # noqa: A002
) -> str:
r"""Convert a sequence of messages to strings and concatenate them into one string.
@@ -98,6 +299,15 @@ def get_buffer_string(
messages: Messages to be converted to strings.
human_prefix: The prefix to prepend to contents of `HumanMessage`s.
ai_prefix: The prefix to prepend to contents of `AIMessage`.
system_prefix: The prefix to prepend to contents of `SystemMessage`s.
function_prefix: The prefix to prepend to contents of `FunctionMessage`s.
tool_prefix: The prefix to prepend to contents of `ToolMessage`s.
message_separator: The separator to use between messages.
format: The output format. `'prefix'` uses `Role: content` format (default).
`'xml'` uses XML-style `<message type='role'>` format with proper character
escaping, which is useful when message content may contain role-like
prefixes that could cause ambiguity.
Returns:
A single string concatenation of all input messages.
@@ -105,9 +315,38 @@ def get_buffer_string(
Raises:
ValueError: If an unsupported message type is encountered.
Note:
If a message is an `AIMessage` and contains both tool calls under `tool_calls`
and a function call under `additional_kwargs["function_call"]`, only the tool
calls will be appended to the string representation.
When using `format='xml'`:
- All messages use uniform `<message type="role">content</message>` format.
- The `type` attribute uses `human_prefix` (lowercased) for `HumanMessage`,
`ai_prefix` (lowercased) for `AIMessage`, `system_prefix` (lowercased)
for `SystemMessage`, `function_prefix` (lowercased) for `FunctionMessage`,
`tool_prefix` (lowercased) for `ToolMessage`, and the original role
(unchanged) for `ChatMessage`.
- Message content is escaped using `xml.sax.saxutils.escape()`.
- Attribute values are escaped using `xml.sax.saxutils.quoteattr()`.
- AI messages with tool calls use nested structure with `<content>` and
`<tool_call>` elements.
- For multi-modal content (list of content blocks), supported block types
are: `text`, `reasoning`, `image` (URL/file_id only), `image_url`
(OpenAI-style, URL only), `audio` (URL/file_id only), `video` (URL/file_id
only), `text-plain`, `server_tool_call`, and `server_tool_result`.
- Content blocks with base64-encoded data are skipped (including blocks
with `base64` field or `data:` URLs).
- Unknown block types are skipped.
- Plain text document content (`text-plain`), server tool call arguments,
and server tool result outputs are truncated to 500 characters.
Example:
Default prefix format:
```python
from langchain_core import AIMessage, HumanMessage
from langchain_core.messages import AIMessage, HumanMessage, get_buffer_string
messages = [
HumanMessage(content="Hi, how are you?"),
@@ -116,7 +355,54 @@ def get_buffer_string(
get_buffer_string(messages)
# -> "Human: Hi, how are you?\nAI: Good, how are you?"
```
XML format (useful when content contains role-like prefixes):
```python
messages = [
HumanMessage(content="Example: Human: some text"),
AIMessage(content="I see the example."),
]
get_buffer_string(messages, format="xml")
# -> '<message type="human">Example: Human: some text</message>\\n'
# -> '<message type="ai">I see the example.</message>'
```
XML format with special characters (automatically escaped):
```python
messages = [
HumanMessage(content="Is 5 < 10 & 10 > 5?"),
]
get_buffer_string(messages, format="xml")
# -> '<message type="human">Is 5 &lt; 10 &amp; 10 &gt; 5?</message>'
```
XML format with tool calls:
```python
messages = [
AIMessage(
content="I'll search for that.",
tool_calls=[
{"id": "call_123", "name": "search", "args": {"query": "weather"}}
],
),
]
get_buffer_string(messages, format="xml")
# -> '<message type="ai">\\n'
# -> ' <content>I\\'ll search for that.</content>\\n'
# -> ' <tool_call id="call_123" name="search">'
# -> '{"query": "weather"}</tool_call>\\n'
# -> '</message>'
```
"""
if format not in ("prefix", "xml"):
msg = (
f"Unrecognized format={format!r}. Supported formats are 'prefix' and 'xml'."
)
raise ValueError(msg)
string_messages = []
for m in messages:
if isinstance(m, HumanMessage):
@@ -124,22 +410,96 @@ def get_buffer_string(
elif isinstance(m, AIMessage):
role = ai_prefix
elif isinstance(m, SystemMessage):
role = "System"
role = system_prefix
elif isinstance(m, FunctionMessage):
role = "Function"
role = function_prefix
elif isinstance(m, ToolMessage):
role = "Tool"
role = tool_prefix
elif isinstance(m, ChatMessage):
role = m.role
else:
msg = f"Got unsupported message type: {m}"
raise ValueError(msg) # noqa: TRY004
message = f"{role}: {m.text}"
if isinstance(m, AIMessage) and "function_call" in m.additional_kwargs:
message += f"{m.additional_kwargs['function_call']}"
if format == "xml":
msg_type = _get_message_type_str(
m, human_prefix, ai_prefix, system_prefix, function_prefix, tool_prefix
)
# Format content blocks
if isinstance(m.content, str):
content_parts = [escape(m.content)] if m.content else []
else:
# List of content blocks
content_parts = []
for block in m.content:
if isinstance(block, str):
if block:
content_parts.append(escape(block))
else:
formatted = _format_content_block_xml(block)
if formatted:
content_parts.append(formatted)
# Check if this is an AIMessage with tool calls
has_tool_calls = isinstance(m, AIMessage) and m.tool_calls
has_function_call = (
isinstance(m, AIMessage)
and not m.tool_calls
and "function_call" in m.additional_kwargs
)
if has_tool_calls or has_function_call:
# Use nested structure for AI messages with tool calls
# Type narrowing: at this point m is AIMessage (verified above)
ai_msg = cast("AIMessage", m)
parts = [f"<message type={quoteattr(msg_type)}>"]
if content_parts:
parts.append(f" <content>{' '.join(content_parts)}</content>")
if has_tool_calls:
for tc in ai_msg.tool_calls:
tc_id = quoteattr(str(tc.get("id") or ""))
tc_name = quoteattr(str(tc.get("name") or ""))
tc_args = escape(
json.dumps(tc.get("args", {}), ensure_ascii=False)
)
parts.append(
f" <tool_call id={tc_id} name={tc_name}>"
f"{tc_args}</tool_call>"
)
elif has_function_call:
fc = ai_msg.additional_kwargs["function_call"]
fc_name = quoteattr(str(fc.get("name") or ""))
fc_args = escape(str(fc.get("arguments") or "{}"))
parts.append(
f" <function_call name={fc_name}>{fc_args}</function_call>"
)
parts.append("</message>")
message = "\n".join(parts)
else:
# Simple structure for messages without tool calls
joined_content = " ".join(content_parts)
message = (
f"<message type={quoteattr(msg_type)}>{joined_content}</message>"
)
else: # format == "prefix"
content = m.text
message = f"{role}: {content}"
tool_info = ""
if isinstance(m, AIMessage):
if m.tool_calls:
tool_info = str(m.tool_calls)
elif "function_call" in m.additional_kwargs:
# Legacy behavior assumes only one function call per message
tool_info = str(m.additional_kwargs["function_call"])
if tool_info:
message += tool_info # Preserve original behavior
string_messages.append(message)
return "\n".join(string_messages)
return message_separator.join(string_messages)
def _message_from_dict(message: dict) -> BaseMessage:
@@ -202,8 +562,11 @@ def message_chunk_to_message(chunk: BaseMessage) -> BaseMessage:
ignore_keys = ["type"]
if isinstance(chunk, AIMessageChunk):
ignore_keys.extend(["tool_call_chunks", "chunk_position"])
return chunk.__class__.__mro__[1](
**{k: v for k, v in chunk.__dict__.items() if k not in ignore_keys}
return cast(
"BaseMessage",
chunk.__class__.__mro__[1](
**{k: v for k, v in chunk.__dict__.items() if k not in ignore_keys}
),
)
@@ -225,13 +588,13 @@ def _create_message_from_message_type(
"""Create a message from a `Message` type and content string.
Args:
message_type: (str) the type of the message (e.g., `'human'`, `'ai'`, etc.).
content: (str) the content string.
name: (str) the name of the message.
tool_call_id: (str) the tool call id.
tool_calls: (list[dict[str, Any]]) the tool calls.
id: (str) the id of the message.
additional_kwargs: (dict[str, Any]) additional keyword arguments.
message_type: the type of the message (e.g., `'human'`, `'ai'`, etc.).
content: the content string.
name: the name of the message.
tool_call_id: the tool call id.
tool_calls: the tool calls.
id: the id of the message.
additional_kwargs: additional keyword arguments.
Returns:
a message of the appropriate type.
@@ -384,33 +747,54 @@ def convert_to_messages(
return [_convert_to_message(m) for m in messages]
def _runnable_support(func: Callable) -> Callable:
_P = ParamSpec("_P")
_R_co = TypeVar("_R_co", covariant=True)
class _RunnableSupportCallable(Protocol[_P, _R_co]):
@overload
def wrapped(
messages: None = None, **kwargs: Any
) -> Runnable[Sequence[MessageLikeRepresentation], list[BaseMessage]]: ...
def __call__(
self,
messages: None = None,
*args: _P.args,
**kwargs: _P.kwargs,
) -> Runnable[Sequence[MessageLikeRepresentation], _R_co]: ...
@overload
def wrapped(
messages: Sequence[MessageLikeRepresentation], **kwargs: Any
) -> list[BaseMessage]: ...
def __call__(
self,
messages: Sequence[MessageLikeRepresentation] | PromptValue,
*args: _P.args,
**kwargs: _P.kwargs,
) -> _R_co: ...
def __call__(
self,
messages: Sequence[MessageLikeRepresentation] | PromptValue | None = None,
*args: _P.args,
**kwargs: _P.kwargs,
) -> _R_co | Runnable[Sequence[MessageLikeRepresentation], _R_co]: ...
def _runnable_support(
func: Callable[
Concatenate[Sequence[MessageLikeRepresentation] | PromptValue, _P], _R_co
],
) -> _RunnableSupportCallable[_P, _R_co]:
@wraps(func)
def wrapped(
messages: Sequence[MessageLikeRepresentation] | None = None,
**kwargs: Any,
) -> (
list[BaseMessage]
| Runnable[Sequence[MessageLikeRepresentation], list[BaseMessage]]
):
messages: Sequence[MessageLikeRepresentation] | PromptValue | None = None,
*args: _P.args,
**kwargs: _P.kwargs,
) -> _R_co | Runnable[Sequence[MessageLikeRepresentation], _R_co]:
# Import locally to prevent circular import.
from langchain_core.runnables.base import RunnableLambda # noqa: PLC0415
if messages is not None:
return func(messages, **kwargs)
return func(messages, *args, **kwargs)
return RunnableLambda(partial(func, **kwargs), name=func.__name__)
wrapped.__doc__ = func.__doc__
return wrapped
return cast("_RunnableSupportCallable[_P, _R_co]", wrapped)
@_runnable_support
@@ -514,6 +898,7 @@ def filter_messages(
):
continue
new_msg = msg
if isinstance(exclude_tool_calls, (list, tuple, set)):
if isinstance(msg, AIMessage) and msg.tool_calls:
tool_calls = [
@@ -537,7 +922,7 @@ def filter_messages(
)
]
msg = msg.model_copy( # noqa: PLW2901
new_msg = msg.model_copy(
update={"tool_calls": tool_calls, "content": content}
)
elif (
@@ -548,11 +933,11 @@ def filter_messages(
# default to inclusion when no inclusion criteria given.
if (
not (include_types or include_ids or include_names)
or (include_names and msg.name in include_names)
or (include_types and _is_message_type(msg, include_types))
or (include_ids and msg.id in include_ids)
or (include_names and new_msg.name in include_names)
or (include_types and _is_message_type(new_msg, include_types))
or (include_ids and new_msg.id in include_ids)
):
filtered.append(msg)
filtered.append(new_msg)
return filtered
@@ -695,7 +1080,8 @@ def trim_messages(
max_tokens: int,
token_counter: Callable[[list[BaseMessage]], int]
| Callable[[BaseMessage], int]
| BaseLanguageModel,
| BaseLanguageModel
| Literal["approximate"],
strategy: Literal["first", "last"] = "last",
allow_partial: bool = False,
end_on: str | type[BaseMessage] | Sequence[str | type[BaseMessage]] | None = None,
@@ -733,53 +1119,65 @@ def trim_messages(
messages: Sequence of Message-like objects to trim.
max_tokens: Max token count of trimmed messages.
token_counter: Function or llm for counting tokens in a `BaseMessage` or a
list of `BaseMessage`. If a `BaseLanguageModel` is passed in then
`BaseLanguageModel.get_num_tokens_from_messages()` will be used.
Set to `len` to count the number of **messages** in the chat history.
list of `BaseMessage`.
If a `BaseLanguageModel` is passed in then
`BaseLanguageModel.get_num_tokens_from_messages()` will be used. Set to
`len` to count the number of **messages** in the chat history.
You can also use string shortcuts for convenience:
- `'approximate'`: Uses `count_tokens_approximately` for fast, approximate
token counts.
!!! note
Use `count_tokens_approximately` to get fast, approximate token
counts.
This is recommended for using `trim_messages` on the hot path, where
exact token counting is not necessary.
`count_tokens_approximately` (or the shortcut `'approximate'`) is
recommended for using `trim_messages` on the hot path, where exact token
counting is not necessary.
strategy: Strategy for trimming.
- `'first'`: Keep the first `<= n_count` tokens of the messages.
- `'last'`: Keep the last `<= n_count` tokens of the messages.
allow_partial: Whether to split a message if only part of the message can be
included. If `strategy='last'` then the last partial contents of a message
are included. If `strategy='first'` then the first partial contents of a
message are included.
end_on: The message type to end on. If specified then every message after the
last occurrence of this type is ignored. If `strategy='last'` then this
is done before we attempt to get the last `max_tokens`. If
`strategy='first'` then this is done after we get the first
`max_tokens`. Can be specified as string names (e.g. `'system'`,
`'human'`, `'ai'`, ...) or as `BaseMessage` classes (e.g.
`SystemMessage`, `HumanMessage`, `AIMessage`, ...). Can be a single
type or a list of types.
included.
start_on: The message type to start on. Should only be specified if
`strategy='last'`. If specified then every message before
the first occurrence of this type is ignored. This is done after we trim
the initial messages to the last `max_tokens`. Does not
apply to a `SystemMessage` at index 0 if `include_system=True`. Can be
specified as string names (e.g. `'system'`, `'human'`, `'ai'`, ...) or
as `BaseMessage` classes (e.g. `SystemMessage`, `HumanMessage`,
`AIMessage`, ...). Can be a single type or a list of types.
If `strategy='last'` then the last partial contents of a message are
included. If `strategy='first'` then the first partial contents of a
message are included.
end_on: The message type to end on.
If specified then every message after the last occurrence of this type is
ignored. If `strategy='last'` then this is done before we attempt to get the
last `max_tokens`. If `strategy='first'` then this is done after we get the
first `max_tokens`. Can be specified as string names (e.g. `'system'`,
`'human'`, `'ai'`, ...) or as `BaseMessage` classes (e.g. `SystemMessage`,
`HumanMessage`, `AIMessage`, ...). Can be a single type or a list of types.
start_on: The message type to start on.
Should only be specified if `strategy='last'`. If specified then every
message before the first occurrence of this type is ignored. This is done
after we trim the initial messages to the last `max_tokens`. Does not apply
to a `SystemMessage` at index 0 if `include_system=True`. Can be specified
as string names (e.g. `'system'`, `'human'`, `'ai'`, ...) or as
`BaseMessage` classes (e.g. `SystemMessage`, `HumanMessage`, `AIMessage`,
...). Can be a single type or a list of types.
include_system: Whether to keep the `SystemMessage` if there is one at index
`0`. Should only be specified if `strategy="last"`.
`0`.
Should only be specified if `strategy="last"`.
text_splitter: Function or `langchain_text_splitters.TextSplitter` for
splitting the string contents of a message. Only used if
`allow_partial=True`. If `strategy='last'` then the last split tokens
from a partial message will be included. if `strategy='first'` then the
first split tokens from a partial message will be included. Token splitter
assumes that separators are kept, so that split contents can be directly
concatenated to recreate the original text. Defaults to splitting on
newlines.
splitting the string contents of a message.
Only used if `allow_partial=True`. If `strategy='last'` then the last split
tokens from a partial message will be included. if `strategy='first'` then
the first split tokens from a partial message will be included. Token
splitter assumes that separators are kept, so that split contents can be
directly concatenated to recreate the original text. Defaults to splitting
on newlines.
Returns:
List of trimmed `BaseMessage`.
@@ -790,8 +1188,8 @@ def trim_messages(
Example:
Trim chat history based on token count, keeping the `SystemMessage` if
present, and ensuring that the chat history starts with a `HumanMessage` (
or a `SystemMessage` followed by a `HumanMessage`).
present, and ensuring that the chat history starts with a `HumanMessage` (or a
`SystemMessage` followed by a `HumanMessage`).
```python
from langchain_core.messages import (
@@ -844,8 +1242,34 @@ def trim_messages(
]
```
Trim chat history using approximate token counting with `'approximate'`:
```python
trim_messages(
messages,
max_tokens=45,
strategy="last",
# Using the "approximate" shortcut for fast token counting
token_counter="approximate",
start_on="human",
include_system=True,
)
# This is equivalent to using `count_tokens_approximately` directly
from langchain_core.messages.utils import count_tokens_approximately
trim_messages(
messages,
max_tokens=45,
strategy="last",
token_counter=count_tokens_approximately,
start_on="human",
include_system=True,
)
```
Trim chat history based on the message count, keeping the `SystemMessage` if
present, and ensuring that the chat history starts with a `HumanMessage` (
present, and ensuring that the chat history starts with a HumanMessage (
or a `SystemMessage` followed by a `HumanMessage`).
trim_messages(
@@ -967,24 +1391,44 @@ def trim_messages(
raise ValueError(msg)
messages = convert_to_messages(messages)
if hasattr(token_counter, "get_num_tokens_from_messages"):
list_token_counter = token_counter.get_num_tokens_from_messages
elif callable(token_counter):
# Handle string shortcuts for token counter
if isinstance(token_counter, str):
if token_counter in _TOKEN_COUNTER_SHORTCUTS:
actual_token_counter = _TOKEN_COUNTER_SHORTCUTS[token_counter]
else:
available_shortcuts = ", ".join(
f"'{key}'" for key in _TOKEN_COUNTER_SHORTCUTS
)
msg = (
f"Invalid token_counter shortcut '{token_counter}'. "
f"Available shortcuts: {available_shortcuts}."
)
raise ValueError(msg)
else:
# Type narrowing: at this point token_counter is not a str
actual_token_counter = token_counter # type: ignore[assignment]
if hasattr(actual_token_counter, "get_num_tokens_from_messages"):
list_token_counter = actual_token_counter.get_num_tokens_from_messages
elif callable(actual_token_counter):
if (
next(iter(inspect.signature(token_counter).parameters.values())).annotation
next(
iter(inspect.signature(actual_token_counter).parameters.values())
).annotation
is BaseMessage
):
def list_token_counter(messages: Sequence[BaseMessage]) -> int:
return sum(token_counter(msg) for msg in messages) # type: ignore[arg-type, misc]
return sum(actual_token_counter(msg) for msg in messages) # type: ignore[arg-type, misc]
else:
list_token_counter = token_counter
list_token_counter = actual_token_counter
else:
msg = (
f"'token_counter' expected to be a model that implements "
f"'get_num_tokens_from_messages()' or a function. Received object of type "
f"{type(token_counter)}."
f"{type(actual_token_counter)}."
)
raise ValueError(msg)
@@ -1019,11 +1463,38 @@ def trim_messages(
raise ValueError(msg)
_SingleMessage = BaseMessage | str | dict[str, Any]
_T = TypeVar("_T", bound=_SingleMessage)
# A sequence of _SingleMessage that is NOT a bare str
_MultipleMessages = Sequence[_T]
@overload
def convert_to_openai_messages(
messages: _SingleMessage,
*,
text_format: Literal["string", "block"] = "string",
include_id: bool = False,
pass_through_unknown_blocks: bool = True,
) -> dict: ...
@overload
def convert_to_openai_messages(
messages: _MultipleMessages,
*,
text_format: Literal["string", "block"] = "string",
include_id: bool = False,
pass_through_unknown_blocks: bool = True,
) -> list[dict]: ...
def convert_to_openai_messages(
messages: MessageLikeRepresentation | Sequence[MessageLikeRepresentation],
*,
text_format: Literal["string", "block"] = "string",
include_id: bool = False,
pass_through_unknown_blocks: bool = True,
) -> dict | list[dict]:
"""Convert LangChain messages into OpenAI message dicts.
@@ -1043,6 +1514,9 @@ def convert_to_openai_messages(
content blocks these are left as is.
include_id: Whether to include message IDs in the openai messages, if they
are present in the source messages.
pass_through_unknown_blocks: Whether to include content blocks with unknown
formats in the output. If `False`, an error is raised if an unknown
content block is encountered.
Raises:
ValueError: if an unrecognized `text_format` is specified, or if a message
@@ -1110,7 +1584,7 @@ def convert_to_openai_messages(
err = f"Unrecognized {text_format=}, expected one of 'string' or 'block'."
raise ValueError(err)
oai_messages: list = []
oai_messages: list[dict] = []
if is_single := isinstance(messages, (BaseMessage, dict, str)):
messages = [messages]
@@ -1292,6 +1766,36 @@ def convert_to_openai_messages(
},
}
)
elif block.get("type") == "function_call": # OpenAI Responses
if not any(
tool_call["id"] == block.get("call_id")
for tool_call in cast("AIMessage", message).tool_calls
):
if missing := [
k
for k in ("call_id", "name", "arguments")
if k not in block
]:
err = (
f"Unrecognized content block at "
f"messages[{i}].content[{j}] has 'type': "
f"'tool_use', but is missing expected key(s) "
f"{missing}. Full content block:\n\n{block}"
)
raise ValueError(err)
oai_msg["tool_calls"] = oai_msg.get("tool_calls", [])
oai_msg["tool_calls"].append(
{
"type": "function",
"id": block.get("call_id"),
"function": {
"name": block.get("name"),
"arguments": block.get("arguments"),
},
}
)
if pass_through_unknown_blocks:
content.append(block)
elif block.get("type") == "tool_result":
if missing := [
k for k in ("content", "tool_use_id") if k not in block
@@ -1372,7 +1876,10 @@ def convert_to_openai_messages(
},
}
)
elif block.get("type") in ["thinking", "reasoning"]:
elif (
block.get("type") in {"thinking", "reasoning"}
or pass_through_unknown_blocks
):
content.append(block)
else:
err = (
@@ -1644,7 +2151,11 @@ def _get_message_openai_role(message: BaseMessage) -> str:
if isinstance(message, ToolMessage):
return "tool"
if isinstance(message, SystemMessage):
return message.additional_kwargs.get("__openai_role__", "system")
role = message.additional_kwargs.get("__openai_role__", "system")
if not isinstance(role, str):
msg = f"Expected '__openai_role__' to be a str, got {type(role).__name__}"
raise TypeError(msg)
return role
if isinstance(message, FunctionMessage):
return "function"
if isinstance(message, ChatMessage):
@@ -1677,26 +2188,29 @@ def count_tokens_approximately(
"""Approximate the total number of tokens in messages.
The token count includes stringified message content, role, and (optionally) name.
- For AI messages, the token count also includes stringified tool calls.
- For tool messages, the token count also includes the tool call ID.
Args:
messages: List of messages to count tokens for.
chars_per_token: Number of characters per token to use for the approximation.
One token corresponds to ~4 chars for common English text.
You can also specify `float` values for more fine-grained control.
[See more here](https://platform.openai.com/tokenizer).
extra_tokens_per_message: Number of extra tokens to add per message, e.g.
special tokens, including beginning/end of message.
You can also specify `float` values for more fine-grained control.
[See more here](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb).
count_name: Whether to include message names in the count.
Enabled by default.
Returns:
Approximate number of tokens in the messages.
!!! note
Note:
This is a simple approximation that may not match the exact token count used by
specific models. For accurate counts, use model-specific tokenizers.
@@ -1704,7 +2218,6 @@ def count_tokens_approximately(
This function does not currently support counting image tokens.
!!! version-added "Added in `langchain-core` 0.3.46"
"""
token_count = 0.0
for message in convert_to_messages(messages):
@@ -1745,3 +2258,14 @@ def count_tokens_approximately(
# round up once more time in case extra_tokens_per_message is a float
return math.ceil(token_count)
# Mapping from string shortcuts to token counter functions
def _approximate_token_counter(messages: Sequence[BaseMessage]) -> int:
"""Wrapper for `count_tokens_approximately` that matches expected signature."""
return count_tokens_approximately(messages)
_TOKEN_COUNTER_SHORTCUTS = {
"approximate": _approximate_token_counter,
}

View File

@@ -9,6 +9,7 @@ from typing import (
Any,
Generic,
TypeVar,
cast,
)
from typing_extensions import override
@@ -46,7 +47,7 @@ class BaseLLMOutputParser(ABC, Generic[T]):
async def aparse_result(
self, result: list[Generation], *, partial: bool = False
) -> T:
"""Async parse a list of candidate model `Generation` objects into a specific format.
"""Parse a list of candidate model `Generation` objects into a specific format.
Args:
result: A list of `Generation` to be parsed. The Generations are assumed
@@ -56,7 +57,7 @@ class BaseLLMOutputParser(ABC, Generic[T]):
Returns:
Structured output.
""" # noqa: E501
"""
return await run_in_executor(None, self.parse_result, result, partial=partial)
@@ -77,7 +78,7 @@ class BaseGenerationOutputParser(
"""Return the output type for the parser."""
# even though mypy complains this isn't valid,
# it is good enough for pydantic to build the schema from
return T # type: ignore[misc]
return cast("type[T]", T) # type: ignore[misc]
@override
def invoke(
@@ -181,7 +182,7 @@ class BaseOutputParser(
if hasattr(base, "__pydantic_generic_metadata__"):
metadata = base.__pydantic_generic_metadata__
if "args" in metadata and len(metadata["args"]) > 0:
return metadata["args"][0]
return cast("type[T]", metadata["args"][0])
msg = (
f"Runnable {self.__class__.__name__} doesn't have an inferable OutputType. "
@@ -267,7 +268,7 @@ class BaseOutputParser(
async def aparse_result(
self, result: list[Generation], *, partial: bool = False
) -> T:
"""Async parse a list of candidate model `Generation` objects into a specific format.
"""Parse a list of candidate model `Generation` objects into a specific format.
The return value is parsed from only the first `Generation` in the result, which
is assumed to be the highest-likelihood `Generation`.
@@ -280,7 +281,7 @@ class BaseOutputParser(
Returns:
Structured output.
""" # noqa: E501
"""
return await run_in_executor(None, self.parse_result, result, partial=partial)
async def aparse(self, text: str) -> T:

View File

@@ -37,7 +37,7 @@ class OutputFunctionsParser(BaseGenerationOutputParser[Any]):
The parsed JSON object.
Raises:
`OutputParserException`: If the output is not valid JSON.
OutputParserException: If the output is not valid JSON.
"""
generation = result[0]
if not isinstance(generation, ChatGeneration):
@@ -88,7 +88,7 @@ class JsonOutputFunctionsParser(BaseCumulativeTransformOutputParser[Any]):
The parsed JSON object.
Raises:
OutputParserExcept`ion: If the output is not valid JSON.
OutputParserException: If the output is not valid JSON.
"""
if len(result) != 1:
msg = f"Expected exactly one result, but got {len(result)}"
@@ -228,7 +228,7 @@ class PydanticOutputFunctionsParser(OutputFunctionsParser):
@model_validator(mode="before")
@classmethod
def validate_schema(cls, values: dict) -> Any:
def validate_schema(cls, values: dict[str, Any]) -> Any:
"""Validate the Pydantic schema.
Args:

View File

@@ -47,22 +47,24 @@ def parse_tool_call(
"""
if "function" not in raw_tool_call:
return None
arguments = raw_tool_call["function"]["arguments"]
if partial:
try:
function_args = parse_partial_json(
raw_tool_call["function"]["arguments"], strict=strict
)
function_args = parse_partial_json(arguments, strict=strict)
except (JSONDecodeError, TypeError): # None args raise TypeError
return None
# Handle None or empty string arguments for parameter-less tools
elif not arguments:
function_args = {}
else:
try:
function_args = json.loads(
raw_tool_call["function"]["arguments"], strict=strict
)
function_args = json.loads(arguments, strict=strict)
except JSONDecodeError as e:
msg = (
f"Function {raw_tool_call['function']['name']} arguments:\n\n"
f"{raw_tool_call['function']['arguments']}\n\nare not valid JSON. "
f"{arguments}\n\nare not valid JSON. "
f"Received JSONDecodeError {e}"
)
raise OutputParserException(msg) from e

View File

@@ -1,7 +1,7 @@
"""Output parsers using Pydantic."""
import json
from typing import Annotated, Generic
from typing import Annotated, Generic, Literal, overload
import pydantic
from pydantic import SkipValidation
@@ -42,6 +42,16 @@ class PydanticOutputParser(JsonOutputParser, Generic[TBaseModel]):
msg = f"Failed to parse {name} from completion {json_string}. Got: {e}"
return OutputParserException(msg, llm_output=json_string)
@overload
def parse_result(
self, result: list[Generation], *, partial: Literal[False] = False
) -> TBaseModel: ...
@overload
def parse_result(
self, result: list[Generation], *, partial: bool = False
) -> TBaseModel | None: ...
def parse_result(
self, result: list[Generation], *, partial: bool = False
) -> TBaseModel | None:
@@ -54,7 +64,7 @@ class PydanticOutputParser(JsonOutputParser, Generic[TBaseModel]):
all the keys that have been returned so far.
Raises:
`OutputParserException`: If the result is not valid JSON
OutputParserException: If the result is not valid JSON
or does not conform to the Pydantic model.
Returns:
@@ -77,7 +87,7 @@ class PydanticOutputParser(JsonOutputParser, Generic[TBaseModel]):
Returns:
The parsed Pydantic object.
"""
return super().parse(text)
return self.parse_result([Generation(text=text)])
def get_format_instructions(self) -> str:
"""Return the format instructions for the JSON output.

View File

@@ -6,7 +6,33 @@ from langchain_core.output_parsers.transform import BaseTransformOutputParser
class StrOutputParser(BaseTransformOutputParser[str]):
"""OutputParser that parses `LLMResult` into the top likely string."""
"""Extract text content from model outputs as a string.
Converts model outputs (such as `AIMessage` or `AIMessageChunk` objects) into plain
text strings. It's the simplest output parser and is useful when you need string
responses for downstream processing, display, or storage.
Supports streaming, yielding text chunks as they're generated by the model.
Example:
```python
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o")
parser = StrOutputParser()
# Get string output from a model
message = model.invoke("Tell me a joke")
result = parser.invoke(message)
print(result) # plain string
# With streaming - use transform() to process a stream
stream = model.stream("Tell me a story")
for chunk in parser.transform(stream):
print(chunk, end="", flush=True)
```
"""
@classmethod
def is_lc_serializable(cls) -> bool:

View File

@@ -57,16 +57,18 @@ class ChatGeneration(Generation):
text = ""
if isinstance(self.message.content, str):
text = self.message.content
# Assumes text in content blocks in OpenAI format.
# Uses first text block.
# Extracts first text block from content blocks.
# Skips blocks with explicit non-text type (e.g., thinking, reasoning).
elif isinstance(self.message.content, list):
for block in self.message.content:
if isinstance(block, str):
text = block
break
if isinstance(block, dict) and "text" in block:
text = block["text"]
break
block_type = block.get("type")
if block_type is None or block_type == "text":
text = block["text"]
break
self.text = text
return self

View File

@@ -104,18 +104,31 @@ class ChatPromptValue(PromptValue):
class ImageURL(TypedDict, total=False):
"""Image URL."""
"""Image URL for multimodal model inputs (OpenAI format).
Represents the inner `image_url` object in OpenAI's Chat Completion API format. This
is used by `ImagePromptTemplate` and `ChatPromptTemplate`.
See Also:
`ImageContentBlock`: LangChain's provider-agnostic image format used in message
content blocks. Use `ImageContentBlock` when working with the standardized
message format across different providers.
Note:
The `detail` field values are not validated locally. Invalid values
will be rejected by the downstream API, allowing new valid values to
be used without requiring a LangChain update.
"""
detail: Literal["auto", "low", "high"]
"""Specifies the detail level of the image.
Can be `'auto'`, `'low'`, or `'high'`.
This follows OpenAI's Chat Completion API's image URL format.
Defaults to ``'auto'`` if not specified. Higher detail levels consume
more tokens but provide better image understanding.
"""
url: str
"""Either a URL of the image or the base64 encoded image data."""
"""URL of the image or base64-encoded image data."""
class ImagePromptValue(PromptValue):

View File

@@ -2,19 +2,14 @@
from __future__ import annotations
import builtins # noqa: TC003
import contextlib
import json
import typing
from abc import ABC, abstractmethod
from collections.abc import Mapping
from collections.abc import Mapping # noqa: TC003
from functools import cached_property
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
Generic,
TypeVar,
)
from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
import yaml
from pydantic import BaseModel, ConfigDict, Field, model_validator
@@ -22,7 +17,7 @@ from typing_extensions import Self, override
from langchain_core.exceptions import ErrorCode, create_message
from langchain_core.load import dumpd
from langchain_core.output_parsers.base import BaseOutputParser
from langchain_core.output_parsers.base import BaseOutputParser # noqa: TC001
from langchain_core.prompt_values import (
ChatPromptValueConcrete,
PromptValue,
@@ -56,7 +51,7 @@ class BasePromptTemplate(
These variables are auto inferred from the prompt and user need not provide them.
"""
input_types: typing.Dict[str, Any] = Field(default_factory=dict, exclude=True) # noqa: UP006
input_types: builtins.dict[str, Any] = Field(default_factory=dict, exclude=True)
"""A dictionary of the types of the variables the prompt template expects.
If not provided, all variables are assumed to be strings.
@@ -69,7 +64,7 @@ class BasePromptTemplate(
Partial variables populate the template so that you don't need to pass them in every
time you call the prompt.
"""
metadata: typing.Dict[str, Any] | None = None # noqa: UP006
metadata: builtins.dict[str, Any] | None = None
"""Metadata to be used for tracing."""
tags: list[str] | None = None
"""Tags to be used for tracing."""
@@ -122,7 +117,10 @@ class BasePromptTemplate(
@cached_property
def _serialized(self) -> dict[str, Any]:
return dumpd(self)
# self is always a Serializable object in this case, thus the result is
# guaranteed to be a dict since dumpd uses the default callback, which uses
# obj.to_json which always returns TypedDict subclasses
return cast("dict[str, Any]", dumpd(self))
@property
@override
@@ -156,7 +154,7 @@ class BasePromptTemplate(
if not isinstance(inner_input, dict):
if len(self.input_variables) == 1:
var_name = self.input_variables[0]
inner_input = {var_name: inner_input}
inner_input_ = {var_name: inner_input}
else:
msg = (
@@ -168,12 +166,14 @@ class BasePromptTemplate(
message=msg, error_code=ErrorCode.INVALID_PROMPT_INPUT
)
)
missing = set(self.input_variables).difference(inner_input)
else:
inner_input_ = inner_input
missing = set(self.input_variables).difference(inner_input_)
if missing:
msg = (
f"Input to {self.__class__.__name__} is missing variables {missing}. "
f" Expected: {self.input_variables}"
f" Received: {list(inner_input.keys())}"
f" Received: {list(inner_input_.keys())}"
)
example_key = missing.pop()
msg += (
@@ -184,7 +184,7 @@ class BasePromptTemplate(
raise KeyError(
create_message(message=msg, error_code=ErrorCode.INVALID_PROMPT_INPUT)
)
return inner_input
return inner_input_
def _format_prompt_with_error_handling(self, inner_input: dict) -> PromptValue:
inner_input_ = self._validate_input(inner_input)

View File

@@ -3,9 +3,9 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import Sequence
from pathlib import Path
from typing import (
TYPE_CHECKING,
Annotated,
Any,
TypedDict,
@@ -48,9 +48,6 @@ from langchain_core.prompts.string import (
from langchain_core.utils import get_colored_text
from langchain_core.utils.interactive_env import is_interactive_env
if TYPE_CHECKING:
from collections.abc import Sequence
class MessagesPlaceholder(BaseMessagePromptTemplate):
"""Prompt template that assumes variable is already list of messages.
@@ -765,7 +762,7 @@ MessageLike = BaseMessagePromptTemplate | BaseMessage | BaseChatPromptTemplate
MessageLikeRepresentation = (
MessageLike
| tuple[str | type, str | list[dict] | list[object]]
| tuple[str | type, str | Sequence[dict] | Sequence[object]]
| str
| dict[str, Any]
)
@@ -848,9 +845,9 @@ class ChatPromptTemplate(BaseChatPromptTemplate):
!!! note "Single-variable template"
If your prompt has only a single input variable (i.e., 1 instance of "{variable_nams}"),
and you invoke the template with a non-dict object, the prompt template will
inject the provided argument into that variable location.
If your prompt has only a single input variable (i.e., 1 instance of
"{variable_nams}"), and you invoke the template with a non-dict object, the
prompt template will inject the provided argument into that variable location.
```python
from langchain_core.prompts import ChatPromptTemplate
@@ -874,7 +871,7 @@ class ChatPromptTemplate(BaseChatPromptTemplate):
# ]
# )
```
""" # noqa: E501
"""
messages: Annotated[list[MessageLike], SkipValidation()]
"""List of messages consisting of either message prompt templates or messages."""
@@ -1428,16 +1425,26 @@ def _convert_to_message_template(
f" Got: {message}"
)
raise ValueError(msg)
message = (message["role"], message["content"])
try:
message_type_str = message["role"]
template = message["content"]
else:
if len(message) != 2: # noqa: PLR2004
msg = f"Expected 2-tuple of (role, template), got {message}"
raise ValueError(msg)
message_type_str, template = message
except ValueError as e:
msg = f"Expected 2-tuple of (role, template), got {message}"
raise ValueError(msg) from e
if isinstance(message_type_str, str):
message_ = _create_template_from_message_type(
message_type_str, template, template_format=template_format
)
elif (
hasattr(message_type_str, "model_fields")
and "type" in message_type_str.model_fields
):
message_type = message_type_str.model_fields["type"].default
message_ = _create_template_from_message_type(
message_type, template, template_format=template_format
)
else:
message_ = message_type_str(
prompt=PromptTemplate.from_template(

View File

@@ -2,7 +2,7 @@
import warnings
from functools import cached_property
from typing import Any, Literal
from typing import Any, Literal, cast
from typing_extensions import override
@@ -65,7 +65,10 @@ class DictPromptTemplate(RunnableSerializable[dict, dict]):
@cached_property
def _serialized(self) -> dict[str, Any]:
return dumpd(self)
# self is always a Serializable object in this case, thus the result is
# guaranteed to be a dict since dumpd uses the default callback, which uses
# obj.to_json which always returns TypedDict subclasses
return cast("dict[str, Any]", dumpd(self))
@classmethod
def is_lc_serializable(cls) -> bool:
@@ -116,7 +119,7 @@ def _insert_input_variables(
inputs: dict[str, Any],
template_format: Literal["f-string", "mustache"],
) -> dict[str, Any]:
formatted = {}
formatted: dict[str, Any] = {}
formatter = DEFAULT_FORMATTER_MAPPING[template_format]
for k, v in template.items():
if isinstance(v, str):
@@ -132,7 +135,7 @@ def _insert_input_variables(
warnings.warn(msg, stacklevel=2)
formatted[k] = _insert_input_variables(v, inputs, template_format)
elif isinstance(v, (list, tuple)):
formatted_v = []
formatted_v: list[str | dict[str, Any]] = []
for x in v:
if isinstance(x, str):
formatted_v.append(formatter(x, **inputs))

View File

@@ -6,6 +6,7 @@ from typing import Any
from pydantic import ConfigDict, model_validator
from typing_extensions import Self
from langchain_core.example_selectors import BaseExampleSelector
from langchain_core.prompts.prompt import PromptTemplate
from langchain_core.prompts.string import (
DEFAULT_FORMATTER_MAPPING,
@@ -21,7 +22,7 @@ class FewShotPromptWithTemplates(StringPromptTemplate):
"""Examples to format into the prompt.
Either this or example_selector should be provided."""
example_selector: Any = None
example_selector: BaseExampleSelector | None = None
"""ExampleSelector to choose the examples to format into the prompt.
Either this or examples should be provided."""

View File

@@ -1,6 +1,6 @@
"""Image prompt template for a multimodal model."""
from typing import Any
from typing import Any, Literal, cast
from pydantic import Field
@@ -125,7 +125,7 @@ class ImagePromptTemplate(BasePromptTemplate[ImageURL]):
output: ImageURL = {"url": url}
if detail:
# Don't check literal values here: let the API check them
output["detail"] = detail
output["detail"] = cast("Literal['auto', 'low', 'high']", detail)
return output
async def aformat(self, **kwargs: Any) -> ImageURL:

View File

@@ -92,4 +92,4 @@ class BaseMessagePromptTemplate(Serializable, ABC):
from langchain_core.prompts.chat import ChatPromptTemplate # noqa: PLC0415
prompt = ChatPromptTemplate(messages=[self])
return prompt + other
return prompt.__add__(other)

View File

@@ -3,11 +3,12 @@
from __future__ import annotations
import warnings
from abc import ABC
from abc import ABC, abstractmethod
from string import Formatter
from typing import TYPE_CHECKING, Any, Literal
from typing import TYPE_CHECKING, Any, Literal, cast
from pydantic import BaseModel, create_model
from typing_extensions import override
from langchain_core.prompt_values import PromptValue, StringPromptValue
from langchain_core.prompts.base import BasePromptTemplate
@@ -20,65 +21,8 @@ if TYPE_CHECKING:
try:
from jinja2 import meta
from jinja2.exceptions import SecurityError
from jinja2.sandbox import SandboxedEnvironment
class _RestrictedSandboxedEnvironment(SandboxedEnvironment):
"""A more restrictive Jinja2 sandbox that blocks all attribute/method access.
This sandbox only allows simple variable lookups, no attribute or method access.
This prevents template injection attacks via methods like parse_raw().
"""
def is_safe_attribute(self, _obj: Any, _attr: str, _value: Any) -> bool:
"""Block ALL attribute access for security.
Only allow accessing variables directly from the context dict,
no attribute access on those objects.
Args:
_obj: The object being accessed (unused, always blocked).
_attr: The attribute name (unused, always blocked).
_value: The attribute value (unused, always blocked).
Returns:
False - all attribute access is blocked.
"""
# Block all attribute access
return False
def is_safe_callable(self, _obj: Any) -> bool:
"""Block all method calls for security.
Args:
_obj: The object being checked (unused, always blocked).
Returns:
False - all callables are blocked.
"""
return False
def getattr(self, obj: Any, attribute: str) -> Any:
"""Override getattr to block all attribute access.
Args:
obj: The object.
attribute: The attribute name.
Returns:
Never returns.
Raises:
SecurityError: Always, to block attribute access.
"""
msg = (
f"Access to attributes is not allowed in templates. "
f"Attempted to access '{attribute}' on {type(obj).__name__}. "
f"Use only simple variable names like {{{{variable}}}} "
f"without dots or methods."
)
raise SecurityError(msg)
_HAS_JINJA2 = True
except ImportError:
_HAS_JINJA2 = False
@@ -121,7 +65,7 @@ def jinja2_formatter(template: str, /, **kwargs: Any) -> str:
# Use a restricted sandbox that blocks ALL attribute/method access
# Only simple variable lookups like {{variable}} are allowed
# Attribute access like {{variable.attr}} or {{variable.method()}} is blocked
return _RestrictedSandboxedEnvironment().from_string(template).render(**kwargs)
return SandboxedEnvironment().from_string(template).render(**kwargs)
def validate_jinja2(template: str, input_variables: list[str]) -> None:
@@ -156,7 +100,7 @@ def _get_jinja2_variables_from_template(template: str) -> set[str]:
"Please install it with `pip install jinja2`."
)
raise ImportError(msg)
env = _RestrictedSandboxedEnvironment()
env = SandboxedEnvironment()
ast = env.parse(template)
return meta.find_undeclared_variables(ast)
@@ -246,17 +190,20 @@ def mustache_schema(template: str) -> type[BaseModel]:
return _create_model_recursive("PromptInput", defs)
def _create_model_recursive(name: str, defs: Defs) -> type:
return create_model( # type: ignore[call-overload]
name,
**{
k: (_create_model_recursive(k, v), None) if v else (type(v), None)
for k, v in defs.items()
},
def _create_model_recursive(name: str, defs: Defs) -> type[BaseModel]:
return cast(
"type[BaseModel]",
create_model( # type: ignore[call-overload]
name,
**{
k: (_create_model_recursive(k, v), None) if v else (type(v), None)
for k, v in defs.items()
},
),
)
DEFAULT_FORMATTER_MAPPING: dict[str, Callable] = {
DEFAULT_FORMATTER_MAPPING: dict[str, Callable[..., str]] = {
"f-string": formatter.format,
"mustache": mustache_formatter,
"jinja2": jinja2_formatter,
@@ -387,6 +334,10 @@ class StringPromptTemplate(BasePromptTemplate, ABC):
"""
return StringPromptValue(text=await self.aformat(**kwargs))
@override
@abstractmethod
def format(self, **kwargs: Any) -> str: ...
def pretty_repr(
self,
html: bool = False, # noqa: FBT001,FBT002

View File

@@ -48,6 +48,9 @@ class StructuredPrompt(ChatPromptTemplate):
schema_: schema for the structured prompt.
structured_output_kwargs: additional kwargs for structured output.
template_format: template format for the prompt.
Raises:
ValueError: if schema is not provided.
"""
schema_ = schema_ or kwargs.pop("schema", None)
if not schema_:

View File

@@ -94,7 +94,7 @@ from langchain_core.tracers.root_listeners import (
AsyncRootListenersTracer,
RootListenersTracer,
)
from langchain_core.utils.aiter import aclosing, atee, py_anext
from langchain_core.utils.aiter import aclosing, atee
from langchain_core.utils.iter import safetee
from langchain_core.utils.pydantic import create_model_v2
@@ -127,10 +127,10 @@ class Runnable(ABC, Generic[Input, Output]):
Key Methods
===========
- **`invoke`/`ainvoke`**: Transforms a single input into an output.
- **`batch`/`abatch`**: Efficiently transforms multiple inputs into outputs.
- **`stream`/`astream`**: Streams output from a single input as it's produced.
- **`astream_log`**: Streams output and selected intermediate results from an
- `invoke`/`ainvoke`: Transforms a single input into an output.
- `batch`/`abatch`: Efficiently transforms multiple inputs into outputs.
- `stream`/`astream`: Streams output from a single input as it's produced.
- `astream_log`: Streams output and selected intermediate results from an
input.
Built-in optimizations:
@@ -315,7 +315,7 @@ class Runnable(ABC, Generic[Input, Output]):
"args" in metadata
and len(metadata["args"]) == _RUNNABLE_GENERIC_NUM_ARGS
):
return metadata["args"][0]
return cast("type[Input]", metadata["args"][0])
# If we didn't find a Pydantic model in the parent classes,
# then loop through __orig_bases__. This corresponds to
@@ -323,7 +323,7 @@ class Runnable(ABC, Generic[Input, Output]):
for cls in self.__class__.__orig_bases__: # type: ignore[attr-defined]
type_args = get_args(cls)
if type_args and len(type_args) == _RUNNABLE_GENERIC_NUM_ARGS:
return type_args[0]
return cast("type[Input]", type_args[0])
msg = (
f"Runnable {self.get_name()} doesn't have an inferable InputType. "
@@ -349,12 +349,12 @@ class Runnable(ABC, Generic[Input, Output]):
"args" in metadata
and len(metadata["args"]) == _RUNNABLE_GENERIC_NUM_ARGS
):
return metadata["args"][1]
return cast("type[Output]", metadata["args"][1])
for cls in self.__class__.__orig_bases__: # type: ignore[attr-defined]
type_args = get_args(cls)
if type_args and len(type_args) == _RUNNABLE_GENERIC_NUM_ARGS:
return type_args[1]
return cast("type[Output]", type_args[1])
msg = (
f"Runnable {self.get_name()} doesn't have an inferable OutputType. "
@@ -369,7 +369,7 @@ class Runnable(ABC, Generic[Input, Output]):
def get_input_schema(
self,
config: RunnableConfig | None = None, # noqa: ARG002
config: RunnableConfig | None = None,
) -> type[BaseModel]:
"""Get a Pydantic model that can be used to validate input to the `Runnable`.
@@ -385,6 +385,7 @@ class Runnable(ABC, Generic[Input, Output]):
Returns:
A Pydantic model that can be used to validate input.
"""
_ = config
root_type = self.InputType
if (
@@ -447,7 +448,7 @@ class Runnable(ABC, Generic[Input, Output]):
def get_output_schema(
self,
config: RunnableConfig | None = None, # noqa: ARG002
config: RunnableConfig | None = None,
) -> type[BaseModel]:
"""Get a Pydantic model that can be used to validate output to the `Runnable`.
@@ -463,6 +464,7 @@ class Runnable(ABC, Generic[Input, Output]):
Returns:
A Pydantic model that can be used to validate output.
"""
_ = config
root_type = self.OutputType
if (
@@ -2277,6 +2279,9 @@ class Runnable(ABC, Generic[Input, Output]):
Use this to implement `stream` or `transform` in `Runnable` subclasses.
"""
# Extract defers_inputs from kwargs if present
defers_inputs = kwargs.pop("defers_inputs", False)
# tee the input so we can iterate over it twice
input_for_tracing, input_for_transform = tee(inputs, 2)
# Start the input iterator to ensure the input Runnable starts before this one
@@ -2293,6 +2298,7 @@ class Runnable(ABC, Generic[Input, Output]):
run_type=run_type,
name=config.get("run_name") or self.get_name(),
run_id=config.pop("run_id", None),
defers_inputs=defers_inputs,
)
try:
child_config = patch_config(config, callbacks=run_manager.get_child())
@@ -2374,10 +2380,13 @@ class Runnable(ABC, Generic[Input, Output]):
Use this to implement `astream` or `atransform` in `Runnable` subclasses.
"""
# Extract defers_inputs from kwargs if present
defers_inputs = kwargs.pop("defers_inputs", False)
# tee the input so we can iterate over it twice
input_for_tracing, input_for_transform = atee(inputs, 2)
# Start the input iterator to ensure the input Runnable starts before this one
final_input: Input | None = await py_anext(input_for_tracing, None)
final_input: Input | None = await anext(input_for_tracing, None)
final_input_supported = True
final_output: Output | None = None
final_output_supported = True
@@ -2390,6 +2399,7 @@ class Runnable(ABC, Generic[Input, Output]):
run_type=run_type,
name=config.get("run_name") or self.get_name(),
run_id=config.pop("run_id", None),
defers_inputs=defers_inputs,
)
try:
child_config = patch_config(config, callbacks=run_manager.get_child())
@@ -2417,7 +2427,7 @@ class Runnable(ABC, Generic[Input, Output]):
iterator = iterator_
try:
while True:
chunk = await coro_with_context(py_anext(iterator), context)
chunk = await coro_with_context(anext(iterator), context)
yield chunk
if final_output_supported:
if final_output is None:
@@ -4025,7 +4035,7 @@ class RunnableParallel(RunnableSerializable[Input, dict[str, Any]]):
# Wrap in a coroutine to satisfy linter
async def get_next_chunk(generator: AsyncIterator) -> Output | None:
return await py_anext(generator)
return await anext(generator)
# Start the first iteration of each generator
tasks = {
@@ -4323,6 +4333,7 @@ class RunnableGenerator(Runnable[Input, Output]):
input,
self._transform, # type: ignore[arg-type]
config,
defers_inputs=True,
**kwargs,
)
@@ -4356,7 +4367,7 @@ class RunnableGenerator(Runnable[Input, Output]):
raise NotImplementedError(msg)
return self._atransform_stream_with_config(
input, self._atransform, config, **kwargs
input, self._atransform, config, defers_inputs=True, **kwargs
)
@override
@@ -4429,6 +4440,138 @@ class RunnableLambda(Runnable[Input, Output]):
```
"""
@overload
def __init__(
self,
func: Callable[[Input, RunnableConfig], Awaitable[Output]],
afunc: None = None,
name: str | None = None,
) -> None: ...
@overload
def __init__(
self,
func: Callable[[Input], Awaitable[Output]],
afunc: None = None,
name: str | None = None,
) -> None: ...
@overload
def __init__(
self,
func: Callable[[Input], AsyncIterator[Output]],
afunc: None = None,
name: str | None = None,
) -> None: ...
@overload
def __init__(
self,
func: Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]],
afunc: None = None,
name: str | None = None,
) -> None: ...
@overload
def __init__(
self,
func: Callable[
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
],
afunc: None = None,
name: str | None = None,
) -> None: ...
@overload
def __init__(
self,
func: Callable[[Input, RunnableConfig], Output],
afunc: Callable[[Input], Awaitable[Output]]
| Callable[[Input], AsyncIterator[Output]]
| Callable[[Input, RunnableConfig], Awaitable[Output]]
| Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]]
| Callable[
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
]
| None = None,
name: str | None = None,
) -> None: ...
@overload
def __init__(
self,
func: Callable[[Input], Iterator[Output]],
afunc: Callable[[Input], Awaitable[Output]]
| Callable[[Input], AsyncIterator[Output]]
| Callable[[Input, RunnableConfig], Awaitable[Output]]
| Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]]
| Callable[
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
]
| None = None,
name: str | None = None,
) -> None: ...
@overload
def __init__(
self,
func: Callable[[Input], Runnable[Input, Output]],
afunc: Callable[[Input], Awaitable[Output]]
| Callable[[Input], AsyncIterator[Output]]
| Callable[[Input, RunnableConfig], Awaitable[Output]]
| Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]]
| Callable[
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
]
| None = None,
name: str | None = None,
) -> None: ...
@overload
def __init__(
self,
func: Callable[[Input, CallbackManagerForChainRun], Output],
afunc: Callable[[Input], Awaitable[Output]]
| Callable[[Input], AsyncIterator[Output]]
| Callable[[Input, RunnableConfig], Awaitable[Output]]
| Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]]
| Callable[
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
]
| None = None,
name: str | None = None,
) -> None: ...
@overload
def __init__(
self,
func: Callable[[Input, CallbackManagerForChainRun, RunnableConfig], Output],
afunc: Callable[[Input], Awaitable[Output]]
| Callable[[Input], AsyncIterator[Output]]
| Callable[[Input, RunnableConfig], Awaitable[Output]]
| Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]]
| Callable[
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
]
| None = None,
name: str | None = None,
) -> None: ...
@overload
def __init__(
self,
func: Callable[[Input], Output],
afunc: Callable[[Input], Awaitable[Output]]
| Callable[[Input], AsyncIterator[Output]]
| Callable[[Input, RunnableConfig], Awaitable[Output]]
| Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]]
| Callable[
[Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
]
| None = None,
name: str | None = None,
) -> None: ...
def __init__(
self,
func: Callable[[Input], Iterator[Output]]

View File

@@ -3,7 +3,9 @@
from __future__ import annotations
import asyncio
import uuid
# Cannot move uuid to TYPE_CHECKING as RunnableConfig is used in Pydantic models
import uuid # noqa: TC003
import warnings
from collections.abc import Awaitable, Callable, Generator, Iterable, Iterator, Sequence
from concurrent.futures import Executor, Future, ThreadPoolExecutor
@@ -49,8 +51,24 @@ class EmptyDict(TypedDict, total=False):
class RunnableConfig(TypedDict, total=False):
"""Configuration for a `Runnable`.
See the [reference docs](https://reference.langchain.com/python/langchain_core/runnables/#langchain_core.runnables.RunnableConfig)
for more details.
!!! note Custom values
The `TypedDict` has `total=False` set intentionally to:
- Allow partial configs to be created and merged together via `merge_configs`
- Support config propagation from parent to child runnables via
`var_child_runnable_config` (a `ContextVar` that automatically passes
config down the call stack without explicit parameter passing), where
configs are merged rather than replaced
!!! example
```python
# Parent sets tags
chain.invoke(input, config={"tags": ["parent"]})
# Child automatically inherits and can add:
# ensure_config({"tags": ["child"]}) -> {"tags": ["parent", "child"]}
```
"""
tags: list[str]
@@ -90,7 +108,8 @@ class RunnableConfig(TypedDict, total=False):
configurable: dict[str, Any]
"""Runtime values for attributes previously made configurable on this `Runnable`,
or sub-Runnables, through `configurable_fields` or `configurable_alternatives`.
or sub-`Runnable` objects, through `configurable_fields` or
`configurable_alternatives`.
Check `output_schema` for a description of the attributes that have been made
configurable.

View File

@@ -28,7 +28,6 @@ from langchain_core.runnables.utils import (
coro_with_context,
get_unique_config_specs,
)
from langchain_core.utils.aiter import py_anext
if TYPE_CHECKING:
from langchain_core.callbacks.manager import AsyncCallbackManagerForChainRun
@@ -563,7 +562,7 @@ class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
child_config,
**kwargs,
)
chunk = await coro_with_context(py_anext(stream), context)
chunk = await coro_with_context(anext(stream), context)
except self.exceptions_to_handle as e:
first_error = e if first_error is None else first_error
last_error = e

View File

@@ -165,6 +165,9 @@ class AsciiCanvas:
y0: y coordinate of the box corner.
width: box width.
height: box height.
Raises:
ValueError: if box dimensions are invalid.
"""
if width <= 1 or height <= 1:
msg = "Box dimensions should be > 1"

View File

@@ -8,9 +8,10 @@ import random
import re
import string
import time
import urllib.parse
from dataclasses import asdict
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal
from typing import TYPE_CHECKING, Any, Literal, cast
import yaml
@@ -40,6 +41,8 @@ except ImportError:
MARKDOWN_SPECIAL_CHARS = "*_`"
_HEX_COLOR_PATTERN = re.compile(r"^#(?:[0-9a-fA-F]{3}){1,2}$")
def draw_mermaid(
nodes: dict[str, Node],
@@ -81,6 +84,7 @@ def draw_mermaid(
}
}
```
Returns:
Mermaid graph syntax.
@@ -389,7 +393,7 @@ async def _render_mermaid_using_pyppeteer(
}
)
img_bytes = await page.screenshot({"fullPage": False})
img_bytes = cast("bytes", await page.screenshot({"fullPage": False}))
await browser.close()
if output_file_path is not None:
@@ -428,14 +432,14 @@ def _render_mermaid_using_api(
)
# Check if the background color is a hexadecimal color code using regex
if background_color is not None:
hex_color_pattern = re.compile(r"^#(?:[0-9a-fA-F]{3}){1,2}$")
if not hex_color_pattern.match(background_color):
background_color = f"!{background_color}"
if background_color is not None and not _HEX_COLOR_PATTERN.match(background_color):
background_color = f"!{background_color}"
# URL-encode the background_color to handle special characters like '!'
encoded_bg_color = urllib.parse.quote(str(background_color), safe="")
image_url = (
f"{base_url}/img/{mermaid_syntax_encoded}"
f"?type={file_type}&bgColor={background_color}"
f"?type={file_type}&bgColor={encoded_bg_color}"
)
error_msg_suffix = (

View File

@@ -1,7 +1,7 @@
"""Helper class to draw a state graph into a PNG file."""
from itertools import groupby
from typing import Any
from typing import Any, cast
from langchain_core.runnables.graph import Graph, LabelsDict
@@ -149,7 +149,7 @@ class PngDrawer:
# Save the graph as PNG
try:
return viz.draw(output_path, format="png", prog="dot")
return cast("bytes | None", viz.draw(output_path, format="png", prog="dot"))
finally:
viz.close()
@@ -201,7 +201,8 @@ class PngDrawer:
viz, start, end, str(data) if data is not None else None, cond
)
def update_styles(self, viz: Any, graph: Graph) -> None:
@staticmethod
def update_styles(viz: Any, graph: Graph) -> None:
"""Update the styles of the entrypoint and END nodes.
Args:

View File

@@ -320,7 +320,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
`RunnableBindingBase` init.
"""
history_chain: Runnable = RunnableLambda(
history_chain: Runnable[Any, Any] = RunnableLambda(
self._enter_history, self._aenter_history
).with_config(run_name="load_history")
messages_key = history_messages_key or input_messages_key
@@ -329,16 +329,16 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
**{messages_key: history_chain}
).with_config(run_name="insert_history")
runnable_sync: Runnable = runnable.with_listeners(on_end=self._exit_history)
runnable_async: Runnable = runnable.with_alisteners(on_end=self._aexit_history)
runnable_sync = runnable.with_listeners(on_end=self._exit_history)
runnable_async = runnable.with_alisteners(on_end=self._aexit_history)
def _call_runnable_sync(_input: Any) -> Runnable:
def _call_runnable_sync(_input: Any) -> Runnable[Any, Any]:
return runnable_sync
async def _call_runnable_async(_input: Any) -> Runnable:
async def _call_runnable_async(_input: Any) -> Runnable[Any, Any]:
return runnable_async
bound: Runnable = (
bound = (
history_chain
| RunnableLambda(
_call_runnable_sync,
@@ -539,7 +539,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
hist: BaseChatMessageHistory = config["configurable"]["message_history"]
# Get the input messages
inputs = load(run.inputs)
inputs = load(run.inputs, allowed_objects="all")
input_messages = self._get_input_messages(inputs)
# If historic messages were prepended to the input messages, remove them to
# avoid adding duplicate messages to history.
@@ -548,7 +548,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
input_messages = input_messages[len(historic_messages) :]
# Get the output messages
output_val = load(run.outputs)
output_val = load(run.outputs, allowed_objects="all")
output_messages = self._get_output_messages(output_val)
hist.add_messages(input_messages + output_messages)
@@ -556,7 +556,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
hist: BaseChatMessageHistory = config["configurable"]["message_history"]
# Get the input messages
inputs = load(run.inputs)
inputs = load(run.inputs, allowed_objects="all")
input_messages = self._get_input_messages(inputs)
# If historic messages were prepended to the input messages, remove them to
# avoid adding duplicate messages to history.
@@ -565,7 +565,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
input_messages = input_messages[len(historic_messages) :]
# Get the output messages
output_val = load(run.outputs)
output_val = load(run.outputs, allowed_objects="all")
output_messages = self._get_output_messages(output_val)
await hist.aadd_messages(input_messages + output_messages)

View File

@@ -33,7 +33,7 @@ from langchain_core.runnables.utils import (
AddableDict,
ConfigurableFieldSpec,
)
from langchain_core.utils.aiter import atee, py_anext
from langchain_core.utils.aiter import atee
from langchain_core.utils.iter import safetee
from langchain_core.utils.pydantic import create_model_v2
@@ -614,7 +614,7 @@ class RunnableAssign(RunnableSerializable[dict[str, Any], dict[str, Any]]):
)
# start map output stream
first_map_chunk_task: asyncio.Task = asyncio.create_task(
py_anext(map_output, None), # type: ignore[arg-type]
anext(map_output, None),
)
# consume passthrough stream
async for chunk in for_passthrough:
@@ -753,25 +753,19 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
return AddableDict(picked)
return None
def _invoke(
self,
value: dict[str, Any],
) -> dict[str, Any]:
return self._pick(value)
@override
def invoke(
self,
input: dict[str, Any],
config: RunnableConfig | None = None,
**kwargs: Any,
) -> dict[str, Any]:
return self._call_with_config(self._invoke, input, config, **kwargs)
) -> Any:
return self._call_with_config(self._pick, input, config, **kwargs)
async def _ainvoke(
self,
value: dict[str, Any],
) -> dict[str, Any]:
) -> Any:
return self._pick(value)
@override
@@ -780,13 +774,13 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
input: dict[str, Any],
config: RunnableConfig | None = None,
**kwargs: Any,
) -> dict[str, Any]:
) -> Any:
return await self._acall_with_config(self._ainvoke, input, config, **kwargs)
def _transform(
self,
chunks: Iterator[dict[str, Any]],
) -> Iterator[dict[str, Any]]:
) -> Iterator[Any]:
for chunk in chunks:
picked = self._pick(chunk)
if picked is not None:
@@ -798,7 +792,7 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
input: Iterator[dict[str, Any]],
config: RunnableConfig | None = None,
**kwargs: Any,
) -> Iterator[dict[str, Any]]:
) -> Iterator[Any]:
yield from self._transform_stream_with_config(
input, self._transform, config, **kwargs
)
@@ -806,7 +800,7 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
async def _atransform(
self,
chunks: AsyncIterator[dict[str, Any]],
) -> AsyncIterator[dict[str, Any]]:
) -> AsyncIterator[Any]:
async for chunk in chunks:
picked = self._pick(chunk)
if picked is not None:
@@ -818,7 +812,7 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
input: AsyncIterator[dict[str, Any]],
config: RunnableConfig | None = None,
**kwargs: Any,
) -> AsyncIterator[dict[str, Any]]:
) -> AsyncIterator[Any]:
async for chunk in self._atransform_stream_with_config(
input, self._atransform, config, **kwargs
):
@@ -830,7 +824,7 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
input: dict[str, Any],
config: RunnableConfig | None = None,
**kwargs: Any,
) -> Iterator[dict[str, Any]]:
) -> Iterator[Any]:
return self.transform(iter([input]), config, **kwargs)
@override
@@ -839,7 +833,7 @@ class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
input: dict[str, Any],
config: RunnableConfig | None = None,
**kwargs: Any,
) -> AsyncIterator[dict[str, Any]]:
) -> AsyncIterator[Any]:
async def input_aiter() -> AsyncIterator[dict[str, Any]]:
yield input

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from collections.abc import Callable, Mapping
from collections.abc import Mapping
from typing import (
TYPE_CHECKING,
Any,
@@ -31,7 +31,7 @@ from langchain_core.runnables.utils import (
)
if TYPE_CHECKING:
from collections.abc import AsyncIterator, Iterator
from collections.abc import AsyncIterator, Callable, Iterator
class RouterInput(TypedDict):
@@ -151,7 +151,7 @@ class RouterRunnable(RunnableSerializable[RouterInput, Output]):
raise ValueError(msg)
def invoke(
runnable: Runnable, input_: Input, config: RunnableConfig
runnable: Runnable[Input, Output], input_: Input, config: RunnableConfig
) -> Output | Exception:
if return_exceptions:
try:
@@ -188,7 +188,7 @@ class RouterRunnable(RunnableSerializable[RouterInput, Output]):
raise ValueError(msg)
async def ainvoke(
runnable: Runnable, input_: Input, config: RunnableConfig
runnable: Runnable[Input, Output], input_: Input, config: RunnableConfig
) -> Output | Exception:
if return_exceptions:
try:

View File

@@ -45,6 +45,12 @@ class EventData(TypedDict, total=False):
chunks support addition in general, and adding them up should result
in the output of the `Runnable` that generated the event.
"""
tool_call_id: NotRequired[str | None]
"""The tool call ID associated with the tool execution.
This field is available for the `on_tool_error` event and can be used to
link errors to specific tool calls in stateless agent implementations.
"""
class BaseStreamEvent(TypedDict):

View File

@@ -7,7 +7,10 @@ import asyncio
import inspect
import sys
import textwrap
from collections.abc import Mapping, Sequence
# Cannot move to TYPE_CHECKING as Mapping and Sequence are needed at runtime by
# RunnableConfigurableFields.
from collections.abc import Mapping, Sequence # noqa: TC003
from functools import lru_cache
from inspect import signature
from itertools import groupby
@@ -129,9 +132,12 @@ def asyncio_accepts_context() -> bool:
return sys.version_info >= (3, 11)
_T = TypeVar("_T")
def coro_with_context(
coro: Awaitable[Any], context: Context, *, create_task: bool = False
) -> Awaitable[Any]:
coro: Awaitable[_T], context: Context, *, create_task: bool = False
) -> Awaitable[_T]:
"""Await a coroutine with a context.
Args:

View File

@@ -205,7 +205,7 @@ class InMemoryBaseStore(BaseStore[str, V], Generic[V]):
async def amdelete(self, keys: Sequence[str]) -> None:
self.mdelete(keys)
def yield_keys(self, prefix: str | None = None) -> Iterator[str]:
def yield_keys(self, *, prefix: str | None = None) -> Iterator[str]:
"""Get an iterator over keys that match the given prefix.
Args:
@@ -221,7 +221,7 @@ class InMemoryBaseStore(BaseStore[str, V], Generic[V]):
if key.startswith(prefix):
yield key
async def ayield_keys(self, prefix: str | None = None) -> AsyncIterator[str]:
async def ayield_keys(self, *, prefix: str | None = None) -> AsyncIterator[str]:
"""Async get an async iterator over keys that match the given prefix.
Args:

View File

@@ -5,10 +5,11 @@ from __future__ import annotations
import functools
import inspect
import json
import logging
import typing
import warnings
from abc import ABC, abstractmethod
from collections.abc import Callable
from collections.abc import Callable # noqa: TC003
from inspect import signature
from typing import (
TYPE_CHECKING,
@@ -22,6 +23,7 @@ from typing import (
get_type_hints,
)
import typing_extensions
from pydantic import (
BaseModel,
ConfigDict,
@@ -31,6 +33,7 @@ from pydantic import (
ValidationError,
validate_arguments,
)
from pydantic.fields import FieldInfo
from pydantic.v1 import BaseModel as BaseModelV1
from pydantic.v1 import ValidationError as ValidationErrorV1
from pydantic.v1 import validate_arguments as validate_arguments_v1
@@ -80,6 +83,8 @@ TOOL_MESSAGE_BLOCK_TYPES = (
"file",
)
_logger = logging.getLogger(__name__)
class SchemaAnnotationError(TypeError):
"""Raised when args_schema is missing or has an incorrect type annotation."""
@@ -94,12 +99,14 @@ def _is_annotated_type(typ: type[Any]) -> bool:
Returns:
`True` if the type is an Annotated type, `False` otherwise.
"""
return get_origin(typ) is typing.Annotated
return get_origin(typ) in {typing.Annotated, typing_extensions.Annotated}
def _get_annotation_description(arg_type: type) -> str | None:
"""Extract description from an Annotated type.
Checks for string annotations and `FieldInfo` objects with descriptions.
Args:
arg_type: The type to extract description from.
@@ -111,6 +118,8 @@ def _get_annotation_description(arg_type: type) -> str | None:
for annotation in annotated_args[1:]:
if isinstance(annotation, str):
return annotation
if isinstance(annotation, FieldInfo) and annotation.description:
return annotation.description
return None
@@ -496,6 +505,24 @@ class ChildTool(BaseTool):
two-tuple corresponding to the `(content, artifact)` of a `ToolMessage`.
"""
extras: dict[str, Any] | None = None
"""Optional provider-specific extra fields for the tool.
This is used to pass provider-specific configuration that doesn't fit into
standard tool fields.
Example:
Anthropic-specific fields like [`cache_control`](https://docs.langchain.com/oss/python/integrations/chat/anthropic#prompt-caching),
[`defer_loading`](https://docs.langchain.com/oss/python/integrations/chat/anthropic#tool-search),
or `input_examples`.
```python
@tool(extras={"defer_loading": True, "cache_control": {"type": "ephemeral"}})
def my_tool(x: str) -> str:
return x
```
"""
def __init__(self, **kwargs: Any) -> None:
"""Initialize the tool.
@@ -542,9 +569,12 @@ class ChildTool(BaseTool):
elif self.args_schema and issubclass(self.args_schema, BaseModelV1):
json_schema = self.args_schema.schema()
else:
input_schema = self.get_input_schema()
json_schema = input_schema.model_json_schema()
return json_schema["properties"]
input_schema = self.tool_call_schema
if isinstance(input_schema, dict):
json_schema = input_schema
else:
json_schema = input_schema.model_json_schema()
return cast("dict", json_schema["properties"])
@property
def tool_call_schema(self) -> ArgsSchema:
@@ -635,6 +665,7 @@ class ChildTool(BaseTool):
TypeError: If `args_schema` is not a Pydantic `BaseModel` or dict.
"""
input_args = self.args_schema
if isinstance(tool_input, str):
if input_args is not None:
if isinstance(input_args, dict):
@@ -652,6 +683,7 @@ class ChildTool(BaseTool):
msg = f"args_schema must be a Pydantic BaseModel, got {input_args}"
raise TypeError(msg)
return tool_input
if input_args is not None:
if isinstance(input_args, dict):
return tool_input
@@ -692,9 +724,30 @@ class ChildTool(BaseTool):
f"args_schema must be a Pydantic BaseModel, got {self.args_schema}"
)
raise NotImplementedError(msg)
validated_input = {
k: getattr(result, k) for k in result_dict if k in tool_input
}
# Include fields from tool_input, plus fields with explicit defaults.
# This applies Pydantic defaults (like Field(default=1)) while excluding
# synthetic "args"/"kwargs" fields that Pydantic creates for *args/**kwargs.
field_info = get_fields(input_args)
validated_input = {}
for k in result_dict:
if k in tool_input:
# Field was provided in input - include it (validated)
validated_input[k] = getattr(result, k)
elif k in field_info and k not in ("args", "kwargs"):
# Check if field has an explicit default defined in the schema.
# Exclude "args"/"kwargs" as these are synthetic fields for variadic
# parameters that should not be passed as keyword arguments.
fi = field_info[k]
# Pydantic v2 uses is_required() method, v1 uses required attribute
has_default = (
not fi.is_required()
if hasattr(fi, "is_required")
else not getattr(fi, "required", True)
)
if has_default:
validated_input[k] = getattr(result, k)
for k in self._injected_args_keys:
if k in tool_input:
validated_input[k] = tool_input[k]
@@ -709,7 +762,9 @@ class ChildTool(BaseTool):
)
raise ValueError(msg)
validated_input[k] = tool_call_id
return validated_input
return tool_input
@abstractmethod
@@ -753,6 +808,9 @@ class ChildTool(BaseTool):
# Start with filtered args from the constant
filtered_keys = set[str](FILTERED_ARGS)
# Add injected args from function signature (e.g., ToolRuntime parameters)
filtered_keys.update(self._injected_args_keys)
# If we have an args_schema, use it to identify injected args
if self.args_schema is not None:
try:
@@ -760,9 +818,12 @@ class ChildTool(BaseTool):
for field_name, field_type in annotations.items():
if _is_injected_arg_type(field_type):
filtered_keys.add(field_name)
except Exception: # noqa: S110
except Exception:
# If we can't get annotations, just use FILTERED_ARGS
pass
_logger.debug(
"Failed to get args_schema annotations for filtering.",
exc_info=True,
)
# Filter out the injected keys from tool_input
return {k: v for k, v in tool_input.items() if k not in filtered_keys}
@@ -877,6 +938,7 @@ class ChildTool(BaseTool):
name=run_name,
run_id=run_id,
inputs=filtered_tool_input,
tool_call_id=tool_call_id,
**kwargs,
)
@@ -927,7 +989,7 @@ class ChildTool(BaseTool):
error_to_raise = e
if error_to_raise:
run_manager.on_tool_error(error_to_raise)
run_manager.on_tool_error(error_to_raise, tool_call_id=tool_call_id)
raise error_to_raise
output = _format_output(content, artifact, tool_call_id, self.name, status)
run_manager.on_tool_end(output, color=color, name=self.name, **kwargs)
@@ -1004,6 +1066,7 @@ class ChildTool(BaseTool):
name=run_name,
run_id=run_id,
inputs=filtered_tool_input,
tool_call_id=tool_call_id,
**kwargs,
)
content = None
@@ -1056,7 +1119,7 @@ class ChildTool(BaseTool):
error_to_raise = e
if error_to_raise:
await run_manager.on_tool_error(error_to_raise)
await run_manager.on_tool_error(error_to_raise, tool_call_id=tool_call_id)
raise error_to_raise
output = _format_output(content, artifact, tool_call_id, self.name, status)
@@ -1494,7 +1557,7 @@ def _replace_type_vars(
_replace_type_vars(arg, generic_map, default_to_bound=default_to_bound)
for arg in args
)
return _py_38_safe_origin(origin)[new_args] # type: ignore[index]
return cast("type", _py_38_safe_origin(origin)[new_args]) # type: ignore[index]
return type_

View File

@@ -2,7 +2,7 @@
import inspect
from collections.abc import Callable
from typing import Any, Literal, get_type_hints, overload
from typing import Any, Literal, cast, get_type_hints, overload
from pydantic import BaseModel, Field, create_model
@@ -23,6 +23,7 @@ def tool(
response_format: Literal["content", "content_and_artifact"] = "content",
parse_docstring: bool = False,
error_on_invalid_docstring: bool = True,
extras: dict[str, Any] | None = None,
) -> Callable[[Callable | Runnable], BaseTool]: ...
@@ -38,6 +39,7 @@ def tool(
response_format: Literal["content", "content_and_artifact"] = "content",
parse_docstring: bool = False,
error_on_invalid_docstring: bool = True,
extras: dict[str, Any] | None = None,
) -> BaseTool: ...
@@ -52,6 +54,7 @@ def tool(
response_format: Literal["content", "content_and_artifact"] = "content",
parse_docstring: bool = False,
error_on_invalid_docstring: bool = True,
extras: dict[str, Any] | None = None,
) -> BaseTool: ...
@@ -66,6 +69,7 @@ def tool(
response_format: Literal["content", "content_and_artifact"] = "content",
parse_docstring: bool = False,
error_on_invalid_docstring: bool = True,
extras: dict[str, Any] | None = None,
) -> Callable[[Callable | Runnable], BaseTool]: ...
@@ -80,6 +84,7 @@ def tool(
response_format: Literal["content", "content_and_artifact"] = "content",
parse_docstring: bool = False,
error_on_invalid_docstring: bool = True,
extras: dict[str, Any] | None = None,
) -> BaseTool | Callable[[Callable | Runnable], BaseTool]:
"""Convert Python functions and `Runnables` to LangChain tools.
@@ -130,6 +135,15 @@ def tool(
parse parameter descriptions from Google Style function docstrings.
error_on_invalid_docstring: If `parse_docstring` is provided, configure
whether to raise `ValueError` on invalid Google Style docstrings.
extras: Optional provider-specific extra fields for the tool.
Used to pass configuration that doesn't fit into standard tool fields.
Chat models should process known extras when constructing model payloads.
!!! example
For example, Anthropic-specific fields like `cache_control`,
`defer_loading`, or `input_examples`.
Raises:
ValueError: If too many positional arguments are provided (e.g. violating the
@@ -292,6 +306,7 @@ def tool(
response_format=response_format,
parse_docstring=parse_docstring,
error_on_invalid_docstring=error_on_invalid_docstring,
extras=extras,
)
# If someone doesn't want a schema applied, we must treat it as
# a simple string->string function
@@ -308,6 +323,7 @@ def tool(
return_direct=return_direct,
coroutine=coroutine,
response_format=response_format,
extras=extras,
)
return _tool_factory
@@ -391,7 +407,7 @@ def _get_schema_from_runnable_and_arg_types(
)
raise TypeError(msg) from e
fields = {key: (key_type, Field(...)) for key, key_type in arg_types.items()}
return create_model(name, **fields) # type: ignore[call-overload]
return cast("type[BaseModel]", create_model(name, **fields)) # type: ignore[call-overload]
def convert_runnable_to_tool(

View File

@@ -2,22 +2,23 @@
from __future__ import annotations
from functools import partial
from typing import TYPE_CHECKING, Literal
from pydantic import BaseModel, Field
# Cannot move Callbacks and Document to TYPE_CHECKING as StructuredTool's
# func/coroutine parameter annotations are evaluated at runtime.
from langchain_core.callbacks import Callbacks # noqa: TC001
from langchain_core.documents import Document # noqa: TC001
from langchain_core.prompts import (
BasePromptTemplate,
PromptTemplate,
aformat_document,
format_document,
)
from langchain_core.tools.simple import Tool
from langchain_core.tools.structured import StructuredTool
if TYPE_CHECKING:
from langchain_core.callbacks import Callbacks
from langchain_core.documents import Document
from langchain_core.retrievers import BaseRetriever
@@ -27,43 +28,6 @@ class RetrieverInput(BaseModel):
query: str = Field(description="query to look up in retriever")
def _get_relevant_documents(
query: str,
retriever: BaseRetriever,
document_prompt: BasePromptTemplate,
document_separator: str,
callbacks: Callbacks = None,
response_format: Literal["content", "content_and_artifact"] = "content",
) -> str | tuple[str, list[Document]]:
docs = retriever.invoke(query, config={"callbacks": callbacks})
content = document_separator.join(
format_document(doc, document_prompt) for doc in docs
)
if response_format == "content_and_artifact":
return (content, docs)
return content
async def _aget_relevant_documents(
query: str,
retriever: BaseRetriever,
document_prompt: BasePromptTemplate,
document_separator: str,
callbacks: Callbacks = None,
response_format: Literal["content", "content_and_artifact"] = "content",
) -> str | tuple[str, list[Document]]:
docs = await retriever.ainvoke(query, config={"callbacks": callbacks})
content = document_separator.join(
[await aformat_document(doc, document_prompt) for doc in docs]
)
if response_format == "content_and_artifact":
return (content, docs)
return content
def create_retriever_tool(
retriever: BaseRetriever,
name: str,
@@ -72,7 +36,7 @@ def create_retriever_tool(
document_prompt: BasePromptTemplate | None = None,
document_separator: str = "\n\n",
response_format: Literal["content", "content_and_artifact"] = "content",
) -> Tool:
) -> StructuredTool:
r"""Create a tool to do retrieval of documents.
Args:
@@ -93,22 +57,31 @@ def create_retriever_tool(
Returns:
Tool class to pass to an agent.
"""
document_prompt = document_prompt or PromptTemplate.from_template("{page_content}")
func = partial(
_get_relevant_documents,
retriever=retriever,
document_prompt=document_prompt,
document_separator=document_separator,
response_format=response_format,
)
afunc = partial(
_aget_relevant_documents,
retriever=retriever,
document_prompt=document_prompt,
document_separator=document_separator,
response_format=response_format,
)
return Tool(
document_prompt_ = document_prompt or PromptTemplate.from_template("{page_content}")
def func(
query: str, callbacks: Callbacks = None
) -> str | tuple[str, list[Document]]:
docs = retriever.invoke(query, config={"callbacks": callbacks})
content = document_separator.join(
format_document(doc, document_prompt_) for doc in docs
)
if response_format == "content_and_artifact":
return (content, docs)
return content
async def afunc(
query: str, callbacks: Callbacks = None
) -> str | tuple[str, list[Document]]:
docs = await retriever.ainvoke(query, config={"callbacks": callbacks})
content = document_separator.join(
[await aformat_document(doc, document_prompt_) for doc in docs]
)
if response_format == "content_and_artifact":
return (content, docs)
return content
return StructuredTool(
name=name,
description=description,
func=func,

View File

@@ -11,9 +11,10 @@ from typing import (
from typing_extensions import override
# Cannot move to TYPE_CHECKING as _run/_arun parameter annotations are needed at runtime
from langchain_core.callbacks import (
AsyncCallbackManagerForToolRun,
CallbackManagerForToolRun,
AsyncCallbackManagerForToolRun, # noqa: TC001
CallbackManagerForToolRun, # noqa: TC001
)
from langchain_core.runnables import RunnableConfig, run_in_executor
from langchain_core.tools.base import (

View File

@@ -16,9 +16,10 @@ from typing import (
from pydantic import Field, SkipValidation
from typing_extensions import override
# Cannot move to TYPE_CHECKING as _run/_arun parameter annotations are needed at runtime
from langchain_core.callbacks import (
AsyncCallbackManagerForToolRun,
CallbackManagerForToolRun,
AsyncCallbackManagerForToolRun, # noqa: TC001
CallbackManagerForToolRun, # noqa: TC001
)
from langchain_core.runnables import RunnableConfig, run_in_executor
from langchain_core.tools.base import (

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