Compare commits

..

145 Commits

Author SHA1 Message Date
Sydney Runkle
d34d3157fc changes 2025-12-03 11:05:33 -05:00
Sydney Runkle
06d4206ffb diff for v1 2025-12-03 10:58:57 -05:00
Mason Daugherty
3108b14164 docs(standard-tests): fix supports_json_mode docstring (#34181) 2025-12-03 00:12:57 -05:00
Mason Daugherty
1922adc092 docs(standard-tests): fix formatting bug, rearrange admonition (#34180) 2025-12-02 23:40:11 -05:00
Mason Daugherty
4a242a8a4f docs(standard-tests): enrich doc to indicate missing default values (#34179) 2025-12-02 23:32:21 -05:00
Mason Daugherty
064b37f90e docs(standard-tests): improve doc for structured_output_kwargs and supports_json_mode (#34178) 2025-12-02 23:18:53 -05:00
Mason Daugherty
062678fa18 fix(standard-tests): fix broken links (#34175) 2025-12-02 20:52:27 -05:00
Mason Daugherty
5d3e3d3f31 fix(standard-tests): remove broken code block docstring title (#34173) 2025-12-02 20:18:31 -05:00
Mason Daugherty
5a7cf87626 style(standard-tests): some fencing (#34171) 2025-12-02 14:42:26 -05:00
ccurme
c63f23d233 revert(model-profiles): update docs link (#34162) 2025-12-01 17:29:45 +00:00
Mason Daugherty
b7091d391d feat(anthropic): auto append relevant beta headers (#34113) 2025-12-01 12:20:41 -05:00
ccurme
7a2952210e fix(langchain): (SummarizationMiddleware) adjust token counts based on model (#34161) 2025-12-01 16:22:44 +00:00
ccurme
7549845d82 chore(anthropic): vcr integration test (#34160) 2025-12-01 15:28:28 +00:00
Mason Daugherty
878f033ed7 docs(langchain): docstrings for summariziation middleware types (#34158)
improving devx :)
2025-12-01 09:39:33 -05:00
Steffen Hausmann
4065106c2e fix(langchain): add types to human_in_the_loop middleware (#34137)
The `HumanInTheLoopMiddleware` is missing a type annotation for the
context schema. Without the fix in this PR, the following code does not
type check:

```
graph = create_agent(
    "gpt-5",
    tools=[send_email_tool, read_email_tool],
    middleware=[
        HumanInTheLoopMiddleware(
            interrupt_on={
                # Require approval or rejection for sending emails
                "send_email_tool": {
                    "allowed_decisions": ["approve", "reject"],
                },
                # Auto-approve reading emails
                "read_email_tool": False,
            }
        ),
    ],
    context_schema=ContextSchema,
)
```

```
Argument of type "list[HumanInTheLoopMiddleware]" cannot be assigned to parameter "middleware" of type "Sequence[AgentMiddleware[StateT_co@create_agent, ContextT@create_agent]]" in function "create_agent"
  "HumanInTheLoopMiddleware" is not assignable to "AgentMiddleware[AgentState[Unknown], ContextSchema | None]"
    Type parameter "ContextT@AgentMiddleware" is invariant, but "None" is not the same as "ContextSchema | None"
```
2025-12-01 08:46:38 -05:00
Mason Daugherty
12df938ace docs(core): update docstrings in RunnableConfig, dereference_refs (#34131) 2025-11-28 03:55:37 -05:00
Mason Daugherty
65ee43cc10 chore(infra): update agent files, remove top-level pyproject (#34128) 2025-11-27 21:06:43 -05:00
Mason Daugherty
fe7c000fc1 fix(model-profiles): update docs link (#34127) 2025-11-28 00:19:36 +00:00
Mason Daugherty
dad50e5624 chore(infra): updated allowed scopes in PR lint configuration (#34115) 2025-11-27 00:34:15 -05:00
Mason Daugherty
0a6d01e61d docs(anthropic,core,langchain): updates (#34106) 2025-11-25 17:58:09 -05:00
Mason Daugherty
c6f8b0875a style(core,langchain,qdrant): fix some docstrings for refs (#34105) 2025-11-25 13:58:53 -05:00
Mason Daugherty
4c3800d743 chore(infra): update PR template, agent files (#34104) 2025-11-25 13:58:41 -05:00
dependabot[bot]
7fe1c4b78f chore(deps): bump actions/checkout from 5 to 6 (#34083)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to
6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>v6-beta by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2298">actions/checkout#2298</a></li>
<li>update readme/changelog for v6 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2311">actions/checkout#2311</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v5.0.0...v6.0.0">https://github.com/actions/checkout/compare/v5.0.0...v6.0.0</a></p>
<h2>v6-beta</h2>
<h2>What's Changed</h2>
<p>Updated persist-credentials to store the credentials under
<code>$RUNNER_TEMP</code> instead of directly in the local git
config.</p>
<p>This requires a minimum Actions Runner version of <a
href="https://github.com/actions/runner/releases/tag/v2.329.0">v2.329.0</a>
to access the persisted credentials for <a
href="https://docs.github.com/en/actions/tutorials/use-containerized-services/create-a-docker-container-action">Docker
container action</a> scenarios.</p>
<h2>v5.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v5...v5.0.1">https://github.com/actions/checkout/compare/v5...v5.0.1</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>V6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>V5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>V5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>V4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>V4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>v4.2.0</h2>
<ul>
<li>Add Ref and Commit outputs by <a
href="https://github.com/lucacome"><code>@​lucacome</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1180">actions/checkout#1180</a></li>
<li>Dependency updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>- <a
href="https://redirect.github.com/actions/checkout/pull/1777">actions/checkout#1777</a>,
<a
href="https://redirect.github.com/actions/checkout/pull/1872">actions/checkout#1872</a></li>
</ul>
<h2>v4.1.7</h2>
<ul>
<li>Bump the minor-npm-dependencies group across 1 directory with 4
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1739">actions/checkout#1739</a></li>
<li>Bump actions/checkout from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1697">actions/checkout#1697</a></li>
<li>Check out other refs/* by commit by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1774">actions/checkout#1774</a></li>
<li>Pin actions/checkout's own workflows to a known, good, stable
version. by <a href="https://github.com/jww3"><code>@​jww3</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1776">actions/checkout#1776</a></li>
</ul>
<h2>v4.1.6</h2>
<ul>
<li>Check platform to set archive extension appropriately by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1732">actions/checkout#1732</a></li>
</ul>
<h2>v4.1.5</h2>
<ul>
<li>Update NPM dependencies by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1703">actions/checkout#1703</a></li>
<li>Bump github/codeql-action from 2 to 3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1694">actions/checkout#1694</a></li>
<li>Bump actions/setup-node from 1 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1696">actions/checkout#1696</a></li>
<li>Bump actions/upload-artifact from 2 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1695">actions/checkout#1695</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="1af3b93b68"><code>1af3b93</code></a>
update readme/changelog for v6 (<a
href="https://redirect.github.com/actions/checkout/issues/2311">#2311</a>)</li>
<li><a
href="71cf2267d8"><code>71cf226</code></a>
v6-beta (<a
href="https://redirect.github.com/actions/checkout/issues/2298">#2298</a>)</li>
<li><a
href="069c695914"><code>069c695</code></a>
Persist creds to a separate file (<a
href="https://redirect.github.com/actions/checkout/issues/2286">#2286</a>)</li>
<li><a
href="ff7abcd0c3"><code>ff7abcd</code></a>
Update README to include Node.js 24 support details and requirements (<a
href="https://redirect.github.com/actions/checkout/issues/2248">#2248</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/checkout/compare/v5...v6">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-11-24 19:10:28 -05:00
Bagatur
c375732396 fix(core): handle missing StructuredPrompt schema (#34096)
- **Description:** if you dont pass in schema= or schema_= to
StrucutredPrompt(...) today you get a confusing KeyError. Raise a more
readable ValueError instead.
- **Issue:** na
- **Dependencies:** na
2025-11-24 18:39:29 -05:00
ccurme
9c21f83e82 release(langchain): 1.1 (#34090) 2025-11-24 10:27:13 -05:00
ccurme
880652b713 release: (integration packages): 1.1 (#34088) 2025-11-24 10:00:06 -05:00
Sydney Runkle
4ab94579ad feat(langchain): support SystemMessage in create_agent's system_prompt (#34055)
* `create_agent`'s `system_prompt` allows `str | SystemMessage`
* added `system_message: SystemMessage` on `ModelRequest`
* `ModelRequest.system_prompt` is a function of `system_message.text`,
now deprecated
* disallow setting `system_prompt` and `system_message`
* `ModelRequest.system_prompt` can still be set (w/ custom setattr) for
custom backwards compat, but the updates just get propogated to the
`ModelRequest.system_message`

---------

Co-authored-by: Chester Curme <chester.curme@gmail.com>
2025-11-24 14:53:57 +00:00
ccurme
eb0545a173 release: (integration packages) 1.1 (#34087) 2025-11-24 09:13:01 -05:00
ccurme
a2e389de9f release(fireworks): 1.1 (#34086) 2025-11-24 09:05:43 -05:00
Alex Kondratev
01573c1375 fix(core): ensure_ascii=False in PydanticOutputParser exception formatting (#34006)
- **Description:** When formatting an error, `PydanticOutputParser`
dumps json with default `ensure_ascii=True`
  -  **Issue:** Fixes #34005
  - **Dependencies:** None

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

Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-11-23 20:22:50 -05:00
Abhinav
2ba3ce81a6 fix(openai): make GPT-5 temperature validation case-insensitive (#34012)
Fixed a bug where GPT-5 temperature validation was case-sensitive,
causing issues when users
specified Azure deployment names or model names in uppercase (e.g.,
`"GPT-5-2025-01-01"`, `"GPT-5-NANO"`). The validation now correctly
handles model names regardless of case.

  Changes made:
- Updated `validate_temperature()` method in `BaseChatOpenAI` to perform
case-insensitive
  model name comparisons
- Updated `_get_encoding_model()` method to use case-insensitive checks
for tiktoken encoder
  selection
- Added comprehensive unit tests to verify case-insensitive behavior
with various case
  combinations

  **Issue:** Fixes #34003

  **Dependencies:** None

  **Test Coverage:**
  - All existing tests pass
- New test `test_gpt_5_temperature_case_insensitive` covers uppercase,
lowercase, and
  mixed-case model names
- Tests verify both non-chat GPT-5 models (temperature removed) and chat
models (temperature
  preserved)
  - Lint and format checks pass (`make lint`, `make format`)

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
2025-11-23 20:17:03 -05:00
dependabot[bot]
4e4e5d7337 chore(infra): bump actions/github-script from 6 to 8 (#33991)
Bumps [actions/github-script](https://github.com/actions/github-script)
from 6 to 8.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/github-script/releases">actions/github-script's
releases</a>.</em></p>
<blockquote>
<h2>v8.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update Node.js version support to 24.x by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/637">actions/github-script#637</a></li>
<li>README for updating actions/github-script from v7 to v8 by <a
href="https://github.com/sneha-krip"><code>@​sneha-krip</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/653">actions/github-script#653</a></li>
</ul>
<h2>⚠️ Minimum Compatible Runner Version</h2>
<p><strong>v2.327.1</strong><br />
<a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Release
Notes</a></p>
<p>Make sure your runner is updated to this version or newer to use this
release.</p>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/637">actions/github-script#637</a></li>
<li><a
href="https://github.com/sneha-krip"><code>@​sneha-krip</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/653">actions/github-script#653</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/github-script/compare/v7.1.0...v8.0.0">https://github.com/actions/github-script/compare/v7.1.0...v8.0.0</a></p>
<h2>v7.1.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Upgrade husky to v9 by <a
href="https://github.com/benelan"><code>@​benelan</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/482">actions/github-script#482</a></li>
<li>Add workflow file for publishing releases to immutable action
package by <a
href="https://github.com/Jcambass"><code>@​Jcambass</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/485">actions/github-script#485</a></li>
<li>Upgrade IA Publish by <a
href="https://github.com/Jcambass"><code>@​Jcambass</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/486">actions/github-script#486</a></li>
<li>Fix workflow status badges by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/497">actions/github-script#497</a></li>
<li>Update usage of <code>actions/upload-artifact</code> by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/512">actions/github-script#512</a></li>
<li>Clear up package name confusion by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/514">actions/github-script#514</a></li>
<li>Update dependencies with <code>npm audit fix</code> by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/515">actions/github-script#515</a></li>
<li>Specify that the used script is JavaScript by <a
href="https://github.com/timotk"><code>@​timotk</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/478">actions/github-script#478</a></li>
<li>chore: Add Dependabot for NPM and Actions by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/472">actions/github-script#472</a></li>
<li>Define <code>permissions</code> in workflows and update actions by
<a href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in
<a
href="https://redirect.github.com/actions/github-script/pull/531">actions/github-script#531</a></li>
<li>chore: Add Dependabot for .github/actions/install-dependencies by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/532">actions/github-script#532</a></li>
<li>chore: Remove .vscode settings by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/533">actions/github-script#533</a></li>
<li>ci: Use github/setup-licensed by <a
href="https://github.com/nschonni"><code>@​nschonni</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/473">actions/github-script#473</a></li>
<li>make octokit instance available as octokit on top of github, to make
it easier to seamlessly copy examples from GitHub rest api or octokit
documentations by <a
href="https://github.com/iamstarkov"><code>@​iamstarkov</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/508">actions/github-script#508</a></li>
<li>Remove <code>octokit</code> README updates for v7 by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/557">actions/github-script#557</a></li>
<li>docs: add &quot;exec&quot; usage examples by <a
href="https://github.com/neilime"><code>@​neilime</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/546">actions/github-script#546</a></li>
<li>Bump ruby/setup-ruby from 1.213.0 to 1.222.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/github-script/pull/563">actions/github-script#563</a></li>
<li>Bump ruby/setup-ruby from 1.222.0 to 1.229.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/github-script/pull/575">actions/github-script#575</a></li>
<li>Clearly document passing inputs to the <code>script</code> by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/603">actions/github-script#603</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/github-script/pull/610">actions/github-script#610</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/benelan"><code>@​benelan</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/482">actions/github-script#482</a></li>
<li><a href="https://github.com/Jcambass"><code>@​Jcambass</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/485">actions/github-script#485</a></li>
<li><a href="https://github.com/timotk"><code>@​timotk</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/478">actions/github-script#478</a></li>
<li><a
href="https://github.com/iamstarkov"><code>@​iamstarkov</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/508">actions/github-script#508</a></li>
<li><a href="https://github.com/neilime"><code>@​neilime</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/546">actions/github-script#546</a></li>
<li><a href="https://github.com/nebuk89"><code>@​nebuk89</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/github-script/pull/610">actions/github-script#610</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/github-script/compare/v7...v7.1.0">https://github.com/actions/github-script/compare/v7...v7.1.0</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="ed597411d8"><code>ed59741</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/github-script/issues/653">#653</a>
from actions/sneha-krip/readme-for-v8</li>
<li><a
href="2dc352e4ba"><code>2dc352e</code></a>
Bold minimum Actions Runner version in README</li>
<li><a
href="01e118c8d0"><code>01e118c</code></a>
Update README for Node 24 runtime requirements</li>
<li><a
href="8b222ac82e"><code>8b222ac</code></a>
Apply suggestion from <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a></li>
<li><a
href="adc0eeac99"><code>adc0eea</code></a>
README for updating actions/github-script from v7 to v8</li>
<li><a
href="20fe497b3f"><code>20fe497</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/github-script/issues/637">#637</a>
from actions/node24</li>
<li><a
href="e7b7f222b1"><code>e7b7f22</code></a>
update licenses</li>
<li><a
href="2c81ba05f3"><code>2c81ba0</code></a>
Update Node.js version support to 24.x</li>
<li><a
href="f28e40c7f3"><code>f28e40c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/github-script/issues/610">#610</a>
from actions/nebuk89-patch-1</li>
<li><a
href="1ae9958572"><code>1ae9958</code></a>
Update README.md</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/github-script/compare/v6...v8">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-23 20:00:22 -05:00
Mason Daugherty
2a863727f9 fix(infra,core): nits (#34079)
* Add missing `nits` to allowed PR linting scopes
* Ensure `MAJOR.MINOR.PATCH` consistency in admonitions
* Ensure valid spacing in admonitions
2025-11-23 20:00:07 -05:00
dumko2001
30e2260e26 fix(core): Decouple provider prefix from model name in init_chat_mode… (#34046)
:…l logic

Addresses Issue #34007.
Fixes a bug where aliases like 'mistral:' were inferred correctly as a
provider but the prefix was not stripped from the model name, causing
API 400 errors. Added logic to strip prefix when inference succeeds.

**Description**
This PR resolves a logic error in `init_chat_model` where inferred
provider aliases (specifically `mistral:`) were correctly identified but
not stripped from the model string.

**The Problem**
When passing a string like `mistral:ministral-8b-latest`, the factory
logic correctly inferred the provider as `mistralai` but failed to enter
the string-splitting block because the alias `mistral` was not in the
hardcoded `_SUPPORTED_PROVIDERS` list. This caused the raw string
`mistral:ministral-8b-latest` to be passed to the `ChatMistralAI`
constructor, resulting in a 400 API error.

**The Fix**
I updated `_parse_model` in
`libs/langchain/langchain/chat_models/base.py`. The logic now attempts
to infer the provider from the prefix *before* determining whether to
split the string. This ensures that valid aliases trigger the stripping
logic, passing only the clean `model_name` to the integration class.

**Issue**
Fixes #34007

**Dependencies**
None.

**Verification**
Validated locally with a reproduction script:
- Input: `mistral:ministral-8b-latest`
- Result: Successfully instantiates `ChatMistralAI` with
`model="ministral-8b-latest"`.
- Validated that standard inputs (e.g., `gpt-4o`) remain unaffected.

Co-authored-by: ioop <ioop@Sidharths-MacBook-Air.local>
2025-11-23 19:52:24 -05:00
Mason Daugherty
cbaea351b2 style(core,langchain-classic,openai): fix griffe warnings (#34074) 2025-11-23 01:06:46 -05:00
ccurme
f070217c3b release(standard-tests): 1.0.2 (#34071)
Resolves https://github.com/langchain-ai/langchain/issues/34069
2025-11-22 18:35:09 -05:00
ccurme
0915682c12 chore(fireworks): update tested models (#34070) 2025-11-22 16:50:49 -05:00
Sydney Runkle
68ab9a1e56 fix: don't reorder tool calls in HITL middleware (#34023) 2025-11-22 05:10:32 -05:00
Mason Daugherty
47b79c30c0 chore(docs): fix a few refs syntax errors (#34044)
missing whitespace for some admonitions
2025-11-22 00:58:21 -05:00
ccurme
5899f980aa release(model-profiles): 0.0.5 (#34064) 2025-11-21 16:12:00 -05:00
ccurme
b0bf4afe81 release(core): 1.1.0 (#34063) 2025-11-21 15:57:25 -05:00
ccurme
33e5d01f7c feat(model-profiles): distribute data across packages (#34024) 2025-11-21 15:47:05 -05:00
Sydney Runkle
ee3373afc2 chore: add more robust test for runtime injection w/ explicit args_schema (#34051) 2025-11-20 16:51:37 +00:00
Sydney Runkle
b296f103a9 feat: ModelRetryMiddleware (#34027)
Closes https://github.com/langchain-ai/langchain/issues/33983

* Adds `ModelRetryMiddleware` modeled after `ToolRetryMiddleware`
* Uses `on_failure` modes of `error` and `continue` to match the
`exit_behavior` modes of model + tool call limit middleware
* In a backwards compatible manner, aligns the API of
`ToolRetryMiddleware`'s `on_failure` with the above
* Centralize common "retry" utils across these middlewares
2025-11-20 11:42:33 -05:00
Eugene Yurtsev
525d5c0169 release(core): 1.0.7 (#34036)
Release core 1.0.7
2025-11-19 21:17:31 +00:00
Eugene Yurtsev
c4b6ba254e fix(core): fix validation for input variables in f-string templates, restrict functionality supported by jinja2, mustache templates (#34035)
* Fix validation for input variables in f-string templates
* Restrict functionality of features supported by jinja2 and mustache
templates
2025-11-19 16:09:46 -05:00
Sydney Runkle
b7d1831f9d fix: deprecate setattr on ModelCallRequest (#34022)
* one alternative considered was setting `frozen=True` on the dataclass,
but this is breaking, so a deprecation is a nicer approach
2025-11-19 11:08:55 -05:00
ccurme
328ba36601 chore(openai): skip Azure text completions tests (#34021) 2025-11-19 09:29:12 -05:00
Sydney Runkle
6f677ef5c1 chore: temporarily skip openai integration tests (#34020)
getting around deprecated azure model issues blocking core release
2025-11-19 14:05:22 +00:00
Sydney Runkle
d47d41cbd3 release: langchain-core 1.0.6 (#34018) 2025-11-19 08:16:34 -05:00
William FH
32bbe99efc chore: Support tool runtime injection when custom args schema is prov… (#33999)
Support injection of injected args (like `InjectedToolCallId`,
`ToolRuntime`) when an `args_schema` is specified that doesn't contain
said args.

This allows for pydantic validation of other args while retaining the
ability to inject langchain specific arguments.

fixes https://github.com/langchain-ai/langchain/issues/33646
fixes https://github.com/langchain-ai/langchain/issues/31688

Taking a deep dive here reminded me that we definitely need to revisit
our internal tooling logic, but I don't think we should do that in this
PR.

---------

Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
Co-authored-by: Sydney Runkle <sydneymarierunkle@gmail.com>
2025-11-18 17:09:59 +00:00
ccurme
990e346c46 release(anthropic): 1.1 (#33997) 2025-11-17 16:24:29 -05:00
ccurme
9b7792631d feat(anthropic): support native structured output feature and strict tool calling (#33980) 2025-11-17 16:14:20 -05:00
CKLogic
558a8fe25b feat(core): add proxy support for mermaid png rendering (#32400)
### Description

This PR adds support for configuring HTTP/HTTPS proxies when rendering
Mermaid diagrams as PNG images using the remote Mermaid.INK API. This
enhancement allows users in restricted network environments to access
the API via a proxy, making the remote rendering feature more robust and
accessible.

The changes include:
- Added optional `proxies` parameter to `draw_mermaid_png` and
`_render_mermaid_using_api` functions
- Updated `Graph.draw_mermaid_png` method to support and pass through
proxy configuration
- Enhanced docstrings with usage examples for the new parameter
- Maintained full backward compatibility with existing code

### Usage Example

```python
proxies = {
        "http": "http://127.0.0.1:7890",
        "https": "http://127.0.0.1:7890"
}

display(Image(chain.get_graph().draw_mermaid_png(proxies=proxies)))

```

### Dependencies

No new dependencies required. Uses existing `requests` library for HTTP
requests.

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2025-11-17 12:45:17 -06:00
Mason Daugherty
52b1516d44 style(langchain): fix some middleware ref syntax (#33988) 2025-11-16 00:33:17 -05:00
Mason Daugherty
8a3bb73c05 release(openai): 1.0.3 (#33981)
- Respect 300k token limit for embeddings API requests #33668
- fix create_agent / response_format for Responses API #33939
- fix response.incomplete event is not handled when using
stream_mode=['messages'] #33871
2025-11-14 19:18:50 -05:00
Mason Daugherty
099c042395 refactor(openai): embedding utils and calculations (#33982)
Now returns (`_iter`, `tokens`, `indices`, token_counts`). The
`token_counts` are calculated directly during tokenization, which is
more accurate and efficient than splitting strings later.
2025-11-14 19:18:37 -05:00
Kaparthy Reddy
2d4f00a451 fix(openai): Respect 300k token limit for embeddings API requests (#33668)
## Description

Fixes #31227 - Resolves the issue where `OpenAIEmbeddings` exceeds
OpenAI's 300,000 token per request limit, causing 400 BadRequest errors.

## Problem

When embedding large document sets, LangChain would send batches
containing more than 300,000 tokens in a single API request, causing
this error:
```
openai.BadRequestError: Error code: 400 - {'error': {'message': 'Requested 673477 tokens, max 300000 tokens per request'}}
```

The issue occurred because:
- The code chunks texts by `embedding_ctx_length` (8191 tokens per
chunk)
- Then batches chunks by `chunk_size` (default 1000 chunks per request)
- **But didn't check**: Total tokens per batch against OpenAI's 300k
limit
- Result: `1000 chunks × 8191 tokens = 8,191,000 tokens` → Exceeds
limit!

## Solution

This PR implements dynamic batching that respects the 300k token limit:

1. **Added constant**: `MAX_TOKENS_PER_REQUEST = 300000`
2. **Track token counts**: Calculate actual tokens for each chunk
3. **Dynamic batching**: Instead of fixed `chunk_size` batches,
accumulate chunks until approaching the 300k limit
4. **Applied to both sync and async**: Fixed both
`_get_len_safe_embeddings` and `_aget_len_safe_embeddings`

## Changes

- Modified `langchain_openai/embeddings/base.py`:
  - Added `MAX_TOKENS_PER_REQUEST` constant
  - Replaced fixed-size batching with token-aware dynamic batching
  - Applied to both sync (line ~478) and async (line ~527) methods
- Added test in `tests/unit_tests/embeddings/test_base.py`:
- `test_embeddings_respects_token_limit()` - Verifies large document
sets are properly batched

## Testing

All existing tests pass (280 passed, 4 xfailed, 1 xpassed).

New test verifies:
- Large document sets (500 texts × 1000 tokens = 500k tokens) are split
into multiple API calls
- Each API call respects the 300k token limit

## Usage

After this fix, users can embed large document sets without errors:
```python
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_text_splitters import CharacterTextSplitter

# This will now work without exceeding token limits
embeddings = OpenAIEmbeddings()
documents = CharacterTextSplitter().split_documents(large_documents)
Chroma.from_documents(documents, embeddings)
```

Resolves #31227

---------

Co-authored-by: Kaparthy Reddy <kaparthyreddy@Kaparthys-MacBook-Air.local>
Co-authored-by: Chester Curme <chester.curme@gmail.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2025-11-14 18:12:07 -05:00
Sydney Runkle
9bd401a6d4 fix: resumable shell, works w/ interrupts (#33978)
fixes https://github.com/langchain-ai/langchain/issues/33684

Now able to run this minimal snippet successfully

```py
import os

from langchain.agents import create_agent
from langchain.agents.middleware import (
    HostExecutionPolicy,
    HumanInTheLoopMiddleware,
    ShellToolMiddleware,
)
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command


shell_middleware = ShellToolMiddleware(
    workspace_root=os.getcwd(),
    env=os.environ,  # danger
    execution_policy=HostExecutionPolicy()
)

hil_middleware = HumanInTheLoopMiddleware(interrupt_on={"shell": True})

checkpointer = InMemorySaver()

agent = create_agent(
    "openai:gpt-4.1-mini",
    middleware=[shell_middleware, hil_middleware],
    checkpointer=checkpointer,
)

input_message = {"role": "user", "content": "run `which python`"}

config = {"configurable": {"thread_id": "1"}}

result = agent.invoke(
    {"messages": [input_message]},
    config=config,
    durability="exit",
)
```
2025-11-14 15:32:25 -05:00
ccurme
6aa3794b74 feat(langchain): reference model profiles for provider strategy (#33974) 2025-11-14 19:24:18 +00:00
Sydney Runkle
189dcf7295 chore: increase coverage for shell, filesystem, and summarization middleware (#33928)
cc generated, just a start here but wanted to bump things up from 70%
ish
2025-11-14 13:30:36 -05:00
Sydney Runkle
1bc88028e6 fix(anthropic): execute bash + file tools via tool node (#33960)
* use `override` instead of directly patching things on `ModelRequest`
* rely on `ToolNode` for execution of tools related to said middleware,
using `wrap_model_call` to inject the relevant claude tool specs +
allowing tool node to forward them along to corresponding langchain tool
implementations
* making the same change for the native shell tool middleware
* allowing shell tool middleware to specify a name for the shell tool
(negative diff then for claude bash middleware)


long term I think the solution might be to attach metadata to a tool to
map the provider spec to a langchain implementation, which we could also
take some lessons from on the MCP front.
2025-11-14 13:17:01 -05:00
Mason Daugherty
d2942351ce release(core): 1.0.5 (#33973) 2025-11-14 11:51:27 -05:00
Sydney Runkle
83c078f363 fix: adding missing async hooks (#33957)
* filling in missing async gaps
* using recommended tool runtime injection instead of injected state
  * updating tests to use helper function as well
2025-11-14 09:13:39 -05:00
ZhangShenao
26d39ffc4a docs: Fix doc links (#33964) 2025-11-14 09:07:32 -05:00
Mason Daugherty
421e2ceeee fix(core): don't mask exceptions (#33959) 2025-11-14 09:05:29 -05:00
Mason Daugherty
275dcbf69f docs(core): add clarity to base token counting methods (#33958)
Wasn't immediately obvious that `get_num_tokens_from_messages` adds
additional prefixes to represent user roles in conversation, which adds
to the overall token count.

```python
from langchain_google_genai import GoogleGenerativeAI

llm = GoogleGenerativeAI(model="gemini-2.5-flash")
num_tokens = llm.get_num_tokens("Hello, world!")
print(f"Number of tokens: {num_tokens}")
# Number of tokens: 4
```

```python
from langchain.messages import HumanMessage

messages = [HumanMessage(content="Hello, world!")]

num_tokens = llm.get_num_tokens_from_messages(messages)
print(f"Number of tokens: {num_tokens}")
# Number of tokens: 6
```
2025-11-13 17:15:47 -05:00
Sydney Runkle
9f87b27a5b fix: add filesystem middleware in init (#33955) 2025-11-13 15:07:33 -05:00
Mason Daugherty
b2e1196e29 chore(core,infra): nits (#33954) 2025-11-13 14:50:54 -05:00
Sydney Runkle
2dc1396380 chore(langchain): update deps (#33951) 2025-11-13 14:21:25 -05:00
Mason Daugherty
77941ab3ce feat(infra): add automatic issue labeling (#33952) 2025-11-13 14:13:52 -05:00
Mason Daugherty
ee19a30dde fix(groq): bump min ver for core dep (#33949)
Due to issue with unit tests and docs URL for exceptions
2025-11-13 11:46:54 -05:00
Mason Daugherty
5d799b3174 release(nomic): 1.0.1 (#33948)
support Python 3.14 #33655
2025-11-13 11:25:39 -05:00
Mason Daugherty
8f33a985a2 release(groq): 1.0.1 (#33947)
- fix: handle tool calls with no args #33896
- add prompt caching token usage details #33708
2025-11-13 11:25:00 -05:00
Mason Daugherty
78eeccef0e release(deepseek): 1.0.1 (#33946)
- support strict beta structured output #32727
2025-11-13 11:24:39 -05:00
ccurme
3d415441e8 fix(langchain, openai): backward compat for response_format (#33945) 2025-11-13 11:11:35 -05:00
ccurme
74385e0ebd fix(langchain, openai): fix create_agent / response_format for Responses API (#33939) 2025-11-13 10:18:15 -05:00
Christophe Bornet
2bfbc29ccc chore(core): fix some ruff TC rules (#33929)
fix some ruff TC rules but still don't enforce them as Pydantic model
fields use type annotations at runtime.
2025-11-12 14:07:19 -05:00
Christophe Bornet
ef79c26f18 chore(cli,standard-tests,text-splitters): fix some ruff TC rules (#33934)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-11-12 14:06:31 -05:00
ccurme
fbe32c8e89 release(anthropic): 1.0.3 (#33935) 2025-11-12 10:55:28 -05:00
Mohammad Mohtashim
2511c28f92 feat(anthropic): support code_execution_20250825 (#33925) 2025-11-12 10:44:51 -05:00
Sydney Runkle
637bb1cbbc feat: refactor tests coverage (#33927)
middleware tests have gotten quite unwieldy, major restructuring, sets
the stage for coverage increase

this is super hard to review -- as a proof that we've retained important
tests, I ran coverage on `master` and this branch and confirmed
identical coverage.

* moving all middleware related tests to `agents/middleware` folder
* consolidating related test files
* adding coverage utility to makefile
2025-11-11 10:40:12 -05:00
Mason Daugherty
3dfea96ec1 chore: update README.md files (#33919) 2025-11-10 22:51:35 -05:00
ccurme
68643153e5 feat(langchain): support async summarization in SummarizationMiddleware (#33918) 2025-11-10 15:48:51 -05:00
Abbas Syed
462762f75b test(core): add comprehensive tests for groq block translator (#33906) 2025-11-10 15:45:36 -05:00
ccurme
4f3729c004 release(model-profiles): 0.0.4 (#33917) 2025-11-10 12:06:32 -05:00
Mason Daugherty
ba428cdf54 chore(infra): add note to pr linting workflow (#33916) 2025-11-10 11:49:31 -05:00
Mason Daugherty
69c7d1b01b test(groq,openai): add retries for flaky tests (#33914) 2025-11-10 10:36:11 -05:00
Mason Daugherty
733299ec13 revert(core): "applied secrets_map in load to plain string values" (#33913)
Reverts langchain-ai/langchain#33678

Breaking API change
2025-11-10 10:29:30 -05:00
ccurme
e1adf781c6 feat(langchain): (SummarizationMiddleware) support use of model context windows when triggering summarization (#33825) 2025-11-10 10:08:52 -05:00
Shahroz Ahmad
31b5e4810c feat(deepseek): support strict beta structured output (#32727)
**Description:** This PR adds support for DeepSeek's beta strict mode
feature for structured
outputs and tool calling. It overrides `bind_tools()` and
`with_structured_output()` to automatically use
DeepSeek's beta endpoint (https://api.deepseek.com/beta) when
`strict=True`. Both methods need overriding because they're independent
entry points and user can call either directly. When DeepSeek's strict
mode graduates from beta, we can just remove both overriden methods. You
can read more about the beta feature here:
https://api-docs.deepseek.com/guides/function_calling#strict-mode-beta
  
**Issue:** Implements #32670 


**Dependencies:** None


**Sample Code**

```python
from langchain_deepseek import ChatDeepSeek
from pydantic import BaseModel, Field
from typing import Optional
import os


# Enter your DeepSeek API Key here
API_KEY = "YOUR_API_KEY"


# location, temperature, condition are required fields
# humidity is optional field with default value
class WeatherInfo(BaseModel):
    location: str = Field(description="City name")
    temperature: int = Field(description="Temperature in Celsius")
    condition: str = Field(description="Weather condition (sunny, cloudy, rainy)")
    humidity: Optional[int] = Field(default=None, description="Humidity percentage")


llm = ChatDeepSeek(
    model="deepseek-chat",
    api_key=API_KEY,
)

# just to confirm that a new instance will use the default base url (instead of beta)
print(f"Default API base: {llm.api_base}")



# Test 1: bind_tools with strict=True shoud list all the tools calls
print("\nTest 1: bind_tools with strict=True")
llm_with_tools = llm.bind_tools([WeatherInfo], strict=True)
response = llm_with_tools.invoke("Tell me the weather in New York. It's 22 degrees, sunny.")
print(response.tool_calls)



# Test 2: with_structured_output with strict=True
print("\nTest 2: with_structured_output with strict=True")
structured_llm = llm.with_structured_output(WeatherInfo, strict=True)
result = structured_llm.invoke("Tell me the weather in New York.")
print(f"  Result: {result}")
assert isinstance(result, WeatherInfo), "Result should be a WeatherInfo instance"
```

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2025-11-09 22:24:33 -05:00
Mason Daugherty
c6801fe159 chore: fix URL underlining in README.md (#33905) 2025-11-09 22:22:56 -05:00
AmazingcatAndrew
1b563067f8 fix(chroma): resolve OpenCLIP + Chroma image embedding test regression (#33899)
**Description:**  
Fixes the OpenCLIP × Chroma regression that caused nested embedding
errors when adding or searching image data.
The test case `test_openclip_chroma_embed_no_nesting_error` has been
restored and verified to work correctly with the current LangChain core
dependencies.
Functional validation confirms that `similarity_search_by_image` now
returns correct, metadata‑preserving results.

**Issue:**  
Fixes #33851

**Dependencies:**  
No new dependencies introduced.  

**Testing:**  
All tests under  
```bash
uv run --group test pytest tests/unit_tests
```  
result:
```
30 passed in 91.26s (0:01:31)
```
have passed successfully using Python 3.13.9 and uv‑managed environment.
This confirms that the regression has been fixed.  

Running  
```bash
make test
```  
still produces cleanup‑time `AttributeError: 'ProactorEventLoop' object
has no attribute '_ssock'` on Windows (Python 3.13+).
This is a benign asyncio teardown message rather than a functional
failure.
`uv run pytest` closes event loops immediately after tests, while `make
test` invokes pytest through a secondary process layer that leaves a
background loop alive at interpreter shutdown.
This difference in teardown behavior explains the extra messages seen
only when using `make test`.

**Summary:**  
- Verified the OpenCLIP + Chroma image pipeline works correctly.  
- `uv run --group test pytest` fully passes; the fix is complete.  
- The residual `_ssock` warnings occur only during
Windows asyncio cleanup and are not related to this code change.

This is my first time contributing code, please contact me with any
questions

---

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2025-11-09 21:24:33 -05:00
Mason Daugherty
1996d81d72 chore(langchain): pass on reference docstrings (middleware) (#33904) 2025-11-09 21:18:28 -05:00
Mason Daugherty
ab0677c6f1 fix(groq): handle tool calls with no args (#33896)
When Groq returns tool calls with no arguments, it sends arguments:
`'null'` (JSON null), but LangChain's core parsing expects either a dict
or converts null to Python None, which fails the `isinstance(args_,
dict)` check and incorrectly marks the tool call as invalid.

Related to #32017
2025-11-08 22:30:44 -05:00
artreimus
bdb53c93cc docs(langchain): correct IBM provider link in chat_models docstring (#33897)
**PR title**

```
docs(langchain): correct IBM provider link in chat_models docstring
```

**PR message**

**Description**
Fix broken link in the `chat_models` docstring. The **ibm** bullet
incorrectly linked to the DeepSeek provider page; update it to the
canonical IBM provider docs.

This only affects generated API reference content on
`reference.langchain.com`. No runtime behavior changes.

**Issue**
N/A (documentation-only).

**Dependencies**
None.

**Testing & quality**

* Ran `make format`, `make lint`, and `make test` in the package (no
code changes expected to affect tests).
2025-11-08 07:02:33 -06:00
Alazar Genene
94d5271cb5 fix(standard-tests): fix semantic typo in if statement (#33890) 2025-11-07 18:01:59 -05:00
ccurme
e499db4266 release(langchain): 1.0.5 (#33893) 2025-11-07 17:54:43 -05:00
npage902
cc3af82b47 fix(core): applied secrets_map in load to plain string values (#33678)
Replaces #33618 

**Description:** Fixes the bug in the `load()` function where secret
placeholders in plain dicts were not replaced, even if they match a key
in `secrets_map`, and adds a test case.

Example:
```py
obj = {"api_key": "__SECRET_API_KEY__"}
secret_key = "secret_key_1234"
secrets_map = {"__SECRET_API_KEY__": secret_key}
result = load(obj, secrets_map=secrets_map)
```
Before this change, printing `api_key` in `result` would output
`"__SECRET_API_KEY__"`. Now, it will properly output
`"secret_key_1234"`.

**Issue:** Fixes #31804 

**Dependencies:** None

`make format`, `make lint`, and `make test` have all passed on my
machine.

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2025-11-07 17:14:13 -05:00
Mshari
9383b78be1 feat(groq): add prompt caching token usage details (#33708)
**Description:** 
Adds support for prompt caching usage metadata in ChatGroq. The
integration now captures cached token information from the Groq API
response and includes it in the `input_token_details` field of the
`usage_metadata`.

Changes:
- Created new `_create_usage_metadata()` helper function to centralize
usage metadata creation logic
- Extracts `cached_tokens` from `prompt_tokens_details` in API responses
and maps to `input_token_details.cache_read`
- Integrated the helper function in both streaming
(`_convert_chunk_to_message_chunk`) and non-streaming
(`_create_chat_result`) code paths
- Added comprehensive unit tests to verify caching metadata handling and
backward compatibility

This enables users to monitor prompt caching effectiveness when using
Groq models with prompt caching enabled.

**Issue:** N/A

**Dependencies:** None

---------

Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-11-07 17:05:22 -05:00
ccurme
3c492571ab release(anthropic): 1.0.2 (#33888) 2025-11-07 16:47:25 -05:00
ccurme
f2410f7ea7 revert: Support for SystemMessage in create_agent (#33889)
Reverts langchain-ai/langchain#33640

Introduces lint errors into langchain-anthropic

Should incorporate into 1.1 instead of patch release.
2025-11-07 16:44:11 -05:00
Mason Daugherty
91560b6a7a chore(infra): expand PR labeling (#33887) 2025-11-07 16:37:35 -05:00
ccurme
b1dd448233 release(core): 1.0.4 (#33886) 2025-11-07 16:26:44 -05:00
dy93
904daf6f40 feat(core): support draw subgraph using pygraphviz (#32966)
The `draw_png()` method currently does not support drawing subgraphs.
This PR adds the ability to render subgraph outlines, improving
visualization clarity when working with nested structures.
2025-11-07 15:58:35 -05:00
Mohammad Mohtashim
8e31a5d7bd fix(core): Fix tool name check in name_dict for PydanticToolsParser (#33479)
- **Description:** The root cause of this issue is that when a user
defines `model_config` in a `BaseModel`, the `{"type": <tool_name>}`
value is derived from the title specified in `model_config` when the
results are parsed
[here](https://vscode.dev/github/keenborder786/langchain/blob/fix/tool_name_dict/libs/core/langchain_core/output_parsers/openai_tools.py#L199).
However,
[tool.__name__](https://vscode.dev/github/keenborder786/langchain/blob/fix/tool_name_dict/libs/core/langchain_core/output_parsers/openai_tools.py#L331)
uses the class name (in uppercase) of the `BaseModel`, resulting in a
`KeyError` when a custom title is provided in `model_config`.
 

The Best Solution will be to use the title provided in `model_config`
attribute if provided one since that is what `type` will be parsed to,
if not then use `tool.__name__`. But need to make sure that this works
only for Pydantic V2.

  - **Issue:** #27260

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-11-07 15:39:47 -05:00
Sydney Runkle
ee630b4539 fix: bump up default recursion limit (#33881)
Fixes https://github.com/langchain-ai/langchain/issues/33740

We don't want to depend on recursion limit here, model call limit
middleware is more appropriate
2025-11-07 13:49:12 -06:00
Jacob Lee
46971447df fix(core): Filter empty content blocks from formatted prompts (#32519)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-07 14:39:25 -05:00
Azibek
d8b94007c1 fix(huggingface): pass llm params to ChatHuggingFace (#32368)
This PR fixes #32234 and improves HuggingFace chat model integration by:

Ensuring ChatHuggingFace inherits key parameters (temperature,
max_tokens, top_p, streaming, etc.) from the underlying LLM when not
explicitly set.
Adding and updating unit tests to verify property inheritance.
No breaking changes; these updates enhance reliability and
maintainability.

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2025-11-07 14:29:15 -05:00
Mohammad Mohtashim
cf595dcc38 chore(langchain): Support for SystemMessage in create_agent (#33640)
- **Description:** Updated Function Signature of `create_agent`, the
system prompt can be both a list and string. I see no harm in doing
this, since SystemMessage accepts both.
- **Issue:** #33630

---------

Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-11-07 13:00:38 -06:00
Copilot
d27211cfa7 fix(core): context preservation in shielded async callbacks (#32163)
The `@shielded` decorator in async callback managers was not preserving
context variables, breaking OpenTelemetry instrumentation and other
context-dependent functionality.

## Problem

When using async callbacks with the `@shielded` decorator (applied to
methods like `on_llm_end`, `on_chain_end`, etc.), context variables were
not being preserved across the shield boundary. This caused issues with:

- OpenTelemetry span context propagation
- Other instrumentation that relies on context variables
- Inconsistent context behavior between sync and async execution

The issue was reproducible with:

```python
from contextvars import copy_context
import asyncio
from langgraph.graph import StateGraph

# Sync case: context remains consistent
print("SYNC")
print(copy_context())  # Same object
graph.invoke({"result": "init"})
print(copy_context())  # Same object

# Async case: context was inconsistent (before fix)
print("ASYNC") 
asyncio.run(graph.ainvoke({"result": "init"}))
print(copy_context())  # Different object than expected
```

## Root Cause

The original `shielded` decorator implementation:

```python
async def wrapped(*args: Any, **kwargs: Any) -> Any:
    return await asyncio.shield(func(*args, **kwargs))
```

Used `asyncio.shield()` directly without preserving the current
execution context, causing context variables to be lost.

## Solution

Modified the `shielded` decorator to:

1. Capture the current context using `copy_context()`
2. Create a task with explicit context using `asyncio.create_task(coro,
context=ctx)` for Python 3.11+
3. Shield the context-aware task
4. Fallback to regular task creation for Python < 3.11

```python
async def wrapped(*args: Any, **kwargs: Any) -> Any:
    # Capture the current context to preserve context variables
    ctx = copy_context()
    coro = func(*args, **kwargs)
    
    try:
        # Create a task with the captured context to preserve context variables
        task = asyncio.create_task(coro, context=ctx)
        return await asyncio.shield(task)
    except TypeError:
        # Python < 3.11 fallback
        task = asyncio.create_task(coro)
        return await asyncio.shield(task)
```

## Testing

- Added comprehensive test
`test_shielded_callback_context_preservation()` that validates context
variables are preserved across shielded callback boundaries
- Verified the fix resolves the original LangGraph context consistency
issue
- Confirmed all existing callback manager tests still pass
- Validated OpenTelemetry-like instrumentation scenarios work correctly

The fix is minimal, maintains backward compatibility, and ensures proper
context preservation for both modern Python versions and older ones.

Fixes #31398.

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 Share your feedback on Copilot coding agent for the chance to win a
$200 gift card! Click
[here](https://survey.alchemer.com/s3/8343779/Copilot-Coding-agent) to
start the survey.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mdrxy <61371264+mdrxy@users.noreply.github.com>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-11-07 13:09:47 -05:00
Swastik-Swarup-Dash
ca1a3fbe88 fix(core): RunnablePick may not return a dict if keys is a string (#31321)
Change made From:
```python
class RunnablePick(RunnableSerializable[dict[str, Any], dict[str, Any]]):
```
To:
```python
class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
```
As suggested by @cbornet 

Fixes ##31309

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2025-11-07 13:04:20 -05:00
williamzhu54
c955b53aed fix(core): fix Runnable parallel schema being empty when children runnable input schemas use TypedDict (#28196)
# Description
This submission is a part of a school project from our team of 4
@EminGul @williamzhu54 @annay54 @donttouch22.

Our pull request fixes the issue with RunnableParallel scheme being
empty by returning the correct schema output when children runnable
input schemas use TypedDicts.

# Issue
Fixes #24326


# Dependencies
No extra dependencies required for this fix.

# Feedback
Any feedback and advice is gladly welcomed. Please feel free to let us
know what we can change or improve upon regarding this issue.

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
2025-11-07 12:01:21 -05:00
Christophe Bornet
2a626d9608 refactor(langchain): use create_importer for HypotheticalDocumentEmbedder (#32078) 2025-11-07 11:16:00 -05:00
Abhinav
0861cba04b fix(chroma): pydantic validation error when using retriever.invoke() (#31377) 2025-11-07 10:59:16 -05:00
Lê Nam Khánh
88246f45b3 docs: fix typos in libs/core/langchain_core/utils/function_calling.py (#33873) 2025-11-07 10:34:28 -05:00
Lê Nam Khánh
1d04514354 docs: fix typos in libs/core/tests/unit_tests/utils/test_strings.py (#33875) 2025-11-07 10:34:12 -05:00
Lê Nam Khánh
c2324b8f3e docs: fix typos in libs/langchain/langchain_classic/chains/summarize/chain.py (#33877) 2025-11-07 10:33:53 -05:00
Lê Nam Khánh
957ea65d12 docs: fix typos in libs/core/tests/unit_tests/indexing/test_hashed_document.py (#33874) 2025-11-07 10:32:20 -05:00
Lê Nam Khánh
00fa38a295 docs: fix typos in libs/core/tests/unit_tests/test_tools.py (#33876) 2025-11-07 10:31:57 -05:00
Lê Nam Khánh
9d98c1b669 docs: fix typos in libs/partners/groq/langchain_groq/chat_models.py (#33878) 2025-11-07 10:31:35 -05:00
Mahmut CAVDAR
00cc9d421f fix(langchain): Update langchain-core dependency version (#33775) 2025-11-07 10:31:06 -05:00
Mohammad Mohtashim
65716cf590 feat(perplexity): Created Dedicated Output Parser to Support Reasoning Model Output for perplexity (#33670) 2025-11-07 10:17:35 -05:00
riunyfir
1b77a191f4 feat: The response.incomplete event is not handled when using stream_mode=['messages'] (#33871) 2025-11-07 09:46:11 -05:00
repeat-Q
ebfde9173c docs: expand "Why use LangChain?" section in README (#33846) 2025-11-07 09:09:05 -05:00
Lê Nam Khánh
2fe0369049 docs: fix typos in some files (#33867) 2025-11-07 09:04:29 -05:00
Mason Daugherty
e023201d42 style: some cleanup (#33857) 2025-11-06 23:50:46 -05:00
Mason Daugherty
d40e340479 chore: attribute package change versions (#33854)
Needed to disambiguate for within inherited docs
2025-11-06 16:57:30 -05:00
Sydney Runkle
9a09ed0659 fix: don't trace conditional edges and no todos in input state (#33842)
while experimenting w/ todo middleware

| Before | After |
|--------|-------|
| ![Screenshot 2025-11-05 at 1 56 21
PM](https://github.com/user-attachments/assets/63195ae4-8122-4662-8246-0fbc16cb1e22)
| ![Screenshot 2025-11-05 at 1 56 03
PM](https://github.com/user-attachments/assets/255e2fa8-e52d-4d1a-949a-33df52ee6668)
|
| Tracing conditional edges (verbose) | Not tracing conditional edges
(cleaner) |
| ![Screenshot 2025-11-05 at 1 57 56
PM](https://github.com/user-attachments/assets/449ccfe9-4c21-4c87-8e0e-6e89d7a97611)
| ![Screenshot 2025-11-05 at 1 56 58
PM](https://github.com/user-attachments/assets/c5c28d0e-2153-4572-af29-b2528761fec6)
|
| Todos in input state (cluttered) | No todos in input state (cleaner) |
2025-11-05 14:25:57 -05:00
Mason Daugherty
5f27b546dd chore: update README.md with deepagents (#33843) 2025-11-05 14:22:20 -05:00
Mason Daugherty
022fdd52c3 fix(core): handle missing dependency version information (#33844)
Follow up to #33347

This continues to make searching issues difficult
2025-11-05 14:19:55 -05:00
Sydney Runkle
7946a8f64e release: langchain v1.0.4 (#33839) 2025-11-05 12:37:58 -05:00
Sydney Runkle
7af79039fc fix: only increment thread count on successful executions (#33837)
* for run count + thread count overflow we should warn model not to call
again
* don't tally mocked tool calls in thread limit -- consider the
following
  * run limit is 1 
  * thread limit is 3
  * first run calls the tool 2 times, 1 executes, 1 is blocked
* we should only count the successful execution above towards the total
thread count
* raise more helpful warnings on invalid config
2025-11-05 10:00:07 -05:00
Sydney Runkle
1755750ca1 fix: more robust tool call limit middleware (#33817)
* improving typing (covariance)
* adding in support for continuing w/ tool calls not yet at threshold,
switching default to continue
* moving all logic into after model

```py
ExitBehavior = Literal["continue", "error", "end"]
"""How to handle execution when tool call limits are exceeded.
- `"continue"`: Block exceeded tools with error messages, let other tools continue (default)
- `"error"`: Raise a `ToolCallLimitExceededError` exception
- `"end"`: Stop execution immediately, injecting a ToolMessage and an AI message
    for the single tool call that exceeded the limit. Raises `NotImplementedError`
    if there are multiple tool calls
"""
```
2025-11-05 09:18:21 -05:00
Mason Daugherty
ddb53672e2 chore(infra): remove unused pr-title-labeler.yml (#33831) 2025-11-04 20:06:52 -05:00
Mason Daugherty
eeae34972f chore(infra): drop langchain_v1 pr lint (#33830)
Just use `langchain`
2025-11-04 19:46:05 -05:00
Mason Daugherty
47d89b1e47 fix(langchain): remove Tigris (#33829)
Removing this code as there is no possible way for it to work.

See https://github.com/langchain-ai/langchain-community/pull/159
2025-11-04 19:45:52 -05:00
Mason Daugherty
ee0bdaeb79 chore: correct langchain-community references (#33827)
fix docstrings that referenced community versions of now-native packages
2025-11-04 17:01:35 -05:00
Christophe Bornet
915c446c48 chore(core): add ruff rule PLR2004 (#33706)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-11-04 13:33:37 -05:00
Mason Daugherty
d1e2099408 chore(core): clean pyproject formatting (#33821) 2025-11-04 18:21:15 +00:00
Mason Daugherty
6ea15b9efa docs(model-profiles): fix typo (#33820) 2025-11-04 18:19:55 +00:00
Mason Daugherty
69f33aaff5 chore(infra): remova unused poetry_setup action (#33819) 2025-11-04 13:18:55 -05:00
Mason Daugherty
3f66f102d2 chore: update issue template xref url (#33818) 2025-11-04 13:17:42 -05:00
Mason Daugherty
c6547f58b7 style(standard-tests): refs pass (#33814) 2025-11-04 00:01:16 -05:00
Mason Daugherty
dfb05a7fa0 style: refs pass (#33813) 2025-11-03 22:11:10 -05:00
419 changed files with 26924 additions and 27930 deletions

View File

@@ -8,16 +8,15 @@ body:
value: |
Thank you for taking the time to file a bug report.
Use this to report BUGS in LangChain. For usage questions, feature requests and general design questions, please use the [LangChain Forum](https://forum.langchain.com/).
For usage questions, feature requests and general design questions, please use the [LangChain Forum](https://forum.langchain.com/).
Relevant links to check before filing a bug report to see if your issue has already been reported, fixed or
if there's another way to solve your problem:
Check these before submitting to see if your issue has already been reported, fixed or if there's another way to solve your problem:
* [LangChain Forum](https://forum.langchain.com/),
* [LangChain documentation with the integrated search](https://docs.langchain.com/oss/python/langchain/overview),
* [API Reference](https://reference.langchain.com/python/),
* [Documentation](https://docs.langchain.com/oss/python/langchain/overview),
* [API Reference Documentation](https://reference.langchain.com/python/),
* [LangChain ChatBot](https://chat.langchain.com/)
* [GitHub search](https://github.com/langchain-ai/langchain),
* [LangChain Forum](https://forum.langchain.com/),
- type: checkboxes
id: checks
attributes:
@@ -36,16 +35,48 @@ body:
required: true
- label: This is not related to the langchain-community package.
required: true
- label: I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example).
required: true
- label: I posted a self-contained, minimal, reproducible example. A maintainer can copy it and run it AS IS.
required: true
- type: checkboxes
id: package
attributes:
label: Package (Required)
description: |
Which `langchain` package(s) is this bug related to? Select at least one.
Note that if the package you are reporting for is not listed here, it is not in this repository (e.g. `langchain-google-genai` is in [`langchain-ai/langchain-google`](https://github.com/langchain-ai/langchain-google/)).
Please report issues for other packages to their respective repositories.
options:
- label: langchain
- label: langchain-openai
- label: langchain-anthropic
- label: langchain-classic
- label: langchain-core
- label: langchain-cli
- label: langchain-model-profiles
- label: langchain-tests
- label: langchain-text-splitters
- label: langchain-chroma
- label: langchain-deepseek
- label: langchain-exa
- label: langchain-fireworks
- label: langchain-groq
- label: langchain-huggingface
- label: langchain-mistralai
- label: langchain-nomic
- label: langchain-ollama
- label: langchain-perplexity
- label: langchain-prompty
- label: langchain-qdrant
- label: langchain-xai
- label: Other / not sure / general
- type: textarea
id: reproduction
validations:
required: true
attributes:
label: Example Code
label: Example Code (Python)
description: |
Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case.
@@ -53,15 +84,12 @@ body:
**Important!**
* Avoid screenshots when possible, as they are hard to read and (more importantly) don't allow others to copy-and-paste your code.
* Reduce your code to the minimum required to reproduce the issue if possible. This makes it much easier for others to help you.
* Use code tags (e.g., ```python ... ```) to correctly [format your code](https://help.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks#syntax-highlighting).
* INCLUDE the language label (e.g. `python`) after the first three backticks to enable syntax highlighting. (e.g., ```python rather than ```).
* Avoid screenshots, as they are hard to read and (more importantly) don't allow others to copy-and-paste your code.
* Reduce your code to the minimum required to reproduce the issue if possible.
(This will be automatically formatted into code, so no need for backticks.)
render: python
placeholder: |
The following code:
```python
from langchain_core.runnables import RunnableLambda
def bad_code(inputs) -> int:
@@ -69,17 +97,14 @@ body:
chain = RunnableLambda(bad_code)
chain.invoke('Hello!')
```
- type: textarea
id: error
validations:
required: false
attributes:
label: Error Message and Stack Trace (if applicable)
description: |
If you are reporting an error, please include the full error message and stack trace.
placeholder: |
Exception + full stack trace
If you are reporting an error, please copy and paste the full error message and
stack trace.
(This will be automatically formatted into code, so no need for backticks.)
render: shell
- type: textarea
id: description
attributes:
@@ -99,9 +124,7 @@ body:
attributes:
label: System Info
description: |
Please share your system info with us. Do NOT skip this step and please don't trim
the output. Most users don't include enough information here and it makes it harder
for us to help you.
Please share your system info with us.
Run the following command in your terminal and paste the output here:
@@ -113,8 +136,6 @@ body:
from langchain_core import sys_info
sys_info.print_sys_info()
```
alternatively, put the entire output of `pip freeze` here.
placeholder: |
python -m langchain_core.sys_info
validations:

View File

@@ -1,9 +1,18 @@
blank_issues_enabled: false
version: 2.1
contact_links:
- name: 📚 Documentation
url: https://github.com/langchain-ai/docs/issues/new?template=langchain.yml
- 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
- name: 📚 LangChain Documentation
url: https://docs.langchain.com/oss/python/langchain/overview
about: View the official LangChain documentation
- 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

View File

@@ -13,11 +13,11 @@ body:
Relevant links to check before filing a feature request to see if your request has already been made or
if there's another way to achieve what you want:
* [LangChain Forum](https://forum.langchain.com/),
* [LangChain documentation with the integrated search](https://docs.langchain.com/oss/python/langchain/overview),
* [API Reference](https://reference.langchain.com/python/),
* [Documentation](https://docs.langchain.com/oss/python/langchain/overview),
* [API Reference Documentation](https://reference.langchain.com/python/),
* [LangChain ChatBot](https://chat.langchain.com/)
* [GitHub search](https://github.com/langchain-ai/langchain),
* [LangChain Forum](https://forum.langchain.com/),
- type: checkboxes
id: checks
attributes:
@@ -34,6 +34,40 @@ body:
required: true
- label: This is not related to the langchain-community package.
required: true
- type: checkboxes
id: package
attributes:
label: Package (Required)
description: |
Which `langchain` package(s) is this request related to? Select at least one.
Note that if the package you are requesting for is not listed here, it is not in this repository (e.g. `langchain-google-genai` is in `langchain-ai/langchain`).
Please submit feature requests for other packages to their respective repositories.
options:
- label: langchain
- label: langchain-openai
- label: langchain-anthropic
- label: langchain-classic
- label: langchain-core
- label: langchain-cli
- label: langchain-model-profiles
- label: langchain-tests
- label: langchain-text-splitters
- label: langchain-chroma
- label: langchain-deepseek
- label: langchain-exa
- label: langchain-fireworks
- label: langchain-groq
- label: langchain-huggingface
- label: langchain-mistralai
- label: langchain-nomic
- label: langchain-ollama
- label: langchain-perplexity
- label: langchain-prompty
- label: langchain-qdrant
- label: langchain-xai
- label: Other / not sure / general
- type: textarea
id: feature-description
validations:

View File

@@ -18,3 +18,33 @@ body:
attributes:
label: Issue Content
description: Add the content of the issue here.
- type: checkboxes
id: package
attributes:
label: Package (Required)
description: |
Please select package(s) that this issue is related to.
options:
- label: langchain
- label: langchain-openai
- label: langchain-anthropic
- label: langchain-classic
- label: langchain-core
- label: langchain-cli
- label: langchain-model-profiles
- label: langchain-tests
- label: langchain-text-splitters
- label: langchain-chroma
- label: langchain-deepseek
- label: langchain-exa
- label: langchain-fireworks
- label: langchain-groq
- label: langchain-huggingface
- label: langchain-mistralai
- label: langchain-nomic
- label: langchain-ollama
- label: langchain-perplexity
- label: langchain-prompty
- label: langchain-qdrant
- label: langchain-xai
- label: Other / not sure / general

View File

@@ -25,13 +25,13 @@ body:
label: Task Description
description: |
Provide a clear and detailed description of the task.
What needs to be done? Be specific about the scope and requirements.
placeholder: |
This task involves...
The goal is to...
Specific requirements:
- ...
- ...
@@ -43,7 +43,7 @@ body:
label: Acceptance Criteria
description: |
Define the criteria that must be met for this task to be considered complete.
What are the specific deliverables or outcomes expected?
placeholder: |
This task will be complete when:
@@ -58,15 +58,15 @@ body:
label: Context and Background
description: |
Provide any relevant context, background information, or links to related issues/PRs.
Why is this task needed? What problem does it solve?
placeholder: |
Background:
- ...
Related issues/PRs:
- #...
Additional context:
- ...
validations:
@@ -77,15 +77,45 @@ body:
label: Dependencies
description: |
List any dependencies or blockers for this task.
Are there other tasks, issues, or external factors that need to be completed first?
placeholder: |
This task depends on:
- [ ] Issue #...
- [ ] PR #...
- [ ] External dependency: ...
Blocked by:
- ...
validations:
required: false
- type: checkboxes
id: package
attributes:
label: Package (Required)
description: |
Please select package(s) that this task is related to.
options:
- label: langchain
- label: langchain-openai
- label: langchain-anthropic
- label: langchain-classic
- label: langchain-core
- label: langchain-cli
- label: langchain-model-profiles
- label: langchain-tests
- label: langchain-text-splitters
- label: langchain-chroma
- label: langchain-deepseek
- label: langchain-exa
- label: langchain-fireworks
- label: langchain-groq
- label: langchain-huggingface
- label: langchain-mistralai
- label: langchain-nomic
- label: langchain-ollama
- label: langchain-perplexity
- label: langchain-prompty
- label: langchain-qdrant
- label: langchain-xai
- label: Other / not sure / general

View File

@@ -1,28 +1,30 @@
(Replace this entire block of text)
Thank you for contributing to LangChain! Follow these steps to mark your pull request as ready for review. **If any of these steps are not completed, your PR will not be considered for review.**
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
- [ ] **PR title**: Follows the format: {TYPE}({SCOPE}): {DESCRIPTION}
- Examples:
- fix(anthropic): resolve flag parsing error
- feat(core): add multi-tenant support
- fix(cli): resolve flag parsing error
- docs(openai): update API usage examples
- Allowed `{TYPE}` values:
- feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, release
- Allowed `{SCOPE}` values (optional):
- core, cli, langchain, standard-tests, text-splitters, docs, anthropic, chroma, deepseek, exa, fireworks, groq, huggingface, mistralai, nomic, ollama, openai, perplexity, prompty, qdrant, xai, infra
- Once you've written the title, please delete this checklist item; do not include it in the PR.
- 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
- [ ] **PR message**: ***Delete this entire checklist*** and replace with
- **Description:** a description of the change. Include a [closing keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) if applicable to a relevant issue.
- **Issue:** the issue # it fixes, if applicable (e.g. Fixes #123)
- **Dependencies:** any dependencies required for this change
2. PR description:
- [ ] **Lint and test**: Run `make format`, `make lint` and `make test` from the root of the package(s) you've modified. **We will not consider a PR unless these three are passing in CI.** See [contribution guidelines](https://docs.langchain.com/oss/python/contributing) for more.
- 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:
- Most PRs should not touch more than one package.
- Please do not add dependencies to `pyproject.toml` files (even optional ones) unless they are **required** for unit tests. Likewise, please do not update the `uv.lock` files unless you are adding a required dependency.
- Changes should be backwards compatible.
- Make sure optional dependencies are imported within a function.
- 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.

View File

@@ -1,93 +0,0 @@
# An action for setting up poetry install with caching.
# Using a custom action since the default action does not
# take poetry install groups into account.
# Action code from:
# https://github.com/actions/setup-python/issues/505#issuecomment-1273013236
name: poetry-install-with-caching
description: Poetry install with support for caching of dependency groups.
inputs:
python-version:
description: Python version, supporting MAJOR.MINOR only
required: true
poetry-version:
description: Poetry version
required: true
cache-key:
description: Cache key to use for manual handling of caching
required: true
working-directory:
description: Directory whose poetry.lock file should be cached
required: true
runs:
using: composite
steps:
- uses: actions/setup-python@v5
name: Setup python ${{ inputs.python-version }}
id: setup-python
with:
python-version: ${{ inputs.python-version }}
- uses: actions/cache@v4
id: cache-bin-poetry
name: Cache Poetry binary - Python ${{ inputs.python-version }}
env:
SEGMENT_DOWNLOAD_TIMEOUT_MIN: "1"
with:
path: |
/opt/pipx/venvs/poetry
# This step caches the poetry installation, so make sure it's keyed on the poetry version as well.
key: bin-poetry-${{ runner.os }}-${{ runner.arch }}-py-${{ inputs.python-version }}-${{ inputs.poetry-version }}
- name: Refresh shell hashtable and fixup softlinks
if: steps.cache-bin-poetry.outputs.cache-hit == 'true'
shell: bash
env:
POETRY_VERSION: ${{ inputs.poetry-version }}
PYTHON_VERSION: ${{ inputs.python-version }}
run: |
set -eux
# Refresh the shell hashtable, to ensure correct `which` output.
hash -r
# `actions/cache@v3` doesn't always seem able to correctly unpack softlinks.
# Delete and recreate the softlinks pipx expects to have.
rm /opt/pipx/venvs/poetry/bin/python
cd /opt/pipx/venvs/poetry/bin
ln -s "$(which "python$PYTHON_VERSION")" python
chmod +x python
cd /opt/pipx_bin/
ln -s /opt/pipx/venvs/poetry/bin/poetry poetry
chmod +x poetry
# Ensure everything got set up correctly.
/opt/pipx/venvs/poetry/bin/python --version
/opt/pipx_bin/poetry --version
- name: Install poetry
if: steps.cache-bin-poetry.outputs.cache-hit != 'true'
shell: bash
env:
POETRY_VERSION: ${{ inputs.poetry-version }}
PYTHON_VERSION: ${{ inputs.python-version }}
# Install poetry using the python version installed by setup-python step.
run: pipx install "poetry==$POETRY_VERSION" --python '${{ steps.setup-python.outputs.python-path }}' --verbose
- name: Restore pip and poetry cached dependencies
uses: actions/cache@v4
env:
SEGMENT_DOWNLOAD_TIMEOUT_MIN: "4"
WORKDIR: ${{ inputs.working-directory == '' && '.' || inputs.working-directory }}
with:
path: |
~/.cache/pip
~/.cache/pypoetry/virtualenvs
~/.cache/pypoetry/cache
~/.cache/pypoetry/artifacts
${{ env.WORKDIR }}/.venv
key: py-deps-${{ runner.os }}-${{ runner.arch }}-py-${{ inputs.python-version }}-poetry-${{ inputs.poetry-version }}-${{ inputs.cache-key }}-${{ hashFiles(format('{0}/**/poetry.lock', env.WORKDIR)) }}

View File

@@ -7,13 +7,12 @@ core:
- any-glob-to-any-file:
- "libs/core/**/*"
langchain:
langchain-classic:
- changed-files:
- any-glob-to-any-file:
- "libs/langchain/**/*"
- "libs/langchain_v1/**/*"
v1:
langchain:
- changed-files:
- any-glob-to-any-file:
- "libs/langchain_v1/**/*"
@@ -28,6 +27,11 @@ standard-tests:
- any-glob-to-any-file:
- "libs/standard-tests/**/*"
model-profiles:
- changed-files:
- any-glob-to-any-file:
- "libs/model-profiles/**/*"
text-splitters:
- changed-files:
- any-glob-to-any-file:
@@ -39,6 +43,81 @@ integration:
- any-glob-to-any-file:
- "libs/partners/**/*"
anthropic:
- changed-files:
- any-glob-to-any-file:
- "libs/partners/anthropic/**/*"
chroma:
- changed-files:
- any-glob-to-any-file:
- "libs/partners/chroma/**/*"
deepseek:
- changed-files:
- any-glob-to-any-file:
- "libs/partners/deepseek/**/*"
exa:
- changed-files:
- any-glob-to-any-file:
- "libs/partners/exa/**/*"
fireworks:
- changed-files:
- any-glob-to-any-file:
- "libs/partners/fireworks/**/*"
groq:
- changed-files:
- any-glob-to-any-file:
- "libs/partners/groq/**/*"
huggingface:
- changed-files:
- any-glob-to-any-file:
- "libs/partners/huggingface/**/*"
mistralai:
- changed-files:
- any-glob-to-any-file:
- "libs/partners/mistralai/**/*"
nomic:
- changed-files:
- any-glob-to-any-file:
- "libs/partners/nomic/**/*"
ollama:
- changed-files:
- any-glob-to-any-file:
- "libs/partners/ollama/**/*"
openai:
- changed-files:
- any-glob-to-any-file:
- "libs/partners/openai/**/*"
perplexity:
- changed-files:
- any-glob-to-any-file:
- "libs/partners/perplexity/**/*"
prompty:
- changed-files:
- any-glob-to-any-file:
- "libs/partners/prompty/**/*"
qdrant:
- changed-files:
- any-glob-to-any-file:
- "libs/partners/qdrant/**/*"
xai:
- changed-files:
- any-glob-to-any-file:
- "libs/partners/xai/**/*"
# Infrastructure and DevOps
infra:
- changed-files:

View File

@@ -1,41 +0,0 @@
# PR title labeler config
#
# Labels PRs based on conventional commit patterns in titles
#
# Format: type(scope): description or type!: description (breaking)
add-missing-labels: true
clear-prexisting: false
include-commits: false
include-title: true
label-for-breaking-changes: breaking
label-mapping:
documentation: ["docs"]
feature: ["feat"]
fix: ["fix"]
infra: ["build", "ci", "chore"]
integration:
[
"anthropic",
"chroma",
"deepseek",
"exa",
"fireworks",
"groq",
"huggingface",
"mistralai",
"nomic",
"ollama",
"openai",
"perplexity",
"prompty",
"qdrant",
"xai",
]
linting: ["style"]
performance: ["perf"]
refactor: ["refactor"]
release: ["release"]
revert: ["revert"]
tests: ["test"]

View File

@@ -25,19 +25,18 @@ import tomllib
from get_min_versions import get_min_version_from_toml
from packaging.requirements import Requirement
LANGCHAIN_DIRS = [
"libs/core",
"libs/text-splitters",
"libs/langchain",
"libs/langchain_v1",
"libs/model-profiles",
]
# Define explicit dependency relationships for main LangChain packages.
# Key = package directory, Value = set of directories that should trigger tests for this package
PACKAGE_DEPENDENCIES: Dict[str, Set[str]] = {
"libs/core": set(), # core has no upstream dependencies
"libs/text-splitters": {"libs/core"},
"libs/langchain": {"libs/core", "libs/text-splitters"},
"libs/langchain_v1": {"libs/core"},
"libs/model-profiles": set(), # model-profiles is independent
}
# When set to True, we are ignoring core dependents
# in order to be able to get CI to pass for each individual
# package that depends on core
# e.g. if you touch core, we don't then add textsplitters/etc to CI
IGNORE_CORE_DEPENDENTS = False
# All main LangChain directories (order doesn't matter with explicit deps)
LANGCHAIN_DIRS = list(PACKAGE_DEPENDENCIES.keys())
# ignored partners are removed from dependents
# but still run if directly edited
@@ -61,7 +60,7 @@ def all_package_dirs() -> Set[str]:
def dependents_graph() -> dict:
"""Construct a mapping of package -> dependents
"""Construct a mapping of package -> dependents.
Done such that we can run tests on all dependents of a package when a change is made.
"""
@@ -126,6 +125,25 @@ def add_dependents(dirs_to_eval: Set[str], dependents: dict) -> List[str]:
return list(updated)
def get_affected_packages(changed_dir: str) -> Set[str]:
"""Get all packages that should be tested when a directory changes.
Args:
changed_dir: The directory that was changed (e.g., "libs/core")
Returns:
Set of package directories that depend on the changed directory
"""
affected = {changed_dir}
# Check each package to see if it depends on the changed directory
for pkg_dir, dependencies in PACKAGE_DEPENDENCIES.items():
if changed_dir in dependencies:
affected.add(pkg_dir)
return affected
def _get_configs_for_single_dir(job: str, dir_: str) -> List[Dict[str, str]]:
if job == "test-pydantic":
return _get_pydantic_test_configs(dir_)
@@ -254,27 +272,23 @@ if __name__ == "__main__":
# infrastructure don't inadvertently break package testing, even if the change
# appears unrelated (e.g., documentation build workflows). This is intentionally
# conservative to catch unexpected side effects from workflow modifications.
#
# Example: A PR modifying .github/workflows/api_doc_build.yml will trigger
# lint/test jobs for libs/core, libs/text-splitters, libs/langchain, and
# libs/langchain_v1, even though the workflow may only affect documentation.
dirs_to_run["extended-test"].update(LANGCHAIN_DIRS)
if file.startswith("libs/core"):
dirs_to_run["codspeed"].add("libs/core")
if any(file.startswith(dir_) for dir_ in LANGCHAIN_DIRS):
# add that dir and all dirs after in LANGCHAIN_DIRS
# for extended testing
found = False
for dir_ in LANGCHAIN_DIRS:
if dir_ == "libs/core" and IGNORE_CORE_DEPENDENTS:
dirs_to_run["extended-test"].add(dir_)
continue
if file.startswith(dir_):
found = True
if found:
dirs_to_run["extended-test"].add(dir_)
# Check if file is in one of the main LangChain directories
matched_langchain_dir = None
for dir_ in LANGCHAIN_DIRS:
if file.startswith(dir_):
matched_langchain_dir = dir_
break
if matched_langchain_dir:
# Add the changed directory and all packages that depend on it
affected_packages = get_affected_packages(matched_langchain_dir)
dirs_to_run["extended-test"].update(affected_packages)
elif file.startswith("libs/standard-tests"):
# TODO: update to include all packages that rely on standard-tests (all partner packages)
# Note: won't run on external repo partners

View File

@@ -98,7 +98,7 @@ def _check_python_version_from_requirement(
return True
else:
marker_str = str(requirement.marker)
if "python_version" or "python_full_version" in marker_str:
if "python_version" in marker_str or "python_full_version" in marker_str:
python_version_str = "".join(
char
for char in marker_str

View File

@@ -35,7 +35,7 @@ jobs:
timeout-minutes: 20
name: "Python ${{ inputs.python-version }}"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: "🐍 Set up Python ${{ inputs.python-version }} + UV"
uses: "./.github/actions/uv_setup"

View File

@@ -38,7 +38,7 @@ jobs:
timeout-minutes: 20
steps:
- name: "📋 Checkout Code"
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: "🐍 Set up Python ${{ inputs.python-version }} + UV"
uses: "./.github/actions/uv_setup"

View File

@@ -54,7 +54,7 @@ jobs:
version: ${{ steps.check-version.outputs.version }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python + uv
uses: "./.github/actions/uv_setup"
@@ -105,7 +105,7 @@ jobs:
outputs:
release-body: ${{ steps.generate-release-body.outputs.release-body }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
repository: langchain-ai/langchain
path: langchain
@@ -206,7 +206,7 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/download-artifact@v6
with:
@@ -237,7 +237,7 @@ jobs:
contents: read
timeout-minutes: 20
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
# We explicitly *don't* set up caching here. This ensures our tests are
# maximally sensitive to catching breakage.
@@ -396,7 +396,7 @@ jobs:
contents: read
strategy:
matrix:
partner: [openai]
partner: [anthropic]
fail-fast: false # Continue testing other partners if one fails
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
@@ -412,7 +412,7 @@ jobs:
AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_NAME }}
LANGCHAIN_TESTS_USER_AGENT: ${{ secrets.LANGCHAIN_TESTS_USER_AGENT }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
# We implement this conditional as Github Actions does not have good support
# for conditionally needing steps. https://github.com/actions/runner/issues/491
@@ -492,7 +492,7 @@ jobs:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python + uv
uses: "./.github/actions/uv_setup"
@@ -532,7 +532,7 @@ jobs:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python + uv
uses: "./.github/actions/uv_setup"

View File

@@ -33,7 +33,7 @@ jobs:
name: "Python ${{ inputs.python-version }}"
steps:
- name: "📋 Checkout Code"
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: "🐍 Set up Python ${{ inputs.python-version }} + UV"
uses: "./.github/actions/uv_setup"

View File

@@ -36,7 +36,7 @@ jobs:
name: "Pydantic ~=${{ inputs.pydantic-version }}"
steps:
- name: "📋 Checkout Code"
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: "🐍 Set up Python ${{ inputs.python-version }} + UV"
uses: "./.github/actions/uv_setup"

View File

@@ -0,0 +1,107 @@
name: Auto Label Issues by Package
on:
issues:
types: [opened, edited]
jobs:
label-by-package:
permissions:
issues: write
runs-on: ubuntu-latest
steps:
- name: Sync package labels
uses: actions/github-script@v8
with:
script: |
const body = context.payload.issue.body || "";
// Extract text under "### Package"
const match = body.match(/### Package\s+([\s\S]*?)\n###/i);
if (!match) return;
const packageSection = match[1].trim();
// Mapping table for package names to labels
const mapping = {
"langchain": "langchain",
"langchain-openai": "openai",
"langchain-anthropic": "anthropic",
"langchain-classic": "langchain-classic",
"langchain-core": "core",
"langchain-cli": "cli",
"langchain-model-profiles": "model-profiles",
"langchain-tests": "standard-tests",
"langchain-text-splitters": "text-splitters",
"langchain-chroma": "chroma",
"langchain-deepseek": "deepseek",
"langchain-exa": "exa",
"langchain-fireworks": "fireworks",
"langchain-groq": "groq",
"langchain-huggingface": "huggingface",
"langchain-mistralai": "mistralai",
"langchain-nomic": "nomic",
"langchain-ollama": "ollama",
"langchain-perplexity": "perplexity",
"langchain-prompty": "prompty",
"langchain-qdrant": "qdrant",
"langchain-xai": "xai",
};
// All possible package labels we manage
const allPackageLabels = Object.values(mapping);
const selectedLabels = [];
// Check if this is checkbox format (multiple selection)
const checkboxMatches = packageSection.match(/- \[x\]\s+([^\n\r]+)/gi);
if (checkboxMatches) {
// Handle checkbox format
for (const match of checkboxMatches) {
const packageName = match.replace(/- \[x\]\s+/i, '').trim();
const label = mapping[packageName];
if (label && !selectedLabels.includes(label)) {
selectedLabels.push(label);
}
}
} else {
// Handle dropdown format (single selection)
const label = mapping[packageSection];
if (label) {
selectedLabels.push(label);
}
}
// Get current issue labels
const issue = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const currentLabels = issue.data.labels.map(label => label.name);
const currentPackageLabels = currentLabels.filter(label => allPackageLabels.includes(label));
// Determine labels to add and remove
const labelsToAdd = selectedLabels.filter(label => !currentPackageLabels.includes(label));
const labelsToRemove = currentPackageLabels.filter(label => !selectedLabels.includes(label));
// Add new labels
if (labelsToAdd.length > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: labelsToAdd
});
}
// Remove old labels
for (const label of labelsToRemove) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: label
});
}

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: "✅ Verify pyproject.toml & version.py Match"
run: |

View File

@@ -47,7 +47,7 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'ci-ignore') }}
steps:
- name: "📋 Checkout Code"
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: "🐍 Setup Python 3.11"
uses: actions/setup-python@v6
with:
@@ -141,7 +141,7 @@ jobs:
run:
working-directory: ${{ matrix.job-configs.working-directory }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: "🐍 Set up Python ${{ matrix.job-configs.python-version }} + UV"
uses: "./.github/actions/uv_setup"
@@ -182,7 +182,7 @@ jobs:
job-configs: ${{ fromJson(needs.build.outputs.codspeed) }}
fail-fast: false
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: "📦 Install UV Package Manager"
uses: astral-sh/setup-uv@v7

View File

@@ -71,14 +71,14 @@ jobs:
working-directory: ${{ fromJSON(needs.compute-matrix.outputs.matrix).working-directory }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
path: langchain
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
repository: langchain-ai/langchain-google
path: langchain-google
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
repository: langchain-ai/langchain-aws
path: langchain-aws

View File

@@ -26,11 +26,13 @@
# * revert — reverts a previous commit
# * release — prepare a new release
#
# Allowed Scopes (optional):
# core, cli, langchain, langchain_v1, langchain-classic, standard-tests,
# text-splitters, docs, anthropic, chroma, deepseek, exa, fireworks, groq,
# huggingface, mistralai, nomic, ollama, openai, perplexity, prompty, qdrant,
# xai, infra, deps
# Allowed Scope(s) (optional):
# core, cli, langchain, langchain_v1, 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.
#
# Rules:
# 1. The 'Type' must start with a lowercase letter.
@@ -79,7 +81,6 @@ jobs:
core
cli
langchain
langchain_v1
langchain-classic
model-profiles
standard-tests
@@ -101,6 +102,7 @@ jobs:
qdrant
xai
infra
deps
requireScope: false
disallowScopes: |
release

View File

@@ -23,12 +23,12 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: v0.3
path: langchain
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
repository: langchain-ai/langchain-api-docs-html
path: langchain-api-docs-html

3
.gitignore vendored
View File

@@ -163,3 +163,6 @@ node_modules
prof
virtualenv/
scratch/
.langgraph_api/

405
AGENTS.md
View File

@@ -1,255 +1,58 @@
# Global Development Guidelines for LangChain Projects
# Global development guidelines for the LangChain monorepo
## Core Development Principles
This document provides context to understand the LangChain Python project and assist with development.
### 1. Maintain Stable Public Interfaces ⚠️ CRITICAL
## Project architecture and context
**Always attempt to preserve function signatures, argument positions, and names for exported/public methods.**
### Monorepo structure
**Bad - Breaking Change:**
This is a Python monorepo with multiple independently versioned packages that use `uv`.
```python
def get_user(id, verbose=False): # Changed from `user_id`
pass
```txt
langchain/
├── libs/
│ ├── core/ # `langchain-core` primitives and base abstractions
│ ├── langchain/ # `langchain-classic` (legacy, no new features)
│ ├── langchain_v1/ # Actively maintained `langchain` package
│ ├── partners/ # Third-party integrations
│ │ ├── openai/ # OpenAI models and embeddings
│ │ ├── anthropic/ # Anthropic (Claude) integration
│ │ ├── ollama/ # Local model support
│ │ └── ... (other integrations maintained by the LangChain team)
│ ├── text-splitters/ # Document chunking utilities
│ ├── standard-tests/ # Shared test suite for integrations
│ ├── model-profiles/ # Model configuration profiles
│ └── cli/ # Command-line interface tools
├── .github/ # CI/CD workflows and templates
├── .vscode/ # VSCode IDE standard settings and recommended extensions
└── README.md # Information about LangChain
```
**Good - Stable Interface:**
- **Core layer** (`langchain-core`): Base abstractions, interfaces, and protocols. Users should not need to know about this layer directly.
- **Implementation layer** (`langchain`): Concrete implementations and high-level public utilities
- **Integration layer** (`partners/`): Third-party service integrations. Note that this monorepo is not exhaustive of all LangChain integrations; some are maintained in separate repos, such as `langchain-ai/langchain-google` and `langchain-ai/langchain-aws`. Usually these repos are cloned at the same level as this monorepo, so if needed, you can refer to their code directly by navigating to `../langchain-google/` from this monorepo.
- **Testing layer** (`standard-tests/`): Standardized integration tests for partner integrations
```python
def get_user(user_id: str, verbose: bool = False) -> User:
"""Retrieve user by ID with optional verbose output."""
pass
```
### Development tools & commands**
**Before making ANY changes to public APIs:**
- `uv` Fast Python package installer and resolver (replaces pip/poetry)
- `make` Task runner for common development commands. Feel free to look at the `Makefile` for available commands and usage patterns.
- `ruff` Fast Python linter and formatter
- `mypy` Static type checking
- `pytest` Testing framework
- 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 warnings (using MkDocs Material admonitions, like `!!! warning`)
This monorepo uses `uv` for dependency management. Local development uses editable installs: `[tool.uv.sources]`
🧠 *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 section 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 (`'low'`, `'normal'`, `'high'`).
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
- If a default is present, DO NOT repeat it in the docstring unless there is post-processing or it is set conditionally.
- Focus on "why" rather than "what" in descriptions
- Document all parameters, return values, and exceptions
- Keep descriptions concise but clear
- Ensure American English spelling (e.g., "behavior", not "behaviour")
📌 *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."""
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
Each package in `libs/` has its own `pyproject.toml` and `uv.lock`.
```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
@@ -261,66 +64,118 @@ make format
uv run --group lint mypy .
```
### Dependency Management Patterns
#### Key config files
**Local Development Dependencies:**
- pyproject.toml: Main workspace configuration with dependency groups
- uv.lock: Locked dependencies for reproducible builds
- Makefile: Development tasks
```toml
[tool.uv.sources]
langchain-core = { path = "../core", editable = true }
langchain-tests = { path = "../standard-tests", editable = true }
```
#### Commit standards
**For tools, use the `@tool` decorator from `langchain_core.tools`:**
Suggest PR titles that follow Conventional Commits format. Refer to .github/workflows/pr_lint for allowed types and scopes.
```python
from langchain_core.tools import tool
#### Pull request guidelines
@tool
def search_database(query: str) -> str:
"""Search the database for relevant information.
- Always add a disclaimer to the PR description mentioning how AI agents are involved with the contribution.
- Describe the "why" of the changes, why the proposed solution is the right one. Limit prose.
- Highlight areas of the proposed changes that require careful review.
## Core development principles
### Maintain stable public interfaces
CRITICAL: Always attempt to preserve function signatures, argument positions, and names for exported/public methods. Do not make breaking changes.
**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 warnings (using MkDocs Material admonitions, like `!!! warning`)
Ask: "Would this change break someone's code if they used it last week?"
### Code quality standards
All Python code MUST include type hints and return types.
```python title="Example"
def filter_unknown_users(users: list[str], known_users: set[str]) -> list[str]:
"""Single line description of the function.
Any additional context about the function can go here.
Args:
query: The search query string.
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.
"""
# Implementation here
return results
```
## Commit Standards
- Use descriptive, self-explanatory variable names.
- Follow existing patterns in the codebase you're modifying
- Attempt to break up complex functions (>20 lines) into smaller, focused functions where it makes sense
**Use Conventional Commits format for PR titles:**
### Testing requirements
- `feat(core): add multi-tenant support`
- `fix(cli): resolve flag parsing error`
- `docs: update API usage examples`
- `docs(openai): update API usage examples`
Every new feature or bugfix MUST be covered by unit tests.
## Framework-Specific Guidelines
- Unit tests: `tests/unit_tests/` (no network calls allowed)
- Integration tests: `tests/integration_tests/` (network calls permitted)
- We use `pytest` as the testing framework; if in doubt, check other existing tests for examples.
- The testing file structure should mirror the source code structure.
- Follow the existing patterns in `langchain-core` for base abstractions
- Use `langchain_core.callbacks` for execution tracking
- Implement proper streaming support where applicable
- Avoid deprecated components like legacy `LLMChain`
**Checklist:**
### Partner Integrations
- [ ] 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)
- [ ] Does the test suite fail if your new logic is broken?
- Follow the established patterns in existing partner libraries
- Implement standard interfaces (`BaseChatModel`, `BaseEmbeddings`, etc.)
- Include comprehensive integration tests
- Document API key requirements and authentication
### Security and risk assessment
---
- 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)
## Quick Reference Checklist
### Documentation standards
Before submitting code changes:
Use Google-style docstrings with Args section for all public functions.
- [ ] **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
```python title="Example"
def send_email(to: str, msg: str, *, priority: str = "normal") -> bool:
"""Send an email to a recipient with specified priority.
Any additional context about the function can go here.
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.
"""
```
- Types go in function signatures, NOT in docstrings
- If a default is present, DO NOT repeat it in the docstring unless there is post-processing or it is set conditionally.
- Focus on "why" rather than "what" in descriptions
- Document all parameters, return values, and exceptions
- Keep descriptions concise but clear
- Ensure American English spelling (e.g., "behavior", not "behaviour")
## Additional resources
- **Documentation:** https://docs.langchain.com/oss/python/langchain/overview and source at https://github.com/langchain-ai/docs or `../docs/`. Prefer the local install and use file search tools for best results. If needed, use the docs MCP server as defined in `.mcp.json` for programmatic access.
- **Contributing Guide:** [`.github/CONTRIBUTING.md`](https://docs.langchain.com/oss/python/contributing/overview)

405
CLAUDE.md
View File

@@ -1,255 +1,58 @@
# Global Development Guidelines for LangChain Projects
# Global development guidelines for the LangChain monorepo
## Core Development Principles
This document provides context to understand the LangChain Python project and assist with development.
### 1. Maintain Stable Public Interfaces ⚠️ CRITICAL
## Project architecture and context
**Always attempt to preserve function signatures, argument positions, and names for exported/public methods.**
### Monorepo structure
**Bad - Breaking Change:**
This is a Python monorepo with multiple independently versioned packages that use `uv`.
```python
def get_user(id, verbose=False): # Changed from `user_id`
pass
```txt
langchain/
├── libs/
│ ├── core/ # `langchain-core` primitives and base abstractions
│ ├── langchain/ # `langchain-classic` (legacy, no new features)
│ ├── langchain_v1/ # Actively maintained `langchain` package
│ ├── partners/ # Third-party integrations
│ │ ├── openai/ # OpenAI models and embeddings
│ │ ├── anthropic/ # Anthropic (Claude) integration
│ │ ├── ollama/ # Local model support
│ │ └── ... (other integrations maintained by the LangChain team)
│ ├── text-splitters/ # Document chunking utilities
│ ├── standard-tests/ # Shared test suite for integrations
│ ├── model-profiles/ # Model configuration profiles
│ └── cli/ # Command-line interface tools
├── .github/ # CI/CD workflows and templates
├── .vscode/ # VSCode IDE standard settings and recommended extensions
└── README.md # Information about LangChain
```
**Good - Stable Interface:**
- **Core layer** (`langchain-core`): Base abstractions, interfaces, and protocols. Users should not need to know about this layer directly.
- **Implementation layer** (`langchain`): Concrete implementations and high-level public utilities
- **Integration layer** (`partners/`): Third-party service integrations. Note that this monorepo is not exhaustive of all LangChain integrations; some are maintained in separate repos, such as `langchain-ai/langchain-google` and `langchain-ai/langchain-aws`. Usually these repos are cloned at the same level as this monorepo, so if needed, you can refer to their code directly by navigating to `../langchain-google/` from this monorepo.
- **Testing layer** (`standard-tests/`): Standardized integration tests for partner integrations
```python
def get_user(user_id: str, verbose: bool = False) -> User:
"""Retrieve user by ID with optional verbose output."""
pass
```
### Development tools & commands**
**Before making ANY changes to public APIs:**
- `uv` Fast Python package installer and resolver (replaces pip/poetry)
- `make` Task runner for common development commands. Feel free to look at the `Makefile` for available commands and usage patterns.
- `ruff` Fast Python linter and formatter
- `mypy` Static type checking
- `pytest` Testing framework
- 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 warnings (using MkDocs Material admonitions, like `!!! warning`)
This monorepo uses `uv` for dependency management. Local development uses editable installs: `[tool.uv.sources]`
🧠 *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 section 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 (`'low'`, `'normal'`, `'high'`).
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
- If a default is present, DO NOT repeat it in the docstring unless there is post-processing or it is set conditionally.
- Focus on "why" rather than "what" in descriptions
- Document all parameters, return values, and exceptions
- Keep descriptions concise but clear
- Ensure American English spelling (e.g., "behavior", not "behaviour")
📌 *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."""
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
Each package in `libs/` has its own `pyproject.toml` and `uv.lock`.
```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
@@ -261,66 +64,118 @@ make format
uv run --group lint mypy .
```
### Dependency Management Patterns
#### Key config files
**Local Development Dependencies:**
- pyproject.toml: Main workspace configuration with dependency groups
- uv.lock: Locked dependencies for reproducible builds
- Makefile: Development tasks
```toml
[tool.uv.sources]
langchain-core = { path = "../core", editable = true }
langchain-tests = { path = "../standard-tests", editable = true }
```
#### Commit standards
**For tools, use the `@tool` decorator from `langchain_core.tools`:**
Suggest PR titles that follow Conventional Commits format. Refer to .github/workflows/pr_lint for allowed types and scopes.
```python
from langchain_core.tools import tool
#### Pull request guidelines
@tool
def search_database(query: str) -> str:
"""Search the database for relevant information.
- Always add a disclaimer to the PR description mentioning how AI agents are involved with the contribution.
- Describe the "why" of the changes, why the proposed solution is the right one. Limit prose.
- Highlight areas of the proposed changes that require careful review.
## Core development principles
### Maintain stable public interfaces
CRITICAL: Always attempt to preserve function signatures, argument positions, and names for exported/public methods. Do not make breaking changes.
**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 warnings (using MkDocs Material admonitions, like `!!! warning`)
Ask: "Would this change break someone's code if they used it last week?"
### Code quality standards
All Python code MUST include type hints and return types.
```python title="Example"
def filter_unknown_users(users: list[str], known_users: set[str]) -> list[str]:
"""Single line description of the function.
Any additional context about the function can go here.
Args:
query: The search query string.
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.
"""
# Implementation here
return results
```
## Commit Standards
- Use descriptive, self-explanatory variable names.
- Follow existing patterns in the codebase you're modifying
- Attempt to break up complex functions (>20 lines) into smaller, focused functions where it makes sense
**Use Conventional Commits format for PR titles:**
### Testing requirements
- `feat(core): add multi-tenant support`
- `fix(cli): resolve flag parsing error`
- `docs: update API usage examples`
- `docs(openai): update API usage examples`
Every new feature or bugfix MUST be covered by unit tests.
## Framework-Specific Guidelines
- Unit tests: `tests/unit_tests/` (no network calls allowed)
- Integration tests: `tests/integration_tests/` (network calls permitted)
- We use `pytest` as the testing framework; if in doubt, check other existing tests for examples.
- The testing file structure should mirror the source code structure.
- Follow the existing patterns in `langchain-core` for base abstractions
- Use `langchain_core.callbacks` for execution tracking
- Implement proper streaming support where applicable
- Avoid deprecated components like legacy `LLMChain`
**Checklist:**
### Partner Integrations
- [ ] 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)
- [ ] Does the test suite fail if your new logic is broken?
- Follow the established patterns in existing partner libraries
- Implement standard interfaces (`BaseChatModel`, `BaseEmbeddings`, etc.)
- Include comprehensive integration tests
- Document API key requirements and authentication
### Security and risk assessment
---
- 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)
## Quick Reference Checklist
### Documentation standards
Before submitting code changes:
Use Google-style docstrings with Args section for all public functions.
- [ ] **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
```python title="Example"
def send_email(to: str, msg: str, *, priority: str = "normal") -> bool:
"""Send an email to a recipient with specified priority.
Any additional context about the function can go here.
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.
"""
```
- Types go in function signatures, NOT in docstrings
- If a default is present, DO NOT repeat it in the docstring unless there is post-processing or it is set conditionally.
- Focus on "why" rather than "what" in descriptions
- Document all parameters, return values, and exceptions
- Keep descriptions concise but clear
- Ensure American English spelling (e.g., "behavior", not "behaviour")
## Additional resources
- **Documentation:** https://docs.langchain.com/oss/python/langchain/overview and source at https://github.com/langchain-ai/docs or `../docs/`. Prefer the local install and use file search tools for best results. If needed, use the docs MCP server as defined in `.mcp.json` for programmatic access.
- **Contributing Guide:** [`.github/CONTRIBUTING.md`](https://docs.langchain.com/oss/python/contributing/overview)

View File

@@ -1,9 +0,0 @@
# Migrating
Please see the following guides for migrating LangChain code:
* Migrate to [LangChain v1.0](https://docs.langchain.com/oss/python/migrate/langchain-v1)
* Migrate to [LangChain v0.3](https://python.langchain.com/docs/versions/v0_3/)
* Migrate to [LangChain v0.2](https://python.langchain.com/docs/versions/v0_2/)
* Migrating from [LangChain 0.0.x Chains](https://python.langchain.com/docs/versions/migrating_chains/)
* Upgrade to [LangGraph Memory](https://python.langchain.com/docs/versions/migrating_memory/)

View File

@@ -1,40 +1,28 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: light)" srcset=".github/images/logo-dark.svg">
<source media="(prefers-color-scheme: dark)" srcset=".github/images/logo-light.svg">
<img alt="LangChain Logo" src=".github/images/logo-dark.svg" width="80%">
</picture>
</p>
<div align="center">
<a href="https://www.langchain.com/">
<picture>
<source media="(prefers-color-scheme: light)" srcset=".github/images/logo-dark.svg">
<source media="(prefers-color-scheme: dark)" srcset=".github/images/logo-light.svg">
<img alt="LangChain Logo" src=".github/images/logo-dark.svg" width="80%">
</picture>
</a>
</div>
<p align="center">
The platform for reliable agents.
</p>
<div align="center">
<h3>The platform for reliable agents.</h3>
</div>
<p align="center">
<a href="https://opensource.org/licenses/MIT" target="_blank">
<img src="https://img.shields.io/pypi/l/langchain" alt="PyPI - License">
</a>
<a href="https://pypistats.org/packages/langchain" target="_blank">
<img src="https://img.shields.io/pepy/dt/langchain" alt="PyPI - Downloads">
</a>
<a href="https://pypi.org/project/langchain/#history" target="_blank">
<img src="https://img.shields.io/pypi/v/langchain?label=%20" alt="Version">
</a>
<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>
</p>
<div align="center">
<a href="https://opensource.org/licenses/MIT" target="_blank"><img src="https://img.shields.io/pypi/l/langchain" alt="PyPI - License"></a>
<a href="https://pypistats.org/packages/langchain" target="_blank"><img src="https://img.shields.io/pepy/dt/langchain" alt="PyPI - Downloads"></a>
<a href="https://pypi.org/project/langchain/#history" target="_blank"><img src="https://img.shields.io/pypi/v/langchain?label=%20" alt="Version"></a>
<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>
</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.
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.
```bash
pip install langchain
@@ -44,7 +32,10 @@ If you're looking for more advanced customization or agent orchestration, check
---
**Documentation**: To learn more about LangChain, check out [the docs](https://docs.langchain.com/oss/python/langchain/overview).
**Documentation**:
- [docs.langchain.com](https://docs.langchain.com/oss/python/langchain/overview) Comprehensive documentation, including conceptual overviews and guides
- [reference.langchain.com/python](https://reference.langchain.com/python) API reference docs for LangChain packages
**Discussions**: Visit the [LangChain Forum](https://forum.langchain.com) to connect with the community and share all of your technical questions, ideas, and feedback.
@@ -57,8 +48,12 @@ LangChain helps developers build applications powered by LLMs through a standard
Use LangChain for:
- **Real-time data augmentation**. Easily connect LLMs to diverse data sources and external/internal systems, drawing from LangChains vast library of integrations with model providers, tools, vector stores, retrievers, and more.
- **Model interoperability**. Swap models in and out as your engineering team experiments to find the best choice for your applications needs. As the industry frontier evolves, adapt quickly LangChains abstractions keep you moving without losing momentum.
- **Real-time data augmentation**. Easily connect LLMs to diverse data sources and external/internal systems, drawing from LangChain's vast library of integrations with model providers, tools, vector stores, retrievers, and more.
- **Model interoperability**. Swap models in and out as your engineering team experiments to find the best choice for your application's needs. As the industry frontier evolves, adapt quickly LangChain's abstractions keep you moving without losing momentum.
- **Rapid prototyping**. Quickly build and iterate on LLM applications with LangChain's modular, component-based architecture. Test different approaches and workflows without rebuilding from scratch, accelerating your development cycle.
- **Production-ready features**. Deploy reliable applications with built-in support for monitoring, evaluation, and debugging through integrations like LangSmith. Scale with confidence using battle-tested patterns and best practices.
- **Vibrant community and ecosystem**. Leverage a rich ecosystem of integrations, templates, and community-contributed components. Benefit from continuous improvements and stay up-to-date with the latest AI developments through an active open-source community.
- **Flexible abstraction layers**. Work at the level of abstraction that suits your needs - from high-level chains for quick starts to low-level components for fine-grained control. LangChain grows with your application's complexity.
## LangChain ecosystem
@@ -66,13 +61,14 @@ While the LangChain framework can be used standalone, it also integrates seamles
To improve your LLM application development, pair LangChain with:
- [LangGraph](https://docs.langchain.com/oss/python/langgraph/overview) - Build agents that can reliably handle complex tasks with LangGraph, our low-level agent orchestration framework. LangGraph offers customizable architecture, long-term memory, and human-in-the-loop workflows and is trusted in production by companies like LinkedIn, Uber, Klarna, and GitLab.
- [LangSmith](https://www.langchain.com/langsmith) - Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
- [LangSmith Deployment](https://docs.langchain.com/langsmith/deployments) - Deploy and scale agents effortlessly with a purpose-built deployment platform for long-running, stateful workflows. Discover, reuse, configure, and share agents across teams — and iterate quickly with visual prototyping in [LangSmith Studio](https://docs.langchain.com/langsmith/studio).
- [LangGraph](https://docs.langchain.com/oss/python/langgraph/overview) Build agents that can reliably handle complex tasks with LangGraph, our low-level agent orchestration framework. LangGraph offers customizable architecture, long-term memory, and human-in-the-loop workflows and is trusted in production by companies like LinkedIn, Uber, Klarna, and GitLab.
- [Integrations](https://docs.langchain.com/oss/python/integrations/providers/overview) List of LangChain integrations, including chat & embedding models, tools & toolkits, and more
- [LangSmith](https://www.langchain.com/langsmith) Helpful for agent evals and observability. Debug poor-performing LLM app runs, evaluate agent trajectories, gain visibility in production, and improve performance over time.
- [LangSmith Deployment](https://docs.langchain.com/langsmith/deployments) Deploy and scale agents effortlessly with a purpose-built deployment platform for long-running, stateful workflows. Discover, reuse, configure, and share agents across teams and iterate quickly with visual prototyping in [LangSmith Studio](https://docs.langchain.com/langsmith/studio).
- [Deep Agents](https://github.com/langchain-ai/deepagents) *(new!)* Build agents that can plan, use subagents, and leverage file systems for complex tasks
## Additional resources
- [API Reference](https://reference.langchain.com/python): Detailed reference on navigating base packages and integrations for LangChain.
- [Integrations](https://docs.langchain.com/oss/python/integrations/providers/overview): List of LangChain integrations, including chat & embedding models, tools & toolkits, and more
- [Contributing Guide](https://docs.langchain.com/oss/python/contributing/overview): Learn how to contribute to LangChain 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.
- [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.

View File

@@ -55,10 +55,10 @@ 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
* **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
* `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

View File

@@ -295,7 +295,7 @@
"source": [
"## TODO: Any functionality specific to this vector store\n",
"\n",
"E.g. creating a persisten database to save to your disk, etc."
"E.g. creating a persistent database to save to your disk, etc."
]
},
{

View File

@@ -6,9 +6,8 @@ import hashlib
import logging
import re
import shutil
from collections.abc import Sequence
from pathlib import Path
from typing import Any, TypedDict
from typing import TYPE_CHECKING, Any, TypedDict
from git import Repo
@@ -18,6 +17,9 @@ from langchain_cli.constants import (
DEFAULT_GIT_SUBDIRECTORY,
)
if TYPE_CHECKING:
from collections.abc import Sequence
logger = logging.getLogger(__name__)

View File

@@ -1,9 +1,11 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
from .file import File
from .folder import Folder
if TYPE_CHECKING:
from .file import File
from .folder import Folder
@dataclass

View File

@@ -1,9 +1,12 @@
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
from .file import File
if TYPE_CHECKING:
from pathlib import Path
class Folder:
def __init__(self, name: str, *files: Folder | File) -> None:

View File

@@ -34,7 +34,7 @@ The LangChain ecosystem is built on top of `langchain-core`. Some of the benefit
## 📖 Documentation
For full documentation, see the [API reference](https://reference.langchain.com/python/langchain_core/).
For full documentation, see the [API reference](https://reference.langchain.com/python/langchain_core/). For conceptual guides, tutorials, and examples on using LangChain, see the [LangChain Docs](https://docs.langchain.com/oss/python/langchain/overview).
## 📕 Releases & Versioning

View File

@@ -52,31 +52,33 @@ class AgentAction(Serializable):
"""The input to pass in to the Tool."""
log: str
"""Additional information to log about the action.
This log can be used in a few ways. First, it can be used to audit
what exactly the LLM predicted to lead to this (tool, tool_input).
Second, it can be used in future iterations to show the LLMs prior
thoughts. This is useful when (tool, tool_input) does not contain
full information about the LLM prediction (for example, any `thought`
before the tool/tool_input)."""
This log can be used in a few ways. First, it can be used to audit what exactly the
LLM predicted to lead to this `(tool, tool_input)`.
Second, it can be used in future iterations to show the LLMs prior thoughts. This is
useful when `(tool, tool_input)` does not contain full information about the LLM
prediction (for example, any `thought` before the tool/tool_input).
"""
type: Literal["AgentAction"] = "AgentAction"
# Override init to support instantiation by position for backward compat.
def __init__(self, tool: str, tool_input: str | dict, log: str, **kwargs: Any):
"""Create an AgentAction.
"""Create an `AgentAction`.
Args:
tool: The name of the tool to execute.
tool_input: The input to pass in to the Tool.
tool_input: The input to pass in to the `Tool`.
log: Additional information to log about the action.
"""
super().__init__(tool=tool, tool_input=tool_input, log=log, **kwargs)
@classmethod
def is_lc_serializable(cls) -> bool:
"""AgentAction is serializable.
"""`AgentAction` is serializable.
Returns:
True
`True`
"""
return True
@@ -98,19 +100,23 @@ class AgentAction(Serializable):
class AgentActionMessageLog(AgentAction):
"""Representation of an action to be executed by an agent.
This is similar to AgentAction, but includes a message log consisting of
chat messages. This is useful when working with ChatModels, and is used
to reconstruct conversation history from the agent's perspective.
This is similar to `AgentAction`, but includes a message log consisting of
chat messages.
This is useful when working with `ChatModels`, and is used to reconstruct
conversation history from the agent's perspective.
"""
message_log: Sequence[BaseMessage]
"""Similar to log, this can be used to pass along extra
information about what exact messages were predicted by the LLM
before parsing out the (tool, tool_input). This is again useful
if (tool, tool_input) cannot be used to fully recreate the LLM
prediction, and you need that LLM prediction (for future agent iteration).
"""Similar to log, this can be used to pass along extra information about what exact
messages were predicted by the LLM before parsing out the `(tool, tool_input)`.
This is again useful if `(tool, tool_input)` cannot be used to fully recreate the
LLM prediction, and you need that LLM prediction (for future agent iteration).
Compared to `log`, this is useful when the underlying LLM is a
chat model (and therefore returns messages rather than a string)."""
chat model (and therefore returns messages rather than a string).
"""
# Ignoring type because we're overriding the type from AgentAction.
# And this is the correct thing to do in this case.
# The type literal is used for serialization purposes.
@@ -132,19 +138,22 @@ class AgentStep(Serializable):
class AgentFinish(Serializable):
"""Final return value of an ActionAgent.
"""Final return value of an `ActionAgent`.
Agents return an AgentFinish when they have reached a stopping condition.
Agents return an `AgentFinish` when they have reached a stopping condition.
"""
return_values: dict
"""Dictionary of return values."""
log: str
"""Additional information to log about the return value.
This is used to pass along the full LLM prediction, not just the parsed out
return value. For example, if the full LLM prediction was
`Final Answer: 2` you may want to just return `2` as a return value, but pass
along the full string as a `log` (for debugging or observability purposes).
return value.
For example, if the full LLM prediction was `Final Answer: 2` you may want to just
return `2` as a return value, but pass along the full string as a `log` (for
debugging or observability purposes).
"""
type: Literal["AgentFinish"] = "AgentFinish"
@@ -154,7 +163,7 @@ class AgentFinish(Serializable):
@classmethod
def is_lc_serializable(cls) -> bool:
"""Return True as this class is serializable."""
"""Return `True` as this class is serializable."""
return True
@classmethod
@@ -202,7 +211,7 @@ def _convert_agent_observation_to_messages(
observation: Observation to convert to a message.
Returns:
AIMessage that corresponds to the original tool invocation.
`AIMessage` that corresponds to the original tool invocation.
"""
if isinstance(agent_action, AgentActionMessageLog):
return [_create_function_message(agent_action, observation)]
@@ -225,7 +234,7 @@ def _create_function_message(
observation: the result of the tool invocation.
Returns:
FunctionMessage that corresponds to the original tool invocation.
`FunctionMessage` that corresponds to the original tool invocation.
"""
if not isinstance(observation, str):
try:

View File

@@ -5,13 +5,12 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
from typing_extensions import Self
if TYPE_CHECKING:
from collections.abc import Sequence
from uuid import UUID
from tenacity import RetryCallState
from typing_extensions import Self
from langchain_core.agents import AgentAction, AgentFinish
from langchain_core.documents import Document

View File

@@ -39,7 +39,6 @@ from langchain_core.tracers.context import (
tracing_v2_callback_var,
)
from langchain_core.tracers.langchain import LangChainTracer
from langchain_core.tracers.schemas import Run
from langchain_core.tracers.stdout import ConsoleCallbackHandler
from langchain_core.utils.env import env_var_is_set
@@ -52,6 +51,7 @@ if TYPE_CHECKING:
from langchain_core.documents import Document
from langchain_core.outputs import ChatGenerationChunk, GenerationChunk, LLMResult
from langchain_core.runnables.config import RunnableConfig
from langchain_core.tracers.schemas import Run
logger = logging.getLogger(__name__)
@@ -229,7 +229,24 @@ def shielded(func: Func) -> Func:
@functools.wraps(func)
async def wrapped(*args: Any, **kwargs: Any) -> Any:
return await asyncio.shield(func(*args, **kwargs))
# Capture the current context to preserve context variables
ctx = copy_context()
# Create the coroutine
coro = func(*args, **kwargs)
# For Python 3.11+, create task with explicit context
# For older versions, fallback to original behavior
try:
# Create a task with the captured context to preserve context variables
task = asyncio.create_task(coro, context=ctx) # type: ignore[call-arg, unused-ignore]
# `call-arg` used to not fail 3.9 or 3.10 tests
return await asyncio.shield(task)
except TypeError:
# Python < 3.11 fallback - create task normally then shield
# This won't preserve context perfectly but is better than nothing
task = asyncio.create_task(coro)
return await asyncio.shield(task)
return cast("Func", wrapped)

View File

@@ -43,7 +43,7 @@ class UsageMetadataCallbackHandler(BaseCallbackHandler):
'input_token_details': {'cache_read': 0, 'cache_creation': 0}}}
```
!!! version-added "Added in version 0.3.49"
!!! version-added "Added in `langchain-core` 0.3.49"
"""
@@ -134,7 +134,7 @@ def get_usage_metadata_callback(
}
```
!!! version-added "Added in version 0.3.49"
!!! version-added "Added in `langchain-core` 0.3.49"
"""
usage_metadata_callback_var: ContextVar[UsageMetadataCallbackHandler | None] = (

View File

@@ -114,11 +114,11 @@ class Blob(BaseMedia):
data: bytes | str | None = None
"""Raw data associated with the `Blob`."""
mimetype: str | None = None
"""MimeType not to be confused with a file extension."""
"""MIME type, not to be confused with a file extension."""
encoding: str = "utf-8"
"""Encoding to use if decoding the bytes into a string.
Use `utf-8` as default encoding, if decoding to string.
Uses `utf-8` as default encoding if decoding to string.
"""
path: PathLike | None = None
"""Location where the original content was found."""
@@ -134,7 +134,7 @@ class Blob(BaseMedia):
If a path is associated with the `Blob`, it will default to the path location.
Unless explicitly set via a metadata field called `"source"`, in which
Unless explicitly set via a metadata field called `'source'`, in which
case that value will be used instead.
"""
if self.metadata and "source" in self.metadata:
@@ -309,7 +309,7 @@ class Document(BaseMedia):
@classmethod
def is_lc_serializable(cls) -> bool:
"""Return True as this class is serializable."""
"""Return `True` as this class is serializable."""
return True
@classmethod
@@ -322,10 +322,10 @@ class Document(BaseMedia):
return ["langchain", "schema", "document"]
def __str__(self) -> str:
"""Override __str__ to restrict it to page_content and metadata.
"""Override `__str__` to restrict it to page_content and metadata.
Returns:
A string representation of the Document.
A string representation of the `Document`.
"""
# The format matches pydantic format for __str__.
#

View File

@@ -29,7 +29,7 @@ class LengthBasedExampleSelector(BaseExampleSelector, BaseModel):
max_length: int = 2048
"""Max length for the prompt, beyond which examples are cut."""
example_text_lengths: list[int] = Field(default_factory=list) # :meta private:
example_text_lengths: list[int] = Field(default_factory=list)
"""Length of each example."""
def add_example(self, example: dict[str, str]) -> None:

View File

@@ -6,16 +6,9 @@ import hashlib
import json
import uuid
import warnings
from collections.abc import (
AsyncIterable,
AsyncIterator,
Callable,
Iterable,
Iterator,
Sequence,
)
from itertools import islice
from typing import (
TYPE_CHECKING,
Any,
Literal,
TypedDict,
@@ -29,6 +22,16 @@ from langchain_core.exceptions import LangChainException
from langchain_core.indexing.base import DocumentIndex, RecordManager
from langchain_core.vectorstores import VectorStore
if TYPE_CHECKING:
from collections.abc import (
AsyncIterable,
AsyncIterator,
Callable,
Iterable,
Iterator,
Sequence,
)
# Magic UUID to use as a namespace for hashing.
# Used to try and generate a unique UUID for each document
# from hashing the document content and metadata.
@@ -298,7 +301,8 @@ def index(
For the time being, documents are indexed using their hashes, and users
are not able to specify the uid of the document.
!!! warning "Behavior changed in 0.3.25"
!!! warning "Behavior changed in `langchain-core` 0.3.25"
Added `scoped_full` cleanup mode.
!!! warning
@@ -349,7 +353,7 @@ def index(
key_encoder: Hashing algorithm to use for hashing the document content and
metadata. Options include "blake2b", "sha256", and "sha512".
!!! version-added "Added in version 0.3.66"
!!! version-added "Added in `langchain-core` 0.3.66"
key_encoder: Hashing algorithm to use for hashing the document.
If not provided, a default encoder using SHA-1 will be used.
@@ -366,7 +370,7 @@ def index(
method of the `VectorStore` or the upsert method of the DocumentIndex.
For example, you can use this to specify a custom vector_field:
upsert_kwargs={"vector_field": "embedding"}
!!! version-added "Added in version 0.3.10"
!!! version-added "Added in `langchain-core` 0.3.10"
Returns:
Indexing result which contains information about how many documents
@@ -636,7 +640,8 @@ async def aindex(
For the time being, documents are indexed using their hashes, and users
are not able to specify the uid of the document.
!!! warning "Behavior changed in 0.3.25"
!!! warning "Behavior changed in `langchain-core` 0.3.25"
Added `scoped_full` cleanup mode.
!!! warning
@@ -687,7 +692,7 @@ async def aindex(
key_encoder: Hashing algorithm to use for hashing the document content and
metadata. Options include "blake2b", "sha256", and "sha512".
!!! version-added "Added in version 0.3.66"
!!! version-added "Added in `langchain-core` 0.3.66"
key_encoder: Hashing algorithm to use for hashing the document.
If not provided, a default encoder using SHA-1 will be used.
@@ -704,7 +709,7 @@ async def aindex(
method of the `VectorStore` or the upsert method of the DocumentIndex.
For example, you can use this to specify a custom vector_field:
upsert_kwargs={"vector_field": "embedding"}
!!! version-added "Added in version 0.3.10"
!!! version-added "Added in `langchain-core` 0.3.10"
Returns:
Indexing result which contains information about how many documents

View File

@@ -53,6 +53,10 @@ if TYPE_CHECKING:
ParrotFakeChatModel,
)
from langchain_core.language_models.llms import LLM, BaseLLM
from langchain_core.language_models.model_profile import (
ModelProfile,
ModelProfileRegistry,
)
__all__ = (
"LLM",
@@ -68,6 +72,8 @@ __all__ = (
"LanguageModelInput",
"LanguageModelLike",
"LanguageModelOutput",
"ModelProfile",
"ModelProfileRegistry",
"ParrotFakeChatModel",
"SimpleChatModel",
"get_tokenizer",
@@ -90,6 +96,8 @@ _dynamic_imports = {
"GenericFakeChatModel": "fake_chat_models",
"ParrotFakeChatModel": "fake_chat_models",
"LLM": "llms",
"ModelProfile": "model_profile",
"ModelProfileRegistry": "model_profile",
"BaseLLM": "llms",
"is_openai_data_block": "_utils",
}

View File

@@ -139,7 +139,8 @@ def _normalize_messages(
directly; this may change in the future
- LangChain v0 standard content blocks for backward compatibility
!!! warning "Behavior changed in 1.0.0"
!!! warning "Behavior changed in `langchain-core` 1.0.0"
In previous versions, this function returned messages in LangChain v0 format.
Now, it returns messages in LangChain v1 format, which upgraded chat models now
expect to receive when passing back in message history. For backward

View File

@@ -131,14 +131,19 @@ class BaseLanguageModel(
Caching is not currently supported for streaming methods of models.
"""
verbose: bool = Field(default_factory=_get_verbosity, exclude=True, repr=False)
"""Whether to print out response text."""
callbacks: Callbacks = Field(default=None, exclude=True)
"""Callbacks to add to the run trace."""
tags: list[str] | None = Field(default=None, exclude=True)
"""Tags to add to the run trace."""
metadata: dict[str, Any] | None = Field(default=None, exclude=True)
"""Metadata to add to the run trace."""
custom_get_token_ids: Callable[[str], list[int]] | None = Field(
default=None, exclude=True
)
@@ -195,15 +200,22 @@ class BaseLanguageModel(
type (e.g., pure text completion models vs chat models).
Args:
prompts: List of `PromptValue` objects. A `PromptValue` is an object that
can be converted to match the format of any language model (string for
pure text generation models and `BaseMessage` objects for chat models).
stop: Stop words to use when generating. Model output is cut off at the
first occurrence of any of these substrings.
callbacks: `Callbacks` to pass through. Used for executing additional
functionality, such as logging or streaming, throughout generation.
**kwargs: Arbitrary additional keyword arguments. These are usually passed
to the model provider API call.
prompts: List of `PromptValue` objects.
A `PromptValue` is an object that can be converted to match the format
of any language model (string for pure text generation models and
`BaseMessage` objects for chat models).
stop: Stop words to use when generating.
Model output is cut off at the first occurrence of any of these
substrings.
callbacks: `Callbacks` to pass through.
Used for executing additional functionality, such as logging or
streaming, throughout generation.
**kwargs: Arbitrary additional keyword arguments.
These are usually passed to the model provider API call.
Returns:
An `LLMResult`, which contains a list of candidate `Generation` objects for
@@ -232,15 +244,22 @@ class BaseLanguageModel(
type (e.g., pure text completion models vs chat models).
Args:
prompts: List of `PromptValue` objects. A `PromptValue` is an object that
can be converted to match the format of any language model (string for
pure text generation models and `BaseMessage` objects for chat models).
stop: Stop words to use when generating. Model output is cut off at the
first occurrence of any of these substrings.
callbacks: `Callbacks` to pass through. Used for executing additional
functionality, such as logging or streaming, throughout generation.
**kwargs: Arbitrary additional keyword arguments. These are usually passed
to the model provider API call.
prompts: List of `PromptValue` objects.
A `PromptValue` is an object that can be converted to match the format
of any language model (string for pure text generation models and
`BaseMessage` objects for chat models).
stop: Stop words to use when generating.
Model output is cut off at the first occurrence of any of these
substrings.
callbacks: `Callbacks` to pass through.
Used for executing additional functionality, such as logging or
streaming, throughout generation.
**kwargs: Arbitrary additional keyword arguments.
These are usually passed to the model provider API call.
Returns:
An `LLMResult`, which contains a list of candidate `Generation` objects for
@@ -280,6 +299,9 @@ class BaseLanguageModel(
Useful for checking if an input fits in a model's context window.
This should be overridden by model-specific implementations to provide accurate
token counts via model-specific tokenizers.
Args:
text: The string input to tokenize.
@@ -298,9 +320,17 @@ class BaseLanguageModel(
Useful for checking if an input fits in a model's context window.
This should be overridden by model-specific implementations to provide accurate
token counts via model-specific tokenizers.
!!! note
The base implementation of `get_num_tokens_from_messages` ignores tool
schemas.
* The base implementation of `get_num_tokens_from_messages` ignores tool
schemas.
* The base implementation of `get_num_tokens_from_messages` adds additional
prefixes to messages in represent user roles, which will add to the
overall token count. Model-specific implementations may choose to
handle this differently.
Args:
messages: The message inputs to tokenize.

View File

@@ -15,7 +15,6 @@ from typing import TYPE_CHECKING, Any, Literal, cast
from pydantic import BaseModel, ConfigDict, Field
from typing_extensions import override
from langchain_core._api.beta_decorator import beta
from langchain_core.caches import BaseCache
from langchain_core.callbacks import (
AsyncCallbackManager,
@@ -34,6 +33,7 @@ from langchain_core.language_models.base import (
LangSmithParams,
LanguageModelInput,
)
from langchain_core.language_models.model_profile import ModelProfile
from langchain_core.load import dumpd, dumps
from langchain_core.messages import (
AIMessage,
@@ -76,8 +76,6 @@ from langchain_core.utils.utils import LC_ID_PREFIX, from_env
if TYPE_CHECKING:
import uuid
from langchain_model_profiles import ModelProfile # type: ignore[import-untyped]
from langchain_core.output_parsers.base import OutputParserLike
from langchain_core.runnables import Runnable, RunnableConfig
from langchain_core.tools import BaseTool
@@ -91,7 +89,10 @@ def _generate_response_from_error(error: BaseException) -> list[ChatGeneration]:
try:
metadata["body"] = response.json()
except Exception:
metadata["body"] = getattr(response, "text", None)
try:
metadata["body"] = getattr(response, "text", None)
except Exception:
metadata["body"] = None
if hasattr(response, "headers"):
try:
metadata["headers"] = dict(response.headers)
@@ -332,10 +333,25 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
[`langchain-openai`](https://pypi.org/project/langchain-openai)) can also use this
field to roll out new content formats in a backward-compatible way.
!!! version-added "Added in version 1.0"
!!! version-added "Added in `langchain-core` 1.0.0"
"""
profile: ModelProfile | None = Field(default=None, exclude=True)
"""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
if data is available.
Example profile data includes context window sizes, supported modalities, or support
for tool calling, structured output, and other features.
!!! version-added "Added in `langchain-core` 1.1.0"
"""
model_config = ConfigDict(
arbitrary_types_allowed=True,
)
@@ -845,16 +861,21 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
Args:
messages: List of list of messages.
stop: Stop words to use when generating. Model output is cut off at the
first occurrence of any of these substrings.
callbacks: `Callbacks` to pass through. Used for executing additional
functionality, such as logging or streaming, throughout generation.
stop: Stop words to use when generating.
Model output is cut off at the first occurrence of any of these
substrings.
callbacks: `Callbacks` to pass through.
Used for executing additional functionality, such as logging or
streaming, throughout generation.
tags: The tags to apply.
metadata: The metadata to apply.
run_name: The name of the run.
run_id: The ID of the run.
**kwargs: Arbitrary additional keyword arguments. These are usually passed
to the model provider API call.
**kwargs: Arbitrary additional keyword arguments.
These are usually passed to the model provider API call.
Returns:
An `LLMResult`, which contains a list of candidate `Generations` for each
@@ -963,16 +984,21 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
Args:
messages: List of list of messages.
stop: Stop words to use when generating. Model output is cut off at the
first occurrence of any of these substrings.
callbacks: `Callbacks` to pass through. Used for executing additional
functionality, such as logging or streaming, throughout generation.
stop: Stop words to use when generating.
Model output is cut off at the first occurrence of any of these
substrings.
callbacks: `Callbacks` to pass through.
Used for executing additional functionality, such as logging or
streaming, throughout generation.
tags: The tags to apply.
metadata: The metadata to apply.
run_name: The name of the run.
run_id: The ID of the run.
**kwargs: Arbitrary additional keyword arguments. These are usually passed
to the model provider API call.
**kwargs: Arbitrary additional keyword arguments.
These are usually passed to the model provider API call.
Returns:
An `LLMResult`, which contains a list of candidate `Generations` for each
@@ -1505,10 +1531,10 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
Args:
schema: The output schema. Can be passed in as:
- an OpenAI function/tool schema,
- a JSON Schema,
- a `TypedDict` class,
- or a Pydantic class.
- An OpenAI function/tool schema,
- A JSON Schema,
- A `TypedDict` class,
- Or a Pydantic class.
If `schema` is a Pydantic class then the model output will be a
Pydantic instance of that class, and the model-generated fields will be
@@ -1520,11 +1546,15 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
when specifying a Pydantic or `TypedDict` class.
include_raw:
If `False` then only the parsed structured output is returned. If
an error occurs during model output parsing it will be raised. If `True`
then both the raw model response (a `BaseMessage`) and the parsed model
response will be returned. If an error occurs during output parsing it
will be caught and returned as well.
If `False` then only the parsed structured output is returned.
If an error occurs during model output parsing it will be raised.
If `True` then both the raw model response (a `BaseMessage`) and the
parsed model response will be returned.
If an error occurs during output parsing it will be caught and returned
as well.
The final output is always a `dict` with keys `'raw'`, `'parsed'`, and
`'parsing_error'`.
@@ -1602,7 +1632,7 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
# }
```
Example: `dict` schema (`include_raw=False`):
Example: Dictionary schema (`include_raw=False`):
```python
from pydantic import BaseModel
@@ -1629,8 +1659,9 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
# }
```
!!! warning "Behavior changed in 0.2.26"
Added support for TypedDict class.
!!! warning "Behavior changed in `langchain-core` 0.2.26"
Added support for `TypedDict` class.
""" # noqa: E501
_ = kwargs.pop("method", None)
@@ -1671,41 +1702,6 @@ class BaseChatModel(BaseLanguageModel[AIMessage], ABC):
return RunnableMap(raw=llm) | parser_with_fallback
return llm | output_parser
@property
@beta()
def profile(self) -> ModelProfile:
"""Return profiling information for the model.
This property will relies on the `langchain-model-profiles` package to
retrieve chat model capabilities, such as context window sizes and supported
features.
Raises:
ImportError: If `langchain-model-profiles` is not installed.
Returns:
A `ModelProfile` object containing profiling information for the model.
"""
try:
from langchain_model_profiles import get_model_profile # noqa: PLC0415
except ImportError as err:
informative_error_message = (
"To access model profiling information, please install the "
"`langchain-model-profiles` package: "
"`pip install langchain-model-profiles`."
)
raise ImportError(informative_error_message) from err
provider_id = self._llm_type
model_name = (
# Model name is not standardized across integrations. New integrations
# should prefer `model`.
getattr(self, "model", None)
or getattr(self, "model_name", None)
or getattr(self, "model_id", "")
)
return get_model_profile(provider_id, model_name) or {}
class SimpleChatModel(BaseChatModel):
"""Simplified implementation for a chat model to inherit from.
@@ -1764,9 +1760,12 @@ def _gen_info_and_msg_metadata(
}
_MAX_CLEANUP_DEPTH = 100
def _cleanup_llm_representation(serialized: Any, depth: int) -> None:
"""Remove non-serializable objects from a serialized object."""
if depth > 100: # Don't cooperate for pathological cases
if depth > _MAX_CLEANUP_DEPTH: # Don't cooperate for pathological cases
return
if not isinstance(serialized, dict):

View File

@@ -651,9 +651,12 @@ class BaseLLM(BaseLanguageModel[str], ABC):
Args:
prompts: The prompts to generate from.
stop: Stop words to use when generating. Model output is cut off at the
first occurrence of any of the stop substrings.
If stop tokens are not supported consider raising NotImplementedError.
stop: Stop words to use when generating.
Model output is cut off at the first occurrence of any of these
substrings.
If stop tokens are not supported consider raising `NotImplementedError`.
run_manager: Callback manager for the run.
Returns:
@@ -671,9 +674,12 @@ class BaseLLM(BaseLanguageModel[str], ABC):
Args:
prompts: The prompts to generate from.
stop: Stop words to use when generating. Model output is cut off at the
first occurrence of any of the stop substrings.
If stop tokens are not supported consider raising NotImplementedError.
stop: Stop words to use when generating.
Model output is cut off at the first occurrence of any of these
substrings.
If stop tokens are not supported consider raising `NotImplementedError`.
run_manager: Callback manager for the run.
Returns:
@@ -705,11 +711,14 @@ class BaseLLM(BaseLanguageModel[str], ABC):
Args:
prompt: The prompt to generate from.
stop: Stop words to use when generating. Model output is cut off at the
first occurrence of any of these substrings.
stop: Stop words to use when generating.
Model output is cut off at the first occurrence of any of these
substrings.
run_manager: Callback manager for the run.
**kwargs: Arbitrary additional keyword arguments. These are usually passed
to the model provider API call.
**kwargs: Arbitrary additional keyword arguments.
These are usually passed to the model provider API call.
Yields:
Generation chunks.
@@ -731,11 +740,14 @@ class BaseLLM(BaseLanguageModel[str], ABC):
Args:
prompt: The prompt to generate from.
stop: Stop words to use when generating. Model output is cut off at the
first occurrence of any of these substrings.
stop: Stop words to use when generating.
Model output is cut off at the first occurrence of any of these
substrings.
run_manager: Callback manager for the run.
**kwargs: Arbitrary additional keyword arguments. These are usually passed
to the model provider API call.
**kwargs: Arbitrary additional keyword arguments.
These are usually passed to the model provider API call.
Yields:
Generation chunks.
@@ -846,10 +858,14 @@ class BaseLLM(BaseLanguageModel[str], ABC):
Args:
prompts: List of string prompts.
stop: Stop words to use when generating. Model output is cut off at the
first occurrence of any of these substrings.
callbacks: `Callbacks` to pass through. Used for executing additional
functionality, such as logging or streaming, throughout generation.
stop: Stop words to use when generating.
Model output is cut off at the first occurrence of any of these
substrings.
callbacks: `Callbacks` to pass through.
Used for executing additional functionality, such as logging or
streaming, throughout generation.
tags: List of tags to associate with each prompt. If provided, the length
of the list must match the length of the prompts list.
metadata: List of metadata dictionaries to associate with each prompt. If
@@ -859,8 +875,9 @@ class BaseLLM(BaseLanguageModel[str], ABC):
length of the list must match the length of the prompts list.
run_id: List of run IDs to associate with each prompt. If provided, the
length of the list must match the length of the prompts list.
**kwargs: Arbitrary additional keyword arguments. These are usually passed
to the model provider API call.
**kwargs: Arbitrary additional keyword arguments.
These are usually passed to the model provider API call.
Raises:
ValueError: If prompts is not a list.
@@ -1116,10 +1133,14 @@ class BaseLLM(BaseLanguageModel[str], ABC):
Args:
prompts: List of string prompts.
stop: Stop words to use when generating. Model output is cut off at the
first occurrence of any of these substrings.
callbacks: `Callbacks` to pass through. Used for executing additional
functionality, such as logging or streaming, throughout generation.
stop: Stop words to use when generating.
Model output is cut off at the first occurrence of any of these
substrings.
callbacks: `Callbacks` to pass through.
Used for executing additional functionality, such as logging or
streaming, throughout generation.
tags: List of tags to associate with each prompt. If provided, the length
of the list must match the length of the prompts list.
metadata: List of metadata dictionaries to associate with each prompt. If
@@ -1129,8 +1150,9 @@ class BaseLLM(BaseLanguageModel[str], ABC):
length of the list must match the length of the prompts list.
run_id: List of run IDs to associate with each prompt. If provided, the
length of the list must match the length of the prompts list.
**kwargs: Arbitrary additional keyword arguments. These are usually passed
to the model provider API call.
**kwargs: Arbitrary additional keyword arguments.
These are usually passed to the model provider API call.
Raises:
ValueError: If the length of `callbacks`, `tags`, `metadata`, or
@@ -1410,12 +1432,16 @@ class LLM(BaseLLM):
Args:
prompt: The prompt to generate from.
stop: Stop words to use when generating. Model output is cut off at the
first occurrence of any of the stop substrings.
If stop tokens are not supported consider raising NotImplementedError.
stop: Stop words to use when generating.
Model output is cut off at the first occurrence of any of these
substrings.
If stop tokens are not supported consider raising `NotImplementedError`.
run_manager: Callback manager for the run.
**kwargs: Arbitrary additional keyword arguments. These are usually passed
to the model provider API call.
**kwargs: Arbitrary additional keyword arguments.
These are usually passed to the model provider API call.
Returns:
The model output as a string. SHOULD NOT include the prompt.
@@ -1436,12 +1462,16 @@ class LLM(BaseLLM):
Args:
prompt: The prompt to generate from.
stop: Stop words to use when generating. Model output is cut off at the
first occurrence of any of the stop substrings.
If stop tokens are not supported consider raising NotImplementedError.
stop: Stop words to use when generating.
Model output is cut off at the first occurrence of any of these
substrings.
If stop tokens are not supported consider raising `NotImplementedError`.
run_manager: Callback manager for the run.
**kwargs: Arbitrary additional keyword arguments. These are usually passed
to the model provider API call.
**kwargs: Arbitrary additional keyword arguments.
These are usually passed to the model provider API call.
Returns:
The model output as a string. SHOULD NOT include the prompt.

View File

@@ -0,0 +1,84 @@
"""Model profile types and utilities."""
from typing_extensions import TypedDict
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
and supported features.
"""
# --- Input constraints ---
max_input_tokens: int
"""Maximum context window (tokens)"""
image_inputs: bool
"""Whether image inputs are supported."""
# TODO: add more detail about formats?
image_url_inputs: bool
"""Whether [image URL inputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
pdf_inputs: bool
"""Whether [PDF inputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
# TODO: add more detail about formats? e.g. bytes or base64
audio_inputs: bool
"""Whether [audio inputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
# TODO: add more detail about formats? e.g. bytes or base64
video_inputs: bool
"""Whether [video inputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
# TODO: add more detail about formats? e.g. bytes or base64
image_tool_message: bool
"""Whether images can be included in tool messages."""
pdf_tool_message: bool
"""Whether PDFs can be included in tool messages."""
# --- Output constraints ---
max_output_tokens: int
"""Maximum output tokens"""
reasoning_output: bool
"""Whether the model supports [reasoning / chain-of-thought](https://docs.langchain.com/oss/python/langchain/models#reasoning)"""
image_outputs: bool
"""Whether [image outputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
audio_outputs: bool
"""Whether [audio outputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
video_outputs: bool
"""Whether [video outputs](https://docs.langchain.com/oss/python/langchain/models#multimodal)
are supported."""
# --- Tool calling ---
tool_calling: bool
"""Whether the model supports [tool calling](https://docs.langchain.com/oss/python/langchain/models#tool-calling)"""
tool_choice: bool
"""Whether the model supports [tool choice](https://docs.langchain.com/oss/python/langchain/models#forcing-tool-calls)"""
# --- Structured output ---
structured_output: bool
"""Whether the model supports a native [structured output](https://docs.langchain.com/oss/python/langchain/models#structured-outputs)
feature"""
ModelProfileRegistry = dict[str, ModelProfile]
"""Registry mapping model identifiers or names to their ModelProfile."""

View File

@@ -61,13 +61,15 @@ class Reviver:
"""Initialize the reviver.
Args:
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.
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.
secrets_from_env: Whether to load secrets from the environment.
additional_import_mappings: A dictionary of additional namespace mappings
You can use this to override default mappings or add new mappings.
ignore_unserializable_fields: Whether to ignore unserializable fields.
"""
@@ -195,13 +197,15 @@ def loads(
Args:
text: The string to load.
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.
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.
secrets_from_env: Whether to load secrets from the environment.
additional_import_mappings: A dictionary of additional namespace mappings
You can use this to override default mappings or add new mappings.
ignore_unserializable_fields: Whether to ignore unserializable fields.
@@ -237,13 +241,15 @@ def load(
Args:
obj: The object to load.
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.
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.
secrets_from_env: Whether to load secrets from the environment.
additional_import_mappings: A dictionary of additional namespace mappings
You can use this to override default mappings or add new mappings.
ignore_unserializable_fields: Whether to ignore unserializable fields.

View File

@@ -50,7 +50,7 @@ class InputTokenDetails(TypedDict, total=False):
May also hold extra provider-specific keys.
!!! version-added "Added in version 0.3.9"
!!! version-added "Added in `langchain-core` 0.3.9"
"""
@@ -85,7 +85,7 @@ class OutputTokenDetails(TypedDict, total=False):
May also hold extra provider-specific keys.
!!! version-added "Added in version 0.3.9"
!!! version-added "Added in `langchain-core` 0.3.9"
"""
@@ -123,10 +123,12 @@ class UsageMetadata(TypedDict):
}
```
!!! warning "Behavior changed in 0.3.9"
!!! warning "Behavior changed in `langchain-core` 0.3.9"
Added `input_token_details` and `output_token_details`.
!!! note "LangSmith SDK"
The LangSmith SDK also has a `UsageMetadata` class. While the two share fields,
LangSmith's `UsageMetadata` has additional fields to capture cost information
used by the LangSmith platform.

View File

@@ -5,11 +5,9 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any, cast, overload
from pydantic import ConfigDict, Field
from typing_extensions import Self
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,6 +15,9 @@ from langchain_core.utils.interactive_env import is_interactive_env
if TYPE_CHECKING:
from collections.abc import Sequence
from typing_extensions import Self
from langchain_core.messages import content as types
from langchain_core.prompts.chat import ChatPromptTemplate
@@ -199,7 +200,7 @@ class BaseMessage(Serializable):
def content_blocks(self) -> list[types.ContentBlock]:
r"""Load content blocks from the message content.
!!! version-added "Added in version 1.0.0"
!!! version-added "Added in `langchain-core` 1.0.0"
"""
# Needed here to avoid circular import, as these classes import BaseMessages

View File

@@ -12,10 +12,11 @@ the implementation in `BaseMessage`.
from __future__ import annotations
from collections.abc import Callable
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Callable
from langchain_core.messages import AIMessage, AIMessageChunk
from langchain_core.messages import content as types

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
import json
import warnings
from collections.abc import Iterable
from typing import TYPE_CHECKING, Any, Literal, cast
from langchain_core.language_models._utils import (
@@ -14,6 +13,8 @@ from langchain_core.language_models._utils import (
from langchain_core.messages import content as types
if TYPE_CHECKING:
from collections.abc import Iterable
from langchain_core.messages import AIMessage, AIMessageChunk

View File

@@ -654,7 +654,7 @@ class PlainTextContentBlock(TypedDict):
!!! note
Title and context are optional fields that may be passed to the model. See
Anthropic [example](https://docs.claude.com/en/docs/build-with-claude/citations#citable-vs-non-citable-content).
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
@@ -867,7 +867,7 @@ def _get_data_content_block_types() -> tuple[str, ...]:
Example: ("image", "video", "audio", "text-plain", "file")
Note that old style multimodal blocks type literals with new style blocks.
Speficially, "image", "audio", and "file".
Specifically, "image", "audio", and "file".
See the docstring of `_normalize_messages` in `language_models._utils` for details.
"""
@@ -906,7 +906,7 @@ def is_data_content_block(block: dict) -> bool:
# 'text' is checked to support v0 PlainTextContentBlock types
# We must guard against new style TextContentBlock which also has 'text' `type`
# by ensuring the presense of `source_type`
# by ensuring the presence of `source_type`
if block["type"] == "text" and "source_type" not in block: # noqa: SIM103 # This is more readable
return False

View File

@@ -328,12 +328,16 @@ def _convert_to_message(message: MessageLikeRepresentation) -> BaseMessage:
"""
if isinstance(message, BaseMessage):
message_ = message
elif isinstance(message, str):
message_ = _create_message_from_message_type("human", message)
elif isinstance(message, Sequence) and len(message) == 2:
# mypy doesn't realise this can't be a string given the previous branch
message_type_str, template = message # type: ignore[misc]
message_ = _create_message_from_message_type(message_type_str, template)
elif isinstance(message, Sequence):
if isinstance(message, str):
message_ = _create_message_from_message_type("human", message)
else:
try:
message_type_str, template = message
except ValueError as e:
msg = "Message as a sequence must be (role string, template)"
raise NotImplementedError(msg) from e
message_ = _create_message_from_message_type(message_type_str, template)
elif isinstance(message, dict):
msg_kwargs = message.copy()
try:
@@ -734,8 +738,10 @@ def trim_messages(
Set to `len` to count the number of **messages** in the chat history.
!!! 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.
@@ -1097,7 +1103,7 @@ def convert_to_openai_messages(
# ]
```
!!! version-added "Added in version 0.3.11"
!!! version-added "Added in `langchain-core` 0.3.11"
""" # noqa: E501
if text_format not in {"string", "block"}:
@@ -1697,7 +1703,7 @@ def count_tokens_approximately(
Warning:
This function does not currently support counting image tokens.
!!! version-added "Added in version 0.3.46"
!!! version-added "Added in `langchain-core` 0.3.46"
"""
token_count = 0.0

View File

@@ -15,7 +15,11 @@ from langchain_core.messages.tool import tool_call as create_tool_call
from langchain_core.output_parsers.transform import BaseCumulativeTransformOutputParser
from langchain_core.outputs import ChatGeneration, Generation
from langchain_core.utils.json import parse_partial_json
from langchain_core.utils.pydantic import TypeBaseModel
from langchain_core.utils.pydantic import (
TypeBaseModel,
is_pydantic_v1_subclass,
is_pydantic_v2_subclass,
)
logger = logging.getLogger(__name__)
@@ -323,7 +327,15 @@ class PydanticToolsParser(JsonOutputToolsParser):
return None if self.first_tool_only else []
json_results = [json_results] if self.first_tool_only else json_results
name_dict = {tool.__name__: tool for tool in self.tools}
name_dict_v2: dict[str, TypeBaseModel] = {
tool.model_config.get("title") or tool.__name__: tool
for tool in self.tools
if is_pydantic_v2_subclass(tool)
}
name_dict_v1: dict[str, TypeBaseModel] = {
tool.__name__: tool for tool in self.tools if is_pydantic_v1_subclass(tool)
}
name_dict: dict[str, TypeBaseModel] = {**name_dict_v2, **name_dict_v1}
pydantic_objects = []
for res in json_results:
if not isinstance(res["args"], dict):

View File

@@ -37,7 +37,7 @@ class PydanticOutputParser(JsonOutputParser, Generic[TBaseModel]):
def _parser_exception(
self, e: Exception, json_object: dict
) -> OutputParserException:
json_string = json.dumps(json_object)
json_string = json.dumps(json_object, ensure_ascii=False)
name = self.pydantic_object.__name__
msg = f"Failed to parse {name} from completion {json_string}. Got: {e}"
return OutputParserException(msg, llm_output=json_string)

View File

@@ -2,15 +2,17 @@
from __future__ import annotations
from typing import Literal
from typing import TYPE_CHECKING, Literal
from pydantic import model_validator
from typing_extensions import Self
from langchain_core.messages import BaseMessage, BaseMessageChunk
from langchain_core.outputs.generation import Generation
from langchain_core.utils._merge import merge_dicts
if TYPE_CHECKING:
from typing_extensions import Self
class ChatGeneration(Generation):
"""A single chat generation output.

View File

@@ -20,8 +20,7 @@ class Generation(Serializable):
LangChain users working with chat models will usually access information via
`AIMessage` (returned from runnable interfaces) or `LLMResult` (available
via callbacks). Please refer the `AIMessage` and `LLMResult` schema documentation
for more information.
via callbacks). Please refer to `AIMessage` and `LLMResult` for more information.
"""
text: str
@@ -34,11 +33,13 @@ class Generation(Serializable):
"""
type: Literal["Generation"] = "Generation"
"""Type is used exclusively for serialization purposes.
Set to "Generation" for this class."""
Set to "Generation" for this class.
"""
@classmethod
def is_lc_serializable(cls) -> bool:
"""Return True as this class is serializable."""
"""Return `True` as this class is serializable."""
return True
@classmethod
@@ -52,7 +53,7 @@ class Generation(Serializable):
class GenerationChunk(Generation):
"""Generation chunk, which can be concatenated with other Generation chunks."""
"""`GenerationChunk`, which can be concatenated with other Generation chunks."""
def __add__(self, other: GenerationChunk) -> GenerationChunk:
"""Concatenate two `GenerationChunk`s.

View File

@@ -30,7 +30,7 @@ class PromptValue(Serializable, ABC):
@classmethod
def is_lc_serializable(cls) -> bool:
"""Return True as this class is serializable."""
"""Return `True` as this class is serializable."""
return True
@classmethod
@@ -48,7 +48,7 @@ class PromptValue(Serializable, ABC):
@abstractmethod
def to_messages(self) -> list[BaseMessage]:
"""Return prompt as a list of Messages."""
"""Return prompt as a list of messages."""
class StringPromptValue(PromptValue):

View File

@@ -6,7 +6,7 @@ import contextlib
import json
import typing
from abc import ABC, abstractmethod
from collections.abc import Callable, Mapping
from collections.abc import Mapping
from functools import cached_property
from pathlib import Path
from typing import (
@@ -33,6 +33,8 @@ from langchain_core.runnables.config import ensure_config
from langchain_core.utils.pydantic import create_model_v2
if TYPE_CHECKING:
from collections.abc import Callable
from langchain_core.documents import Document
@@ -46,23 +48,27 @@ class BasePromptTemplate(
input_variables: list[str]
"""A list of the names of the variables whose values are required as inputs to the
prompt."""
prompt.
"""
optional_variables: list[str] = Field(default=[])
"""A list of the names of the variables for placeholder or `MessagePlaceholder` that
are optional.
These variables are auto inferred from the prompt and user need not provide them."""
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
"""A dictionary of the types of the variables the prompt template expects.
If not provided, all variables are assumed to be strings."""
If not provided, all variables are assumed to be strings.
"""
output_parser: BaseOutputParser | None = None
"""How to parse the output of calling an LLM on this formatted prompt."""
partial_variables: Mapping[str, Any] = Field(default_factory=dict)
"""A dictionary of the partial variables the prompt template carries.
Partial variables populate the template so that you don't need to
pass them in every time you call the prompt."""
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 to be used for tracing."""
tags: list[str] | None = None
@@ -107,7 +113,7 @@ class BasePromptTemplate(
@classmethod
def is_lc_serializable(cls) -> bool:
"""Return True as this class is serializable."""
"""Return `True` as this class is serializable."""
return True
model_config = ConfigDict(
@@ -129,7 +135,7 @@ class BasePromptTemplate(
"""Get the input schema for the prompt.
Args:
config: configuration for the prompt.
config: Configuration for the prompt.
Returns:
The input schema for the prompt.
@@ -197,8 +203,8 @@ class BasePromptTemplate(
"""Invoke the prompt.
Args:
input: Dict, input to the prompt.
config: RunnableConfig, configuration for the prompt.
input: Input to the prompt.
config: Configuration for the prompt.
Returns:
The output of the prompt.
@@ -223,8 +229,8 @@ class BasePromptTemplate(
"""Async invoke the prompt.
Args:
input: Dict, input to the prompt.
config: RunnableConfig, configuration for the prompt.
input: Input to the prompt.
config: Configuration for the prompt.
Returns:
The output of the prompt.
@@ -244,7 +250,7 @@ class BasePromptTemplate(
@abstractmethod
def format_prompt(self, **kwargs: Any) -> PromptValue:
"""Create Prompt Value.
"""Create `PromptValue`.
Args:
**kwargs: Any arguments to be passed to the prompt template.
@@ -254,7 +260,7 @@ class BasePromptTemplate(
"""
async def aformat_prompt(self, **kwargs: Any) -> PromptValue:
"""Async create Prompt Value.
"""Async create `PromptValue`.
Args:
**kwargs: Any arguments to be passed to the prompt template.
@@ -268,7 +274,7 @@ class BasePromptTemplate(
"""Return a partial of the prompt template.
Args:
**kwargs: partial variables to set.
**kwargs: Partial variables to set.
Returns:
A partial of the prompt template.
@@ -298,9 +304,9 @@ class BasePromptTemplate(
A formatted string.
Example:
```python
prompt.format(variable1="foo")
```
```python
prompt.format(variable1="foo")
```
"""
async def aformat(self, **kwargs: Any) -> FormatOutputType:
@@ -313,9 +319,9 @@ class BasePromptTemplate(
A formatted string.
Example:
```python
await prompt.aformat(variable1="foo")
```
```python
await prompt.aformat(variable1="foo")
```
"""
return self.format(**kwargs)
@@ -350,9 +356,9 @@ class BasePromptTemplate(
NotImplementedError: If the prompt type is not implemented.
Example:
```python
prompt.save(file_path="path/prompt.yaml")
```
```python
prompt.save(file_path="path/prompt.yaml")
```
"""
if self.partial_variables:
msg = "Cannot save prompt with partial variables."
@@ -404,23 +410,23 @@ def format_document(doc: Document, prompt: BasePromptTemplate[str]) -> str:
First, this pulls information from the document from two sources:
1. page_content:
This takes the information from the `document.page_content`
and assigns it to a variable named `page_content`.
2. metadata:
This takes information from `document.metadata` and assigns
it to variables of the same name.
1. `page_content`:
This takes the information from the `document.page_content` and assigns it to a
variable named `page_content`.
2. `metadata`:
This takes information from `document.metadata` and assigns it to variables of
the same name.
Those variables are then passed into the `prompt` to produce a formatted string.
Args:
doc: Document, the page_content and metadata will be used to create
doc: `Document`, the `page_content` and `metadata` will be used to create
the final string.
prompt: BasePromptTemplate, will be used to format the page_content
and metadata into the final string.
prompt: `BasePromptTemplate`, will be used to format the `page_content`
and `metadata` into the final string.
Returns:
string of the document formatted.
String of the document formatted.
Example:
```python
@@ -431,7 +437,6 @@ def format_document(doc: Document, prompt: BasePromptTemplate[str]) -> str:
prompt = PromptTemplate.from_template("Page {page}: {page_content}")
format_document(doc, prompt)
>>> "Page 1: This is a joke"
```
"""
return prompt.format(**_get_document_info(doc, prompt))
@@ -442,22 +447,22 @@ async def aformat_document(doc: Document, prompt: BasePromptTemplate[str]) -> st
First, this pulls information from the document from two sources:
1. page_content:
This takes the information from the `document.page_content`
and assigns it to a variable named `page_content`.
2. metadata:
This takes information from `document.metadata` and assigns
it to variables of the same name.
1. `page_content`:
This takes the information from the `document.page_content` and assigns it to a
variable named `page_content`.
2. `metadata`:
This takes information from `document.metadata` and assigns it to variables of
the same name.
Those variables are then passed into the `prompt` to produce a formatted string.
Args:
doc: Document, the page_content and metadata will be used to create
doc: `Document`, the `page_content` and `metadata` will be used to create
the final string.
prompt: BasePromptTemplate, will be used to format the page_content
and metadata into the final string.
prompt: `BasePromptTemplate`, will be used to format the `page_content`
and `metadata` into the final string.
Returns:
string of the document formatted.
String of the document formatted.
"""
return await prompt.aformat(**_get_document_info(doc, prompt))

View File

@@ -587,14 +587,15 @@ class _StringImageMessagePromptTemplate(BaseMessagePromptTemplate):
for prompt in self.prompt:
inputs = {var: kwargs[var] for var in prompt.input_variables}
if isinstance(prompt, StringPromptTemplate):
formatted: str | ImageURL | dict[str, Any] = prompt.format(**inputs)
content.append({"type": "text", "text": formatted})
formatted_text: str = prompt.format(**inputs)
if formatted_text != "":
content.append({"type": "text", "text": formatted_text})
elif isinstance(prompt, ImagePromptTemplate):
formatted = prompt.format(**inputs)
content.append({"type": "image_url", "image_url": formatted})
formatted_image: ImageURL = prompt.format(**inputs)
content.append({"type": "image_url", "image_url": formatted_image})
elif isinstance(prompt, DictPromptTemplate):
formatted = prompt.format(**inputs)
content.append(formatted)
formatted_dict: dict[str, Any] = prompt.format(**inputs)
content.append(formatted_dict)
return self._msg_class(
content=content, additional_kwargs=self.additional_kwargs
)
@@ -617,16 +618,15 @@ class _StringImageMessagePromptTemplate(BaseMessagePromptTemplate):
for prompt in self.prompt:
inputs = {var: kwargs[var] for var in prompt.input_variables}
if isinstance(prompt, StringPromptTemplate):
formatted: str | ImageURL | dict[str, Any] = await prompt.aformat(
**inputs
)
content.append({"type": "text", "text": formatted})
formatted_text: str = await prompt.aformat(**inputs)
if formatted_text != "":
content.append({"type": "text", "text": formatted_text})
elif isinstance(prompt, ImagePromptTemplate):
formatted = await prompt.aformat(**inputs)
content.append({"type": "image_url", "image_url": formatted})
formatted_image: ImageURL = await prompt.aformat(**inputs)
content.append({"type": "image_url", "image_url": formatted_image})
elif isinstance(prompt, DictPromptTemplate):
formatted = prompt.format(**inputs)
content.append(formatted)
formatted_dict: dict[str, Any] = prompt.format(**inputs)
content.append(formatted_dict)
return self._msg_class(
content=content, additional_kwargs=self.additional_kwargs
)
@@ -903,23 +903,28 @@ class ChatPromptTemplate(BaseChatPromptTemplate):
5. A string which is shorthand for `("human", template)`; e.g.,
`"{user_input}"`
template_format: Format of the template.
input_variables: A list of the names of the variables whose values are
required as inputs to the prompt.
optional_variables: A list of the names of the variables for placeholder
or MessagePlaceholder that are optional.
**kwargs: Additional keyword arguments passed to `BasePromptTemplate`,
including (but not limited to):
These variables are auto inferred from the prompt and user need not
provide them.
partial_variables: A dictionary of the partial variables the prompt
template carries.
- `input_variables`: A list of the names of the variables whose values
are required as inputs to the prompt.
- `optional_variables`: A list of the names of the variables for
placeholder or `MessagePlaceholder` that are optional.
Partial variables populate the template so that you don't need to pass
them in every time you call the prompt.
validate_template: Whether to validate the template.
input_types: A dictionary of the types of the variables the prompt template
expects.
These variables are auto inferred from the prompt and user need not
provide them.
If not provided, all variables are assumed to be strings.
- `partial_variables`: A dictionary of the partial variables the prompt
template carries.
Partial variables populate the template so that you don't need to
pass them in every time you call the prompt.
- `validate_template`: Whether to validate the template.
- `input_types`: A dictionary of the types of the variables the prompt
template expects.
If not provided, all variables are assumed to be strings.
Examples:
Instantiation from a list of message templates:
@@ -1343,11 +1348,25 @@ def _create_template_from_message_type(
raise ValueError(msg)
var_name = template[1:-1]
message = MessagesPlaceholder(variable_name=var_name, optional=True)
elif len(template) == 2 and isinstance(template[1], bool):
var_name_wrapped, is_optional = template
else:
try:
var_name_wrapped, is_optional = template
except ValueError as e:
msg = (
"Unexpected arguments for placeholder message type."
" Expected either a single string variable name"
" or a list of [variable_name: str, is_optional: bool]."
f" Got: {template}"
)
raise ValueError(msg) from e
if not isinstance(is_optional, bool):
msg = f"Expected is_optional to be a boolean. Got: {is_optional}"
raise ValueError(msg) # noqa: TRY004
if not isinstance(var_name_wrapped, str):
msg = f"Expected variable name to be a string. Got: {var_name_wrapped}"
raise ValueError(msg) # noqa:TRY004
raise ValueError(msg) # noqa: TRY004
if var_name_wrapped[0] != "{" or var_name_wrapped[-1] != "}":
msg = (
f"Invalid placeholder template: {var_name_wrapped}."
@@ -1357,14 +1376,6 @@ def _create_template_from_message_type(
var_name = var_name_wrapped[1:-1]
message = MessagesPlaceholder(variable_name=var_name, optional=is_optional)
else:
msg = (
"Unexpected arguments for placeholder message type."
" Expected either a single string variable name"
" or a list of [variable_name: str, is_optional: bool]."
f" Got: {template}"
)
raise ValueError(msg)
else:
msg = (
f"Unexpected message type: {message_type}. Use one of 'human',"
@@ -1418,10 +1429,11 @@ def _convert_to_message_template(
)
raise ValueError(msg)
message = (message["role"], message["content"])
if len(message) != 2:
try:
message_type_str, template = message
except ValueError as e:
msg = f"Expected 2-tuple of (role, template), got {message}"
raise ValueError(msg)
message_type_str, template = 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

View File

@@ -69,7 +69,7 @@ class DictPromptTemplate(RunnableSerializable[dict, dict]):
@classmethod
def is_lc_serializable(cls) -> bool:
"""Return True as this class is serializable."""
"""Return `True` as this class is serializable."""
return True
@classmethod

View File

@@ -6,10 +6,10 @@ from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any
from langchain_core.load import Serializable
from langchain_core.messages import BaseMessage
from langchain_core.utils.interactive_env import is_interactive_env
if TYPE_CHECKING:
from langchain_core.messages import BaseMessage
from langchain_core.prompts.chat import ChatPromptTemplate
@@ -18,7 +18,7 @@ class BaseMessagePromptTemplate(Serializable, ABC):
@classmethod
def is_lc_serializable(cls) -> bool:
"""Return True as this class is serializable."""
"""Return `True` as this class is serializable."""
return True
@classmethod
@@ -32,13 +32,13 @@ class BaseMessagePromptTemplate(Serializable, ABC):
@abstractmethod
def format_messages(self, **kwargs: Any) -> list[BaseMessage]:
"""Format messages from kwargs. Should return a list of BaseMessages.
"""Format messages from kwargs. Should return a list of `BaseMessage` objects.
Args:
**kwargs: Keyword arguments to use for formatting.
Returns:
List of BaseMessages.
List of `BaseMessage` objects.
"""
async def aformat_messages(self, **kwargs: Any) -> list[BaseMessage]:
@@ -48,7 +48,7 @@ class BaseMessagePromptTemplate(Serializable, ABC):
**kwargs: Keyword arguments to use for formatting.
Returns:
List of BaseMessages.
List of `BaseMessage` objects.
"""
return self.format_messages(**kwargs)

View File

@@ -4,9 +4,8 @@ from __future__ import annotations
import warnings
from abc import ABC
from collections.abc import Callable, Sequence
from string import Formatter
from typing import Any, Literal
from typing import TYPE_CHECKING, Any, Literal
from pydantic import BaseModel, create_model
@@ -16,10 +15,70 @@ from langchain_core.utils import get_colored_text, mustache
from langchain_core.utils.formatting import formatter
from langchain_core.utils.interactive_env import is_interactive_env
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
try:
from jinja2 import Environment, meta
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
@@ -59,14 +118,10 @@ def jinja2_formatter(template: str, /, **kwargs: Any) -> str:
)
raise ImportError(msg)
# This uses a sandboxed environment to prevent arbitrary code execution.
# Jinja2 uses an opt-out rather than opt-in approach for sand-boxing.
# Please treat this sand-boxing as a best-effort approach rather than
# a guarantee of security.
# We recommend to never use jinja2 templates with untrusted inputs.
# https://jinja.palletsprojects.com/en/3.1.x/sandbox/
# approach not a guarantee of security.
return SandboxedEnvironment().from_string(template).render(**kwargs)
# 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)
def validate_jinja2(template: str, input_variables: list[str]) -> None:
@@ -101,7 +156,7 @@ def _get_jinja2_variables_from_template(template: str) -> set[str]:
"Please install it with `pip install jinja2`."
)
raise ImportError(msg)
env = Environment() # noqa: S701
env = _RestrictedSandboxedEnvironment()
ast = env.parse(template)
return meta.find_undeclared_variables(ast)
@@ -271,6 +326,30 @@ def get_template_variables(template: str, template_format: str) -> list[str]:
msg = f"Unsupported template format: {template_format}"
raise ValueError(msg)
# For f-strings, block attribute access and indexing syntax
# This prevents template injection attacks via accessing dangerous attributes
if template_format == "f-string":
for var in input_variables:
# Formatter().parse() returns field names with dots/brackets if present
# e.g., "obj.attr" or "obj[0]" - we need to block these
if "." in var or "[" in var or "]" in var:
msg = (
f"Invalid variable name {var!r} in f-string template. "
f"Variable names cannot contain attribute "
f"access (.) or indexing ([])."
)
raise ValueError(msg)
# Block variable names that are all digits (e.g., "0", "100")
# These are interpreted as positional arguments, not keyword arguments
if var.isdigit():
msg = (
f"Invalid variable name {var!r} in f-string template. "
f"Variable names cannot be all digits as they are interpreted "
f"as positional arguments."
)
raise ValueError(msg)
return sorted(input_variables)

View File

@@ -49,7 +49,13 @@ class StructuredPrompt(ChatPromptTemplate):
structured_output_kwargs: additional kwargs for structured output.
template_format: template format for the prompt.
"""
schema_ = schema_ or kwargs.pop("schema")
schema_ = schema_ or kwargs.pop("schema", None)
if not schema_:
err_msg = (
"Must pass in a non-empty structured output schema. Received: "
f"{schema_}"
)
raise ValueError(err_msg)
structured_output_kwargs = structured_output_kwargs or {}
for k in set(kwargs).difference(get_pydantic_field_names(self.__class__)):
structured_output_kwargs[k] = kwargs.pop(k)

View File

@@ -118,6 +118,8 @@ if TYPE_CHECKING:
Other = TypeVar("Other")
_RUNNABLE_GENERIC_NUM_ARGS = 2 # Input and Output
class Runnable(ABC, Generic[Input, Output]):
"""A unit of work that can be invoked, batched, streamed, transformed and composed.
@@ -309,7 +311,10 @@ class Runnable(ABC, Generic[Input, Output]):
for base in self.__class__.mro():
if hasattr(base, "__pydantic_generic_metadata__"):
metadata = base.__pydantic_generic_metadata__
if "args" in metadata and len(metadata["args"]) == 2:
if (
"args" in metadata
and len(metadata["args"]) == _RUNNABLE_GENERIC_NUM_ARGS
):
return metadata["args"][0]
# If we didn't find a Pydantic model in the parent classes,
@@ -317,7 +322,7 @@ class Runnable(ABC, Generic[Input, Output]):
# Runnables that are not pydantic models.
for cls in self.__class__.__orig_bases__: # type: ignore[attr-defined]
type_args = get_args(cls)
if type_args and len(type_args) == 2:
if type_args and len(type_args) == _RUNNABLE_GENERIC_NUM_ARGS:
return type_args[0]
msg = (
@@ -340,12 +345,15 @@ class Runnable(ABC, Generic[Input, Output]):
for base in self.__class__.mro():
if hasattr(base, "__pydantic_generic_metadata__"):
metadata = base.__pydantic_generic_metadata__
if "args" in metadata and len(metadata["args"]) == 2:
if (
"args" in metadata
and len(metadata["args"]) == _RUNNABLE_GENERIC_NUM_ARGS
):
return 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) == 2:
if type_args and len(type_args) == _RUNNABLE_GENERIC_NUM_ARGS:
return type_args[1]
msg = (
@@ -424,7 +432,7 @@ class Runnable(ABC, Generic[Input, Output]):
print(runnable.get_input_jsonschema())
```
!!! version-added "Added in version 0.3.0"
!!! version-added "Added in `langchain-core` 0.3.0"
"""
return self.get_input_schema(config).model_json_schema()
@@ -502,7 +510,7 @@ class Runnable(ABC, Generic[Input, Output]):
print(runnable.get_output_jsonschema())
```
!!! version-added "Added in version 0.3.0"
!!! version-added "Added in `langchain-core` 0.3.0"
"""
return self.get_output_schema(config).model_json_schema()
@@ -566,7 +574,7 @@ class Runnable(ABC, Generic[Input, Output]):
Returns:
A JSON schema that represents the config of the `Runnable`.
!!! version-added "Added in version 0.3.0"
!!! version-added "Added in `langchain-core` 0.3.0"
"""
return self.config_schema(include=include).model_json_schema()
@@ -699,51 +707,53 @@ class Runnable(ABC, Generic[Input, Output]):
def pick(self, keys: str | list[str]) -> RunnableSerializable[Any, Any]:
"""Pick keys from the output `dict` of this `Runnable`.
Pick a single key:
!!! example "Pick a single key"
```python
import json
```python
import json
from langchain_core.runnables import RunnableLambda, RunnableMap
from langchain_core.runnables import RunnableLambda, RunnableMap
as_str = RunnableLambda(str)
as_json = RunnableLambda(json.loads)
chain = RunnableMap(str=as_str, json=as_json)
as_str = RunnableLambda(str)
as_json = RunnableLambda(json.loads)
chain = RunnableMap(str=as_str, json=as_json)
chain.invoke("[1, 2, 3]")
# -> {"str": "[1, 2, 3]", "json": [1, 2, 3]}
chain.invoke("[1, 2, 3]")
# -> {"str": "[1, 2, 3]", "json": [1, 2, 3]}
json_only_chain = chain.pick("json")
json_only_chain.invoke("[1, 2, 3]")
# -> [1, 2, 3]
```
json_only_chain = chain.pick("json")
json_only_chain.invoke("[1, 2, 3]")
# -> [1, 2, 3]
```
Pick a list of keys:
!!! example "Pick a list of keys"
```python
from typing import Any
```python
from typing import Any
import json
import json
from langchain_core.runnables import RunnableLambda, RunnableMap
from langchain_core.runnables import RunnableLambda, RunnableMap
as_str = RunnableLambda(str)
as_json = RunnableLambda(json.loads)
as_str = RunnableLambda(str)
as_json = RunnableLambda(json.loads)
def as_bytes(x: Any) -> bytes:
return bytes(x, "utf-8")
def as_bytes(x: Any) -> bytes:
return bytes(x, "utf-8")
chain = RunnableMap(str=as_str, json=as_json, bytes=RunnableLambda(as_bytes))
chain = RunnableMap(
str=as_str, json=as_json, bytes=RunnableLambda(as_bytes)
)
chain.invoke("[1, 2, 3]")
# -> {"str": "[1, 2, 3]", "json": [1, 2, 3], "bytes": b"[1, 2, 3]"}
chain.invoke("[1, 2, 3]")
# -> {"str": "[1, 2, 3]", "json": [1, 2, 3], "bytes": b"[1, 2, 3]"}
json_and_bytes_chain = chain.pick(["json", "bytes"])
json_and_bytes_chain.invoke("[1, 2, 3]")
# -> {"json": [1, 2, 3], "bytes": b"[1, 2, 3]"}
```
json_and_bytes_chain = chain.pick(["json", "bytes"])
json_and_bytes_chain.invoke("[1, 2, 3]")
# -> {"json": [1, 2, 3], "bytes": b"[1, 2, 3]"}
```
Args:
keys: A key or list of keys to pick from the output dict.
@@ -766,7 +776,7 @@ class Runnable(ABC, Generic[Input, Output]):
"""Assigns new fields to the `dict` output of this `Runnable`.
```python
from langchain_community.llms.fake import FakeStreamingListLLM
from langchain_core.language_models.fake import FakeStreamingListLLM
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import SystemMessagePromptTemplate
from langchain_core.runnables import Runnable
@@ -818,10 +828,12 @@ class Runnable(ABC, Generic[Input, Output]):
Args:
input: The input to the `Runnable`.
config: A config to use when invoking the `Runnable`.
The config supports standard keys like `'tags'`, `'metadata'` for
tracing purposes, `'max_concurrency'` for controlling how much work to
do in parallel, and other keys. Please refer to the `RunnableConfig`
for more details.
do in parallel, and other keys.
Please refer to `RunnableConfig` for more details.
Returns:
The output of the `Runnable`.
@@ -838,10 +850,12 @@ class Runnable(ABC, Generic[Input, Output]):
Args:
input: The input to the `Runnable`.
config: A config to use when invoking the `Runnable`.
The config supports standard keys like `'tags'`, `'metadata'` for
tracing purposes, `'max_concurrency'` for controlling how much work to
do in parallel, and other keys. Please refer to the `RunnableConfig`
for more details.
do in parallel, and other keys.
Please refer to `RunnableConfig` for more details.
Returns:
The output of the `Runnable`.
@@ -868,8 +882,9 @@ class Runnable(ABC, Generic[Input, Output]):
config: A config to use when invoking the `Runnable`. The config supports
standard keys like `'tags'`, `'metadata'` for
tracing purposes, `'max_concurrency'` for controlling how much work
to do in parallel, and other keys. Please refer to the
`RunnableConfig` for more details.
to do in parallel, and other keys.
Please refer to `RunnableConfig` for more details.
return_exceptions: Whether to return exceptions instead of raising them.
**kwargs: Additional keyword arguments to pass to the `Runnable`.
@@ -932,10 +947,12 @@ class Runnable(ABC, Generic[Input, Output]):
Args:
inputs: A list of inputs to the `Runnable`.
config: A config to use when invoking the `Runnable`.
The config supports standard keys like `'tags'`, `'metadata'` for
tracing purposes, `'max_concurrency'` for controlling how much work to
do in parallel, and other keys. Please refer to the `RunnableConfig`
for more details.
do in parallel, and other keys.
Please refer to `RunnableConfig` for more details.
return_exceptions: Whether to return exceptions instead of raising them.
**kwargs: Additional keyword arguments to pass to the `Runnable`.
@@ -998,10 +1015,12 @@ class Runnable(ABC, Generic[Input, Output]):
Args:
inputs: A list of inputs to the `Runnable`.
config: A config to use when invoking the `Runnable`.
The config supports standard keys like `'tags'`, `'metadata'` for
tracing purposes, `'max_concurrency'` for controlling how much work to
do in parallel, and other keys. Please refer to the `RunnableConfig`
for more details.
do in parallel, and other keys.
Please refer to `RunnableConfig` for more details.
return_exceptions: Whether to return exceptions instead of raising them.
**kwargs: Additional keyword arguments to pass to the `Runnable`.
@@ -1061,10 +1080,12 @@ class Runnable(ABC, Generic[Input, Output]):
Args:
inputs: A list of inputs to the `Runnable`.
config: A config to use when invoking the `Runnable`.
The config supports standard keys like `'tags'`, `'metadata'` for
tracing purposes, `'max_concurrency'` for controlling how much work to
do in parallel, and other keys. Please refer to the `RunnableConfig`
for more details.
do in parallel, and other keys.
Please refer to `RunnableConfig` for more details.
return_exceptions: Whether to return exceptions instead of raising them.
**kwargs: Additional keyword arguments to pass to the `Runnable`.
@@ -1353,48 +1374,50 @@ class Runnable(ABC, Generic[Input, Output]):
).with_config({"run_name": "my_template", "tags": ["my_template"]})
```
For instance:
!!! example
```python
from langchain_core.runnables import RunnableLambda
```python
from langchain_core.runnables import RunnableLambda
async def reverse(s: str) -> str:
return s[::-1]
async def reverse(s: str) -> str:
return s[::-1]
chain = RunnableLambda(func=reverse)
chain = RunnableLambda(func=reverse)
events = [event async for event in chain.astream_events("hello", version="v2")]
events = [
event async for event in chain.astream_events("hello", version="v2")
]
# Will produce the following events
# (run_id, and parent_ids has been omitted for brevity):
[
{
"data": {"input": "hello"},
"event": "on_chain_start",
"metadata": {},
"name": "reverse",
"tags": [],
},
{
"data": {"chunk": "olleh"},
"event": "on_chain_stream",
"metadata": {},
"name": "reverse",
"tags": [],
},
{
"data": {"output": "olleh"},
"event": "on_chain_end",
"metadata": {},
"name": "reverse",
"tags": [],
},
]
```
# Will produce the following events
# (run_id, and parent_ids has been omitted for brevity):
[
{
"data": {"input": "hello"},
"event": "on_chain_start",
"metadata": {},
"name": "reverse",
"tags": [],
},
{
"data": {"chunk": "olleh"},
"event": "on_chain_stream",
"metadata": {},
"name": "reverse",
"tags": [],
},
{
"data": {"output": "olleh"},
"event": "on_chain_end",
"metadata": {},
"name": "reverse",
"tags": [],
},
]
```
```python title="Example: Dispatch Custom Event"
```python title="Dispatch custom event"
from langchain_core.callbacks.manager import (
adispatch_custom_event,
)
@@ -1428,10 +1451,13 @@ class Runnable(ABC, Generic[Input, Output]):
Args:
input: The input to the `Runnable`.
config: The config to use for the `Runnable`.
version: The version of the schema to use either `'v2'` or `'v1'`.
version: The version of the schema to use, either `'v2'` or `'v1'`.
Users should use `'v2'`.
`'v1'` is for backwards compatibility and will be deprecated
in `0.4.0`.
No default will be assigned until the API is stabilized.
custom events will only be surfaced in `'v2'`.
include_names: Only include events from `Runnable` objects with matching names.
@@ -1441,6 +1467,7 @@ class Runnable(ABC, Generic[Input, Output]):
exclude_types: Exclude events from `Runnable` objects with matching types.
exclude_tags: Exclude events from `Runnable` objects with matching tags.
**kwargs: Additional keyword arguments to pass to the `Runnable`.
These will be passed to `astream_log` as this implementation
of `astream_events` is built on top of `astream_log`.
@@ -1742,46 +1769,52 @@ class Runnable(ABC, Generic[Input, Output]):
import time
import asyncio
def format_t(timestamp: float) -> str:
return datetime.fromtimestamp(timestamp, tz=timezone.utc).isoformat()
async def test_runnable(time_to_sleep: int):
print(f"Runnable[{time_to_sleep}s]: starts at {format_t(time.time())}")
await asyncio.sleep(time_to_sleep)
print(f"Runnable[{time_to_sleep}s]: ends at {format_t(time.time())}")
async def fn_start(run_obj: Runnable):
print(f"on start callback starts at {format_t(time.time())}")
await asyncio.sleep(3)
print(f"on start callback ends at {format_t(time.time())}")
async def fn_end(run_obj: Runnable):
print(f"on end callback starts at {format_t(time.time())}")
await asyncio.sleep(2)
print(f"on end callback ends at {format_t(time.time())}")
runnable = RunnableLambda(test_runnable).with_alisteners(
on_start=fn_start,
on_end=fn_end
on_start=fn_start, on_end=fn_end
)
async def concurrent_runs():
await asyncio.gather(runnable.ainvoke(2), runnable.ainvoke(3))
asyncio.run(concurrent_runs())
Result:
on start callback starts at 2025-03-01T07:05:22.875378+00:00
on start callback starts at 2025-03-01T07:05:22.875495+00:00
on start callback ends at 2025-03-01T07:05:25.878862+00:00
on start callback ends at 2025-03-01T07:05:25.878947+00:00
Runnable[2s]: starts at 2025-03-01T07:05:25.879392+00:00
Runnable[3s]: starts at 2025-03-01T07:05:25.879804+00:00
Runnable[2s]: ends at 2025-03-01T07:05:27.881998+00:00
on end callback starts at 2025-03-01T07:05:27.882360+00:00
Runnable[3s]: ends at 2025-03-01T07:05:28.881737+00:00
on end callback starts at 2025-03-01T07:05:28.882428+00:00
on end callback ends at 2025-03-01T07:05:29.883893+00:00
on end callback ends at 2025-03-01T07:05:30.884831+00:00
asyncio.run(concurrent_runs())
# Result:
# on start callback starts at 2025-03-01T07:05:22.875378+00:00
# on start callback starts at 2025-03-01T07:05:22.875495+00:00
# on start callback ends at 2025-03-01T07:05:25.878862+00:00
# on start callback ends at 2025-03-01T07:05:25.878947+00:00
# Runnable[2s]: starts at 2025-03-01T07:05:25.879392+00:00
# Runnable[3s]: starts at 2025-03-01T07:05:25.879804+00:00
# Runnable[2s]: ends at 2025-03-01T07:05:27.881998+00:00
# on end callback starts at 2025-03-01T07:05:27.882360+00:00
# Runnable[3s]: ends at 2025-03-01T07:05:28.881737+00:00
# on end callback starts at 2025-03-01T07:05:28.882428+00:00
# on end callback ends at 2025-03-01T07:05:29.883893+00:00
# on end callback ends at 2025-03-01T07:05:30.884831+00:00
```
"""
return RunnableBinding(
@@ -1843,7 +1876,7 @@ class Runnable(ABC, Generic[Input, Output]):
`exp_base`, and `jitter` (all `float` values).
Returns:
A new Runnable that retries the original Runnable on exceptions.
A new `Runnable` that retries the original `Runnable` on exceptions.
Example:
```python
@@ -1927,7 +1960,9 @@ class Runnable(ABC, Generic[Input, Output]):
exceptions_to_handle: A tuple of exception types to handle.
exception_key: If `string` is specified then handled exceptions will be
passed to fallbacks as part of the input under the specified key.
If `None`, exceptions will not be passed to fallbacks.
If used, the base `Runnable` and its fallbacks must accept a
dictionary as input.
@@ -1963,7 +1998,9 @@ class Runnable(ABC, Generic[Input, Output]):
exceptions_to_handle: A tuple of exception types to handle.
exception_key: If `string` is specified then handled exceptions will be
passed to fallbacks as part of the input under the specified key.
If `None`, exceptions will not be passed to fallbacks.
If used, the base `Runnable` and its fallbacks must accept a
dictionary as input.
@@ -2429,10 +2466,14 @@ class Runnable(ABC, Generic[Input, Output]):
`as_tool` will instantiate a `BaseTool` with a name, description, and
`args_schema` from a `Runnable`. Where possible, schemas are inferred
from `runnable.get_input_schema`. Alternatively (e.g., if the
`Runnable` takes a dict as input and the specific dict keys are not typed),
the schema can be specified directly with `args_schema`. You can also
pass `arg_types` to just specify the required arguments and their types.
from `runnable.get_input_schema`.
Alternatively (e.g., if the `Runnable` takes a dict as input and the specific
`dict` keys are not typed), the schema can be specified directly with
`args_schema`.
You can also pass `arg_types` to just specify the required arguments and their
types.
Args:
args_schema: The schema for the tool.
@@ -2443,82 +2484,82 @@ class Runnable(ABC, Generic[Input, Output]):
Returns:
A `BaseTool` instance.
Typed dict input:
!!! example "`TypedDict` input"
```python
from typing_extensions import TypedDict
from langchain_core.runnables import RunnableLambda
```python
from typing_extensions import TypedDict
from langchain_core.runnables import RunnableLambda
class Args(TypedDict):
a: int
b: list[int]
class Args(TypedDict):
a: int
b: list[int]
def f(x: Args) -> str:
return str(x["a"] * max(x["b"]))
def f(x: Args) -> str:
return str(x["a"] * max(x["b"]))
runnable = RunnableLambda(f)
as_tool = runnable.as_tool()
as_tool.invoke({"a": 3, "b": [1, 2]})
```
runnable = RunnableLambda(f)
as_tool = runnable.as_tool()
as_tool.invoke({"a": 3, "b": [1, 2]})
```
`dict` input, specifying schema via `args_schema`:
!!! example "`dict` input, specifying schema via `args_schema`"
```python
from typing import Any
from pydantic import BaseModel, Field
from langchain_core.runnables import RunnableLambda
```python
from typing import Any
from pydantic import BaseModel, Field
from langchain_core.runnables import RunnableLambda
def f(x: dict[str, Any]) -> str:
return str(x["a"] * max(x["b"]))
def f(x: dict[str, Any]) -> str:
return str(x["a"] * max(x["b"]))
class FSchema(BaseModel):
\"\"\"Apply a function to an integer and list of integers.\"\"\"
class FSchema(BaseModel):
\"\"\"Apply a function to an integer and list of integers.\"\"\"
a: int = Field(..., description="Integer")
b: list[int] = Field(..., description="List of ints")
a: int = Field(..., description="Integer")
b: list[int] = Field(..., description="List of ints")
runnable = RunnableLambda(f)
as_tool = runnable.as_tool(FSchema)
as_tool.invoke({"a": 3, "b": [1, 2]})
```
runnable = RunnableLambda(f)
as_tool = runnable.as_tool(FSchema)
as_tool.invoke({"a": 3, "b": [1, 2]})
```
`dict` input, specifying schema via `arg_types`:
!!! example "`dict` input, specifying schema via `arg_types`"
```python
from typing import Any
from langchain_core.runnables import RunnableLambda
```python
from typing import Any
from langchain_core.runnables import RunnableLambda
def f(x: dict[str, Any]) -> str:
return str(x["a"] * max(x["b"]))
def f(x: dict[str, Any]) -> str:
return str(x["a"] * max(x["b"]))
runnable = RunnableLambda(f)
as_tool = runnable.as_tool(arg_types={"a": int, "b": list[int]})
as_tool.invoke({"a": 3, "b": [1, 2]})
```
runnable = RunnableLambda(f)
as_tool = runnable.as_tool(arg_types={"a": int, "b": list[int]})
as_tool.invoke({"a": 3, "b": [1, 2]})
```
String input:
!!! example "`str` input"
```python
from langchain_core.runnables import RunnableLambda
```python
from langchain_core.runnables import RunnableLambda
def f(x: str) -> str:
return x + "a"
def f(x: str) -> str:
return x + "a"
def g(x: str) -> str:
return x + "z"
def g(x: str) -> str:
return x + "z"
runnable = RunnableLambda(f) | g
as_tool = runnable.as_tool()
as_tool.invoke("b")
```
runnable = RunnableLambda(f) | g
as_tool = runnable.as_tool()
as_tool.invoke("b")
```
"""
# Avoid circular import
from langchain_core.tools import convert_runnable_to_tool # noqa: PLC0415
@@ -2570,29 +2611,33 @@ class RunnableSerializable(Serializable, Runnable[Input, Output]):
Returns:
A new `Runnable` with the fields configured.
```python
from langchain_core.runnables import ConfigurableField
from langchain_openai import ChatOpenAI
!!! example
model = ChatOpenAI(max_tokens=20).configurable_fields(
max_tokens=ConfigurableField(
id="output_token_number",
name="Max tokens in the output",
description="The maximum number of tokens in the output",
```python
from langchain_core.runnables import ConfigurableField
from langchain_openai import ChatOpenAI
model = ChatOpenAI(max_tokens=20).configurable_fields(
max_tokens=ConfigurableField(
id="output_token_number",
name="Max tokens in the output",
description="The maximum number of tokens in the output",
)
)
)
# max_tokens = 20
print("max_tokens_20: ", model.invoke("tell me something about chess").content)
# max_tokens = 20
print(
"max_tokens_20: ", model.invoke("tell me something about chess").content
)
# max_tokens = 200
print(
"max_tokens_200: ",
model.with_config(configurable={"output_token_number": 200})
.invoke("tell me something about chess")
.content,
)
```
# max_tokens = 200
print(
"max_tokens_200: ",
model.with_config(configurable={"output_token_number": 200})
.invoke("tell me something about chess")
.content,
)
```
"""
# Import locally to prevent circular import
from langchain_core.runnables.configurable import ( # noqa: PLC0415
@@ -2631,29 +2676,31 @@ class RunnableSerializable(Serializable, Runnable[Input, Output]):
Returns:
A new `Runnable` with the alternatives configured.
```python
from langchain_anthropic import ChatAnthropic
from langchain_core.runnables.utils import ConfigurableField
from langchain_openai import ChatOpenAI
!!! example
model = ChatAnthropic(
model_name="claude-sonnet-4-5-20250929"
).configurable_alternatives(
ConfigurableField(id="llm"),
default_key="anthropic",
openai=ChatOpenAI(),
)
```python
from langchain_anthropic import ChatAnthropic
from langchain_core.runnables.utils import ConfigurableField
from langchain_openai import ChatOpenAI
# uses the default model ChatAnthropic
print(model.invoke("which organization created you?").content)
model = ChatAnthropic(
model_name="claude-sonnet-4-5-20250929"
).configurable_alternatives(
ConfigurableField(id="llm"),
default_key="anthropic",
openai=ChatOpenAI(),
)
# uses ChatOpenAI
print(
model.with_config(configurable={"llm": "openai"})
.invoke("which organization created you?")
.content
)
```
# uses the default model ChatAnthropic
print(model.invoke("which organization created you?").content)
# uses ChatOpenAI
print(
model.with_config(configurable={"llm": "openai"})
.invoke("which organization created you?")
.content
)
```
"""
# Import locally to prevent circular import
from langchain_core.runnables.configurable import ( # noqa: PLC0415
@@ -2750,6 +2797,9 @@ def _seq_output_schema(
return last.get_output_schema(config)
_RUNNABLE_SEQUENCE_MIN_STEPS = 2
class RunnableSequence(RunnableSerializable[Input, Output]):
"""Sequence of `Runnable` objects, where the output of one is the input of the next.
@@ -2859,7 +2909,7 @@ class RunnableSequence(RunnableSerializable[Input, Output]):
name: The name of the `Runnable`.
first: The first `Runnable` in the sequence.
middle: The middle `Runnable` objects in the sequence.
last: The last Runnable in the sequence.
last: The last `Runnable` in the sequence.
Raises:
ValueError: If the sequence has less than 2 steps.
@@ -2872,8 +2922,11 @@ class RunnableSequence(RunnableSerializable[Input, Output]):
steps_flat.extend(step.steps)
else:
steps_flat.append(coerce_to_runnable(step))
if len(steps_flat) < 2:
msg = f"RunnableSequence must have at least 2 steps, got {len(steps_flat)}"
if len(steps_flat) < _RUNNABLE_SEQUENCE_MIN_STEPS:
msg = (
f"RunnableSequence must have at least {_RUNNABLE_SEQUENCE_MIN_STEPS} "
f"steps, got {len(steps_flat)}"
)
raise ValueError(msg)
super().__init__(
first=steps_flat[0],
@@ -2904,7 +2957,7 @@ class RunnableSequence(RunnableSerializable[Input, Output]):
@classmethod
@override
def is_lc_serializable(cls) -> bool:
"""Return True as this class is serializable."""
"""Return `True` as this class is serializable."""
return True
model_config = ConfigDict(
@@ -3610,7 +3663,7 @@ class RunnableParallel(RunnableSerializable[Input, dict[str, Any]]):
@classmethod
@override
def is_lc_serializable(cls) -> bool:
"""Return True as this class is serializable."""
"""Return `True` as this class is serializable."""
return True
@classmethod
@@ -3668,6 +3721,12 @@ class RunnableParallel(RunnableSerializable[Input, dict[str, Any]]):
== "object"
for s in self.steps__.values()
):
for step in self.steps__.values():
fields = step.get_input_schema(config).model_fields
root_field = fields.get("root")
if root_field is not None and root_field.annotation != Any:
return super().get_input_schema(config)
# This is correct, but pydantic typings/mypy don't think so.
return create_model_v2(
self.get_name("Input"),
@@ -4477,7 +4536,7 @@ class RunnableLambda(Runnable[Input, Output]):
# on itemgetter objects, so we have to parse the repr
items = str(func).replace("operator.itemgetter(", "")[:-1].split(", ")
if all(
item[0] == "'" and item[-1] == "'" and len(item) > 2 for item in items
item[0] == "'" and item[-1] == "'" and item != "''" for item in items
):
fields = {item[1:-1]: (Any, ...) for item in items}
# It's a dict, lol
@@ -5139,7 +5198,7 @@ class RunnableEachBase(RunnableSerializable[list[Input], list[Output]]):
@classmethod
@override
def is_lc_serializable(cls) -> bool:
"""Return True as this class is serializable."""
"""Return `True` as this class is serializable."""
return True
@classmethod
@@ -5322,7 +5381,7 @@ class RunnableEach(RunnableEachBase[Input, Output]):
class RunnableBindingBase(RunnableSerializable[Input, Output]): # type: ignore[no-redef]
"""`Runnable` that delegates calls to another `Runnable` with a set of kwargs.
"""`Runnable` that delegates calls to another `Runnable` with a set of `**kwargs`.
Use only if creating a new `RunnableBinding` subclass with different `__init__`
args.
@@ -5462,7 +5521,7 @@ class RunnableBindingBase(RunnableSerializable[Input, Output]): # type: ignore[
@classmethod
@override
def is_lc_serializable(cls) -> bool:
"""Return True as this class is serializable."""
"""Return `True` as this class is serializable."""
return True
@classmethod
@@ -5752,7 +5811,7 @@ class RunnableBinding(RunnableBindingBase[Input, Output]): # type: ignore[no-re
```python
# Create a Runnable binding that invokes the chat model with the
# additional kwarg `stop=['-']` when running it.
from langchain_community.chat_models import ChatOpenAI
from langchain_openai import ChatOpenAI
model = ChatOpenAI()
model.invoke('Say "Parrot-MAGIC"', stop=["-"]) # Should return `Parrot`

View File

@@ -36,11 +36,13 @@ from langchain_core.runnables.utils import (
get_unique_config_specs,
)
_MIN_BRANCHES = 2
class RunnableBranch(RunnableSerializable[Input, Output]):
"""Runnable that selects which branch to run based on a condition.
"""`Runnable` that selects which branch to run based on a condition.
The Runnable is initialized with a list of `(condition, Runnable)` pairs and
The `Runnable` is initialized with a list of `(condition, Runnable)` pairs and
a default branch.
When operating on an input, the first condition that evaluates to True is
@@ -86,12 +88,12 @@ class RunnableBranch(RunnableSerializable[Input, Output]):
Defaults a `Runnable` to run if no condition is met.
Raises:
ValueError: If the number of branches is less than 2.
ValueError: If the number of branches is less than `2`.
TypeError: If the default branch is not `Runnable`, `Callable` or `Mapping`.
TypeError: If a branch is not a tuple or list.
ValueError: If a branch is not of length 2.
TypeError: If a branch is not a `tuple` or `list`.
ValueError: If a branch is not of length `2`.
"""
if len(branches) < 2:
if len(branches) < _MIN_BRANCHES:
msg = "RunnableBranch requires at least two branches"
raise ValueError(msg)
@@ -118,7 +120,7 @@ class RunnableBranch(RunnableSerializable[Input, Output]):
)
raise TypeError(msg)
if len(branch) != 2:
if len(branch) != _MIN_BRANCHES:
msg = (
f"RunnableBranch branches must be "
f"tuples or lists of length 2, not {len(branch)}"
@@ -140,7 +142,7 @@ class RunnableBranch(RunnableSerializable[Input, Output]):
@classmethod
def is_lc_serializable(cls) -> bool:
"""Return True as this class is serializable."""
"""Return `True` as this class is serializable."""
return True
@classmethod
@@ -187,12 +189,12 @@ class RunnableBranch(RunnableSerializable[Input, Output]):
def invoke(
self, input: Input, config: RunnableConfig | None = None, **kwargs: Any
) -> Output:
"""First evaluates the condition, then delegate to true or false branch.
"""First evaluates the condition, then delegate to `True` or `False` branch.
Args:
input: The input to the Runnable.
config: The configuration for the Runnable.
**kwargs: Additional keyword arguments to pass to the Runnable.
input: The input to the `Runnable`.
config: The configuration for the `Runnable`.
**kwargs: Additional keyword arguments to pass to the `Runnable`.
Returns:
The output of the branch that was run.
@@ -297,12 +299,12 @@ class RunnableBranch(RunnableSerializable[Input, Output]):
config: RunnableConfig | None = None,
**kwargs: Any | None,
) -> Iterator[Output]:
"""First evaluates the condition, then delegate to true or false branch.
"""First evaluates the condition, then delegate to `True` or `False` branch.
Args:
input: The input to the Runnable.
config: The configuration for the Runnable.
**kwargs: Additional keyword arguments to pass to the Runnable.
input: The input to the `Runnable`.
config: The configuration for the `Runnable`.
**kwargs: Additional keyword arguments to pass to the `Runnable`.
Yields:
The output of the branch that was run.
@@ -381,12 +383,12 @@ class RunnableBranch(RunnableSerializable[Input, Output]):
config: RunnableConfig | None = None,
**kwargs: Any | None,
) -> AsyncIterator[Output]:
"""First evaluates the condition, then delegate to true or false branch.
"""First evaluates the condition, then delegate to `True` or `False` branch.
Args:
input: The input to the Runnable.
config: The configuration for the Runnable.
**kwargs: Additional keyword arguments to pass to the Runnable.
input: The input to the `Runnable`.
config: The configuration for the `Runnable`.
**kwargs: Additional keyword arguments to pass to the `Runnable`.
Yields:
The output of the branch that was run.

View File

@@ -47,54 +47,59 @@ class EmptyDict(TypedDict, total=False):
class RunnableConfig(TypedDict, total=False):
"""Configuration for a Runnable."""
"""Configuration for a `Runnable`.
See the [reference docs](https://reference.langchain.com/python/langchain_core/runnables/#langchain_core.runnables.RunnableConfig)
for more details.
"""
tags: list[str]
"""
Tags for this call and any sub-calls (eg. a Chain calling an LLM).
"""Tags for this call and any sub-calls (e.g. a Chain calling an LLM).
You can use these to filter calls.
"""
metadata: dict[str, Any]
"""
Metadata for this call and any sub-calls (eg. a Chain calling an LLM).
"""Metadata for this call and any sub-calls (e.g. a Chain calling an LLM).
Keys should be strings, values should be JSON-serializable.
"""
callbacks: Callbacks
"""
Callbacks for this call and any sub-calls (eg. a Chain calling an LLM).
"""Callbacks for this call and any sub-calls (e.g. a Chain calling an LLM).
Tags are passed to all callbacks, metadata is passed to handle*Start callbacks.
"""
run_name: str
"""
Name for the tracer run for this call. Defaults to the name of the class.
"""
"""Name for the tracer run for this call.
Defaults to the name of the class."""
max_concurrency: int | None
"""
Maximum number of parallel calls to make. If not provided, defaults to
`ThreadPoolExecutor`'s default.
"""Maximum number of parallel calls to make.
If not provided, defaults to `ThreadPoolExecutor`'s default.
"""
recursion_limit: int
"""
Maximum number of times a call can recurse. If not provided, defaults to `25`.
"""Maximum number of times a call can recurse.
If not provided, defaults to `25`.
"""
configurable: dict[str, Any]
"""
Runtime values for attributes previously made configurable on this `Runnable`,
"""Runtime values for attributes previously made configurable on this `Runnable`,
or sub-Runnables, through `configurable_fields` or `configurable_alternatives`.
Check `output_schema` for a description of the attributes that have been made
configurable.
"""
run_id: uuid.UUID | None
"""
Unique identifier for the tracer run for this call. If not provided, a new UUID
will be generated.
"""Unique identifier for the tracer run for this call.
If not provided, a new UUID will be generated.
"""

View File

@@ -1,4 +1,4 @@
"""Runnables that can be dynamically configured."""
"""`Runnable` objects that can be dynamically configured."""
from __future__ import annotations
@@ -47,14 +47,14 @@ if TYPE_CHECKING:
class DynamicRunnable(RunnableSerializable[Input, Output]):
"""Serializable Runnable that can be dynamically configured.
"""Serializable `Runnable` that can be dynamically configured.
A DynamicRunnable should be initiated using the `configurable_fields` or
`configurable_alternatives` method of a Runnable.
A `DynamicRunnable` should be initiated using the `configurable_fields` or
`configurable_alternatives` method of a `Runnable`.
"""
default: RunnableSerializable[Input, Output]
"""The default Runnable to use."""
"""The default `Runnable` to use."""
config: RunnableConfig | None = None
"""The configuration to use."""
@@ -66,7 +66,7 @@ class DynamicRunnable(RunnableSerializable[Input, Output]):
@classmethod
@override
def is_lc_serializable(cls) -> bool:
"""Return True as this class is serializable."""
"""Return `True` as this class is serializable."""
return True
@classmethod
@@ -120,13 +120,13 @@ class DynamicRunnable(RunnableSerializable[Input, Output]):
def prepare(
self, config: RunnableConfig | None = None
) -> tuple[Runnable[Input, Output], RunnableConfig]:
"""Prepare the Runnable for invocation.
"""Prepare the `Runnable` for invocation.
Args:
config: The configuration to use.
Returns:
The prepared Runnable and configuration.
The prepared `Runnable` and configuration.
"""
runnable: Runnable[Input, Output] = self
while isinstance(runnable, DynamicRunnable):
@@ -316,12 +316,12 @@ class DynamicRunnable(RunnableSerializable[Input, Output]):
class RunnableConfigurableFields(DynamicRunnable[Input, Output]):
"""Runnable that can be dynamically configured.
"""`Runnable` that can be dynamically configured.
A RunnableConfigurableFields should be initiated using the
`configurable_fields` method of a Runnable.
A `RunnableConfigurableFields` should be initiated using the
`configurable_fields` method of a `Runnable`.
Here is an example of using a RunnableConfigurableFields with LLMs:
Here is an example of using a `RunnableConfigurableFields` with LLMs:
```python
from langchain_core.prompts import PromptTemplate
@@ -348,7 +348,7 @@ class RunnableConfigurableFields(DynamicRunnable[Input, Output]):
chain.invoke({"x": 0}, config={"configurable": {"temperature": 0.9}})
```
Here is an example of using a RunnableConfigurableFields with HubRunnables:
Here is an example of using a `RunnableConfigurableFields` with `HubRunnables`:
```python
from langchain_core.prompts import PromptTemplate
@@ -380,7 +380,7 @@ class RunnableConfigurableFields(DynamicRunnable[Input, Output]):
@property
def config_specs(self) -> list[ConfigurableFieldSpec]:
"""Get the configuration specs for the RunnableConfigurableFields.
"""Get the configuration specs for the `RunnableConfigurableFields`.
Returns:
The configuration specs.
@@ -473,10 +473,10 @@ _enums_for_spec_lock = threading.Lock()
class RunnableConfigurableAlternatives(DynamicRunnable[Input, Output]):
"""Runnable that can be dynamically configured.
"""`Runnable` that can be dynamically configured.
A `RunnableConfigurableAlternatives` should be initiated using the
`configurable_alternatives` method of a Runnable or can be
`configurable_alternatives` method of a `Runnable` or can be
initiated directly as well.
Here is an example of using a `RunnableConfigurableAlternatives` that uses
@@ -531,7 +531,7 @@ class RunnableConfigurableAlternatives(DynamicRunnable[Input, Output]):
"""
which: ConfigurableField
"""The ConfigurableField to use to choose between alternatives."""
"""The `ConfigurableField` to use to choose between alternatives."""
alternatives: dict[
str,
@@ -544,8 +544,9 @@ class RunnableConfigurableAlternatives(DynamicRunnable[Input, Output]):
prefix_keys: bool
"""Whether to prefix configurable fields of each alternative with a namespace
of the form <which.id>==<alternative_key>, eg. a key named "temperature" used by
the alternative named "gpt3" becomes "model==gpt3/temperature"."""
of the form <which.id>==<alternative_key>, e.g. a key named "temperature" used by
the alternative named "gpt3" becomes "model==gpt3/temperature".
"""
@property
@override
@@ -638,24 +639,24 @@ class RunnableConfigurableAlternatives(DynamicRunnable[Input, Output]):
def _strremoveprefix(s: str, prefix: str) -> str:
"""str.removeprefix() is only available in Python 3.9+."""
"""`str.removeprefix()` is only available in Python 3.9+."""
return s.replace(prefix, "", 1) if s.startswith(prefix) else s
def prefix_config_spec(
spec: ConfigurableFieldSpec, prefix: str
) -> ConfigurableFieldSpec:
"""Prefix the id of a ConfigurableFieldSpec.
"""Prefix the id of a `ConfigurableFieldSpec`.
This is useful when a RunnableConfigurableAlternatives is used as a
ConfigurableField of another RunnableConfigurableAlternatives.
This is useful when a `RunnableConfigurableAlternatives` is used as a
`ConfigurableField` of another `RunnableConfigurableAlternatives`.
Args:
spec: The ConfigurableFieldSpec to prefix.
spec: The `ConfigurableFieldSpec` to prefix.
prefix: The prefix to add.
Returns:
The prefixed ConfigurableFieldSpec.
The prefixed `ConfigurableFieldSpec`.
"""
return (
ConfigurableFieldSpec(
@@ -677,15 +678,15 @@ def make_options_spec(
) -> ConfigurableFieldSpec:
"""Make options spec.
Make a ConfigurableFieldSpec for a ConfigurableFieldSingleOption or
ConfigurableFieldMultiOption.
Make a `ConfigurableFieldSpec` for a `ConfigurableFieldSingleOption` or
`ConfigurableFieldMultiOption`.
Args:
spec: The ConfigurableFieldSingleOption or ConfigurableFieldMultiOption.
spec: The `ConfigurableFieldSingleOption` or `ConfigurableFieldMultiOption`.
description: The description to use if the spec does not have one.
Returns:
The ConfigurableFieldSpec.
The `ConfigurableFieldSpec`.
"""
with _enums_for_spec_lock:
if enum := _enums_for_spec.get(spec):

View File

@@ -35,20 +35,20 @@ if TYPE_CHECKING:
class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
"""Runnable that can fallback to other Runnables if it fails.
"""`Runnable` that can fallback to other `Runnable`s if it fails.
External APIs (e.g., APIs for a language model) may at times experience
degraded performance or even downtime.
In these cases, it can be useful to have a fallback Runnable that can be
used in place of the original Runnable (e.g., fallback to another LLM provider).
In these cases, it can be useful to have a fallback `Runnable` that can be
used in place of the original `Runnable` (e.g., fallback to another LLM provider).
Fallbacks can be defined at the level of a single Runnable, or at the level
of a chain of Runnables. Fallbacks are tried in order until one succeeds or
Fallbacks can be defined at the level of a single `Runnable`, or at the level
of a chain of `Runnable`s. Fallbacks are tried in order until one succeeds or
all fail.
While you can instantiate a `RunnableWithFallbacks` directly, it is usually
more convenient to use the `with_fallbacks` method on a Runnable.
more convenient to use the `with_fallbacks` method on a `Runnable`.
Example:
```python
@@ -87,7 +87,7 @@ class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
"""
runnable: Runnable[Input, Output]
"""The Runnable to run first."""
"""The `Runnable` to run first."""
fallbacks: Sequence[Runnable[Input, Output]]
"""A sequence of fallbacks to try."""
exceptions_to_handle: tuple[type[BaseException], ...] = (Exception,)
@@ -97,9 +97,12 @@ class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
"""
exception_key: str | None = None
"""If `string` is specified then handled exceptions will be passed to fallbacks as
part of the input under the specified key. If `None`, exceptions
will not be passed to fallbacks. If used, the base Runnable and its fallbacks
must accept a dictionary as input."""
part of the input under the specified key.
If `None`, exceptions will not be passed to fallbacks.
If used, the base `Runnable` and its fallbacks must accept a dictionary as input.
"""
model_config = ConfigDict(
arbitrary_types_allowed=True,
@@ -137,7 +140,7 @@ class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
@classmethod
@override
def is_lc_serializable(cls) -> bool:
"""Return True as this class is serializable."""
"""Return `True` as this class is serializable."""
return True
@classmethod
@@ -152,10 +155,10 @@ class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
@property
def runnables(self) -> Iterator[Runnable[Input, Output]]:
"""Iterator over the Runnable and its fallbacks.
"""Iterator over the `Runnable` and its fallbacks.
Yields:
The Runnable then its fallbacks.
The `Runnable` then its fallbacks.
"""
yield self.runnable
yield from self.fallbacks
@@ -589,14 +592,14 @@ class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
await run_manager.on_chain_end(output)
def __getattr__(self, name: str) -> Any:
"""Get an attribute from the wrapped Runnable and its fallbacks.
"""Get an attribute from the wrapped `Runnable` and its fallbacks.
Returns:
If the attribute is anything other than a method that outputs a Runnable,
returns getattr(self.runnable, name). If the attribute is a method that
does return a new Runnable (e.g. model.bind_tools([...]) outputs a new
RunnableBinding) then self.runnable and each of the runnables in
self.fallbacks is replaced with getattr(x, name).
If the attribute is anything other than a method that outputs a `Runnable`,
returns `getattr(self.runnable, name)`. If the attribute is a method that
does return a new `Runnable` (e.g. `model.bind_tools([...])` outputs a new
`RunnableBinding`) then `self.runnable` and each of the runnables in
`self.fallbacks` is replaced with `getattr(x, name)`.
Example:
```python
@@ -618,7 +621,6 @@ class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
runnable=RunnableBinding(bound=ChatOpenAI(...), kwargs={"tools": [...]}),
fallbacks=[RunnableBinding(bound=ChatAnthropic(...), kwargs={"tools": [...]})],
)
```
""" # noqa: E501
attr = getattr(self.runnable, name)

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
import inspect
from collections import defaultdict
from collections.abc import Callable
from dataclasses import dataclass, field
from enum import Enum
from typing import (
@@ -22,7 +21,7 @@ from langchain_core.runnables.base import Runnable, RunnableSerializable
from langchain_core.utils.pydantic import _IgnoreUnserializable, is_basemodel_subclass
if TYPE_CHECKING:
from collections.abc import Sequence
from collections.abc import Callable, Sequence
from pydantic import BaseModel
@@ -642,6 +641,7 @@ class Graph:
retry_delay: float = 1.0,
frontmatter_config: dict[str, Any] | None = None,
base_url: str | None = None,
proxies: dict[str, str] | None = None,
) -> bytes:
"""Draw the graph as a PNG image using Mermaid.
@@ -674,11 +674,10 @@ class Graph:
}
```
base_url: The base URL of the Mermaid server for rendering via API.
proxies: HTTP/HTTPS proxies for requests (e.g. `{"http": "http://127.0.0.1:7890"}`).
Returns:
The PNG image as bytes.
"""
# Import locally to prevent circular import
from langchain_core.runnables.graph_mermaid import ( # noqa: PLC0415
@@ -699,6 +698,7 @@ class Graph:
padding=padding,
max_retries=max_retries,
retry_delay=retry_delay,
proxies=proxies,
base_url=base_url,
)

View File

@@ -7,7 +7,6 @@ from __future__ import annotations
import math
import os
from collections.abc import Mapping, Sequence
from typing import TYPE_CHECKING, Any
try:
@@ -20,6 +19,8 @@ except ImportError:
_HAS_GRANDALF = False
if TYPE_CHECKING:
from collections.abc import Mapping, Sequence
from langchain_core.runnables.graph import Edge as LangEdge

View File

@@ -281,6 +281,7 @@ def draw_mermaid_png(
max_retries: int = 1,
retry_delay: float = 1.0,
base_url: str | None = None,
proxies: dict[str, str] | None = None,
) -> bytes:
"""Draws a Mermaid graph as PNG using provided syntax.
@@ -293,6 +294,7 @@ def draw_mermaid_png(
max_retries: Maximum number of retries (MermaidDrawMethod.API).
retry_delay: Delay between retries (MermaidDrawMethod.API).
base_url: Base URL for the Mermaid.ink API.
proxies: HTTP/HTTPS proxies for requests (e.g. `{"http": "http://127.0.0.1:7890"}`).
Returns:
PNG image bytes.
@@ -314,6 +316,7 @@ def draw_mermaid_png(
max_retries=max_retries,
retry_delay=retry_delay,
base_url=base_url,
proxies=proxies,
)
else:
supported_methods = ", ".join([m.value for m in MermaidDrawMethod])
@@ -405,6 +408,7 @@ def _render_mermaid_using_api(
file_type: Literal["jpeg", "png", "webp"] | None = "png",
max_retries: int = 1,
retry_delay: float = 1.0,
proxies: dict[str, str] | None = None,
base_url: str | None = None,
) -> bytes:
"""Renders Mermaid graph using the Mermaid.INK API."""
@@ -445,7 +449,7 @@ def _render_mermaid_using_api(
for attempt in range(max_retries + 1):
try:
response = requests.get(image_url, timeout=10)
response = requests.get(image_url, timeout=10, proxies=proxies)
if response.status_code == requests.codes.ok:
img_bytes = response.content
if output_file_path is not None:
@@ -454,7 +458,10 @@ def _render_mermaid_using_api(
return img_bytes
# If we get a server error (5xx), retry
if 500 <= response.status_code < 600 and attempt < max_retries:
if (
requests.codes.internal_server_error <= response.status_code
and attempt < max_retries
):
# Exponential backoff with jitter
sleep_time = retry_delay * (2**attempt) * (0.5 + 0.5 * random.random()) # noqa: S311 not used for crypto
time.sleep(sleep_time)

View File

@@ -1,5 +1,6 @@
"""Helper class to draw a state graph into a PNG file."""
from itertools import groupby
from typing import Any
from langchain_core.runnables.graph import Graph, LabelsDict
@@ -141,6 +142,7 @@ class PngDrawer:
# Add nodes, conditional edges, and edges to the graph
self.add_nodes(viz, graph)
self.add_edges(viz, graph)
self.add_subgraph(viz, [node.split(":") for node in graph.nodes])
# Update entrypoint and END styles
self.update_styles(viz, graph)
@@ -161,6 +163,32 @@ class PngDrawer:
for node in graph.nodes:
self.add_node(viz, node)
def add_subgraph(
self,
viz: Any,
nodes: list[list[str]],
parent_prefix: list[str] | None = None,
) -> None:
"""Add subgraphs to the graph.
Args:
viz: The graphviz object.
nodes: The nodes to add.
parent_prefix: The prefix of the parent subgraph.
"""
for prefix, grouped in groupby(
[node[:] for node in sorted(nodes)],
key=lambda x: x.pop(0),
):
current_prefix = (parent_prefix or []) + [prefix]
grouped_nodes = list(grouped)
if len(grouped_nodes) > 1:
subgraph = viz.add_subgraph(
[":".join(current_prefix + node) for node in grouped_nodes],
name="cluster_" + ":".join(current_prefix),
)
self.add_subgraph(subgraph, grouped_nodes, current_prefix)
def add_edges(self, viz: Any, graph: Graph) -> None:
"""Add edges to the graph.

View File

@@ -36,23 +36,23 @@ GetSessionHistoryCallable = Callable[..., BaseChatMessageHistory]
class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
"""Runnable that manages chat message history for another Runnable.
"""`Runnable` that manages chat message history for another `Runnable`.
A chat message history is a sequence of messages that represent a conversation.
RunnableWithMessageHistory wraps another Runnable and manages the chat message
`RunnableWithMessageHistory` wraps another `Runnable` and manages the chat message
history for it; it is responsible for reading and updating the chat message
history.
The formats supported for the inputs and outputs of the wrapped Runnable
The formats supported for the inputs and outputs of the wrapped `Runnable`
are described below.
RunnableWithMessageHistory must always be called with a config that contains
`RunnableWithMessageHistory` must always be called with a config that contains
the appropriate parameters for the chat message history factory.
By default, the Runnable is expected to take a single configuration parameter
By default, the `Runnable` is expected to take a single configuration parameter
called `session_id` which is a string. This parameter is used to create a new
or look up an existing chat message history that matches the given session_id.
or look up an existing chat message history that matches the given `session_id`.
In this case, the invocation would look like this:
@@ -117,12 +117,12 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
```
Example where the wrapped Runnable takes a dictionary input:
Example where the wrapped `Runnable` takes a dictionary input:
```python
from typing import Optional
from langchain_community.chat_models import ChatAnthropic
from langchain_anthropic import ChatAnthropic
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
@@ -166,7 +166,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
print(store) # noqa: T201
```
Example where the session factory takes two keys, user_id and conversation id):
Example where the session factory takes two keys (`user_id` and `conversation_id`):
```python
store = {}
@@ -223,21 +223,28 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
"""
get_session_history: GetSessionHistoryCallable
"""Function that returns a new BaseChatMessageHistory.
"""Function that returns a new `BaseChatMessageHistory`.
This function should either take a single positional argument `session_id` of type
string and return a corresponding chat message history instance"""
string and return a corresponding chat message history instance
"""
input_messages_key: str | None = None
"""Must be specified if the base runnable accepts a dict as input.
The key in the input dict that contains the messages."""
"""Must be specified if the base `Runnable` accepts a `dict` as input.
The key in the input `dict` that contains the messages.
"""
output_messages_key: str | None = None
"""Must be specified if the base Runnable returns a dict as output.
The key in the output dict that contains the messages."""
"""Must be specified if the base `Runnable` returns a `dict` as output.
The key in the output `dict` that contains the messages.
"""
history_messages_key: str | None = None
"""Must be specified if the base runnable accepts a dict as input and expects a
separate key for historical messages."""
"""Must be specified if the base `Runnable` accepts a `dict` as input and expects a
separate key for historical messages.
"""
history_factory_config: Sequence[ConfigurableFieldSpec]
"""Configure fields that should be passed to the chat history factory.
See `ConfigurableFieldSpec` for more details."""
See `ConfigurableFieldSpec` for more details.
"""
def __init__(
self,
@@ -254,15 +261,16 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
history_factory_config: Sequence[ConfigurableFieldSpec] | None = None,
**kwargs: Any,
) -> None:
"""Initialize RunnableWithMessageHistory.
"""Initialize `RunnableWithMessageHistory`.
Args:
runnable: The base Runnable to be wrapped.
runnable: The base `Runnable` to be wrapped.
Must take as input one of:
1. A list of `BaseMessage`
2. A dict with one key for all messages
3. A dict with one key for the current input string/message(s) and
2. A `dict` with one key for all messages
3. A `dict` with one key for the current input string/message(s) and
a separate key for historical messages. If the input key points
to a string, it will be treated as a `HumanMessage` in history.
@@ -270,13 +278,15 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
1. A string which can be treated as an `AIMessage`
2. A `BaseMessage` or sequence of `BaseMessage`
3. A dict with a key for a `BaseMessage` or sequence of
3. A `dict` with a key for a `BaseMessage` or sequence of
`BaseMessage`
get_session_history: Function that returns a new BaseChatMessageHistory.
get_session_history: Function that returns a new `BaseChatMessageHistory`.
This function should either take a single positional argument
`session_id` of type string and return a corresponding
chat message history instance.
```python
def get_session_history(
session_id: str, *, user_id: str | None = None
@@ -295,16 +305,17 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
) -> BaseChatMessageHistory: ...
```
input_messages_key: Must be specified if the base runnable accepts a dict
input_messages_key: Must be specified if the base runnable accepts a `dict`
as input.
output_messages_key: Must be specified if the base runnable returns a dict
output_messages_key: Must be specified if the base runnable returns a `dict`
as output.
history_messages_key: Must be specified if the base runnable accepts a dict
as input and expects a separate key for historical messages.
history_messages_key: Must be specified if the base runnable accepts a
`dict` as input and expects a separate key for historical messages.
history_factory_config: Configure fields that should be passed to the
chat history factory. See `ConfigurableFieldSpec` for more details.
Specifying these allows you to pass multiple config keys
into the get_session_history factory.
Specifying these allows you to pass multiple config keys into the
`get_session_history` factory.
**kwargs: Arbitrary additional kwargs to pass to parent class
`RunnableBindingBase` init.
@@ -364,7 +375,7 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
@property
@override
def config_specs(self) -> list[ConfigurableFieldSpec]:
"""Get the configuration specs for the RunnableWithMessageHistory."""
"""Get the configuration specs for the `RunnableWithMessageHistory`."""
return get_unique_config_specs(
super().config_specs + list(self.history_factory_config)
)
@@ -606,6 +617,6 @@ class RunnableWithMessageHistory(RunnableBindingBase): # type: ignore[no-redef]
def _get_parameter_names(callable_: GetSessionHistoryCallable) -> list[str]:
"""Get the parameter names of the callable."""
"""Get the parameter names of the `Callable`."""
sig = inspect.signature(callable_)
return list(sig.parameters.keys())

View File

@@ -51,10 +51,10 @@ def identity(x: Other) -> Other:
"""Identity function.
Args:
x: input.
x: Input.
Returns:
output.
Output.
"""
return x
@@ -63,10 +63,10 @@ async def aidentity(x: Other) -> Other:
"""Async identity function.
Args:
x: input.
x: Input.
Returns:
output.
Output.
"""
return x
@@ -74,11 +74,11 @@ async def aidentity(x: Other) -> Other:
class RunnablePassthrough(RunnableSerializable[Other, Other]):
"""Runnable to passthrough inputs unchanged or with additional keys.
This Runnable behaves almost like the identity function, except that it
This `Runnable` behaves almost like the identity function, except that it
can be configured to add additional keys to the output, if the input is a
dict.
The examples below demonstrate this Runnable works using a few simple
The examples below demonstrate this `Runnable` works using a few simple
chains. The chains rely on simple lambdas to make the examples easy to execute
and experiment with.
@@ -164,7 +164,7 @@ class RunnablePassthrough(RunnableSerializable[Other, Other]):
input_type: type[Other] | None = None,
**kwargs: Any,
) -> None:
"""Create e RunnablePassthrough.
"""Create a `RunnablePassthrough`.
Args:
func: Function to be called with the input.
@@ -180,7 +180,7 @@ class RunnablePassthrough(RunnableSerializable[Other, Other]):
@classmethod
@override
def is_lc_serializable(cls) -> bool:
"""Return True as this class is serializable."""
"""Return `True` as this class is serializable."""
return True
@classmethod
@@ -213,11 +213,11 @@ class RunnablePassthrough(RunnableSerializable[Other, Other]):
"""Merge the Dict input with the output produced by the mapping argument.
Args:
**kwargs: Runnable, Callable or a Mapping from keys to Runnables
or Callables.
**kwargs: `Runnable`, `Callable` or a `Mapping` from keys to `Runnable`
objects or `Callable`s.
Returns:
A Runnable that merges the Dict input with the output produced by the
A `Runnable` that merges the `dict` input with the output produced by the
mapping argument.
"""
return RunnableAssign(RunnableParallel[dict[str, Any]](kwargs))
@@ -350,7 +350,7 @@ _graph_passthrough: RunnablePassthrough = RunnablePassthrough()
class RunnableAssign(RunnableSerializable[dict[str, Any], dict[str, Any]]):
"""Runnable that assigns key-value pairs to dict[str, Any] inputs.
"""Runnable that assigns key-value pairs to `dict[str, Any]` inputs.
The `RunnableAssign` class takes input dictionaries and, through a
`RunnableParallel` instance, applies transformations, then combines
@@ -392,7 +392,7 @@ class RunnableAssign(RunnableSerializable[dict[str, Any], dict[str, Any]]):
mapper: RunnableParallel
def __init__(self, mapper: RunnableParallel[dict[str, Any]], **kwargs: Any) -> None:
"""Create a RunnableAssign.
"""Create a `RunnableAssign`.
Args:
mapper: A `RunnableParallel` instance that will be used to transform the
@@ -403,7 +403,7 @@ class RunnableAssign(RunnableSerializable[dict[str, Any], dict[str, Any]]):
@classmethod
@override
def is_lc_serializable(cls) -> bool:
"""Return True as this class is serializable."""
"""Return `True` as this class is serializable."""
return True
@classmethod
@@ -668,13 +668,19 @@ class RunnableAssign(RunnableSerializable[dict[str, Any], dict[str, Any]]):
yield chunk
class RunnablePick(RunnableSerializable[dict[str, Any], dict[str, Any]]):
"""Runnable that picks keys from dict[str, Any] inputs.
class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
"""`Runnable` that picks keys from `dict[str, Any]` inputs.
RunnablePick class represents a Runnable that selectively picks keys from a
`RunnablePick` class represents a `Runnable` that selectively picks keys from a
dictionary input. It allows you to specify one or more keys to extract
from the input dictionary. It returns a new dictionary containing only
the selected keys.
from the input dictionary.
!!! note "Return Type Behavior"
The return type depends on the `keys` parameter:
- When `keys` is a `str`: Returns the single value associated with that key
- When `keys` is a `list`: Returns a dictionary containing only the selected
keys
Example:
```python
@@ -687,18 +693,22 @@ class RunnablePick(RunnableSerializable[dict[str, Any], dict[str, Any]]):
"country": "USA",
}
runnable = RunnablePick(keys=["name", "age"])
# Single key - returns the value directly
runnable_single = RunnablePick(keys="name")
result_single = runnable_single.invoke(input_data)
print(result_single) # Output: "John"
output_data = runnable.invoke(input_data)
print(output_data) # Output: {'name': 'John', 'age': 30}
# Multiple keys - returns a dictionary
runnable_multiple = RunnablePick(keys=["name", "age"])
result_multiple = runnable_multiple.invoke(input_data)
print(result_multiple) # Output: {'name': 'John', 'age': 30}
```
"""
keys: str | list[str]
def __init__(self, keys: str | list[str], **kwargs: Any) -> None:
"""Create a RunnablePick.
"""Create a `RunnablePick`.
Args:
keys: A single key or a list of keys to pick from the input dictionary.
@@ -708,7 +718,7 @@ class RunnablePick(RunnableSerializable[dict[str, Any], dict[str, Any]]):
@classmethod
@override
def is_lc_serializable(cls) -> bool:
"""Return True as this class is serializable."""
"""Return `True` as this class is serializable."""
return True
@classmethod

View File

@@ -40,11 +40,11 @@ class RouterInput(TypedDict):
key: str
"""The key to route on."""
input: Any
"""The input to pass to the selected Runnable."""
"""The input to pass to the selected `Runnable`."""
class RouterRunnable(RunnableSerializable[RouterInput, Output]):
"""Runnable that routes to a set of Runnables based on Input['key'].
"""`Runnable` that routes to a set of `Runnable` based on `Input['key']`.
Returns the output of the selected Runnable.
@@ -74,10 +74,10 @@ class RouterRunnable(RunnableSerializable[RouterInput, Output]):
self,
runnables: Mapping[str, Runnable[Any, Output] | Callable[[Any], Output]],
) -> None:
"""Create a RouterRunnable.
"""Create a `RouterRunnable`.
Args:
runnables: A mapping of keys to Runnables.
runnables: A mapping of keys to `Runnable` objects.
"""
super().__init__(
runnables={key: coerce_to_runnable(r) for key, r in runnables.items()}
@@ -90,7 +90,7 @@ class RouterRunnable(RunnableSerializable[RouterInput, Output]):
@classmethod
@override
def is_lc_serializable(cls) -> bool:
"""Return True as this class is serializable."""
"""Return `True` as this class is serializable."""
return True
@classmethod

View File

@@ -28,7 +28,7 @@ class EventData(TypedDict, total=False):
This field is only available if the `Runnable` raised an exception.
!!! version-added "Added in version 1.0.0"
!!! version-added "Added in `langchain-core` 1.0.0"
"""
output: Any
"""The output of the `Runnable` that generated the event.

View File

@@ -7,8 +7,7 @@ import asyncio
import inspect
import sys
import textwrap
from collections.abc import Callable, Mapping, Sequence
from contextvars import Context
from collections.abc import Mapping, Sequence
from functools import lru_cache
from inspect import signature
from itertools import groupby
@@ -31,9 +30,11 @@ if TYPE_CHECKING:
AsyncIterable,
AsyncIterator,
Awaitable,
Callable,
Coroutine,
Iterable,
)
from contextvars import Context
from langchain_core.runnables.schema import StreamEvent

View File

@@ -125,9 +125,11 @@ def print_sys_info(*, additional_pkgs: Sequence[str] = ()) -> None:
for dep in sub_dependencies:
try:
dep_version = metadata.version(dep)
print(f"> {dep}: {dep_version}")
except Exception:
print(f"> {dep}: Installed. No version info available.")
dep_version = None
if dep_version is not None:
print(f"> {dep}: {dep_version}")
if __name__ == "__main__":

View File

@@ -386,6 +386,8 @@ class ToolException(Exception): # noqa: N818
ArgsSchema = TypeBaseModel | dict[str, Any]
_EMPTY_SET: frozenset[str] = frozenset()
class BaseTool(RunnableSerializable[str | dict | ToolCall, Any]):
"""Base class for all LangChain tools.
@@ -569,6 +571,11 @@ class ChildTool(BaseTool):
self.name, full_schema, fields, fn_description=self.description
)
@functools.cached_property
def _injected_args_keys(self) -> frozenset[str]:
# base implementation doesn't manage injected args
return _EMPTY_SET
# --- Runnable ---
@override
@@ -649,6 +656,7 @@ class ChildTool(BaseTool):
if isinstance(input_args, dict):
return tool_input
if issubclass(input_args, BaseModel):
# Check args_schema for InjectedToolCallId
for k, v in get_all_basemodel_annotations(input_args).items():
if _is_injected_arg_type(v, injected_type=InjectedToolCallId):
if tool_call_id is None:
@@ -664,6 +672,7 @@ class ChildTool(BaseTool):
result = input_args.model_validate(tool_input)
result_dict = result.model_dump()
elif issubclass(input_args, BaseModelV1):
# Check args_schema for InjectedToolCallId
for k, v in get_all_basemodel_annotations(input_args).items():
if _is_injected_arg_type(v, injected_type=InjectedToolCallId):
if tool_call_id is None:
@@ -683,9 +692,25 @@ class ChildTool(BaseTool):
f"args_schema must be a Pydantic BaseModel, got {self.args_schema}"
)
raise NotImplementedError(msg)
return {
k: getattr(result, k) for k, v in result_dict.items() if k in tool_input
validated_input = {
k: getattr(result, k) for k in result_dict if k in tool_input
}
for k in self._injected_args_keys:
if k == "tool_call_id":
if tool_call_id is None:
msg = (
"When tool includes an InjectedToolCallId "
"argument, tool must always be invoked with a full "
"model ToolCall of the form: {'args': {...}, "
"'name': '...', 'type': 'tool_call', "
"'tool_call_id': '...'}"
)
raise ValueError(msg)
validated_input[k] = tool_call_id
if k in tool_input:
injected_val = tool_input[k]
validated_input[k] = injected_val
return validated_input
return tool_input
@abstractmethod
@@ -872,16 +897,19 @@ class ChildTool(BaseTool):
tool_kwargs |= {config_param: config}
response = context.run(self._run, *tool_args, **tool_kwargs)
if self.response_format == "content_and_artifact":
if not isinstance(response, tuple) or len(response) != 2:
msg = (
"Since response_format='content_and_artifact' "
"a two-tuple of the message content and raw tool output is "
f"expected. Instead generated response of type: "
f"{type(response)}."
)
msg = (
"Since response_format='content_and_artifact' "
"a two-tuple of the message content and raw tool output is "
f"expected. Instead, generated response is of type: "
f"{type(response)}."
)
if not isinstance(response, tuple):
error_to_raise = ValueError(msg)
else:
content, artifact = response
try:
content, artifact = response
except ValueError:
error_to_raise = ValueError(msg)
else:
content = response
except (ValidationError, ValidationErrorV1) as e:
@@ -998,16 +1026,19 @@ class ChildTool(BaseTool):
coro = self._arun(*tool_args, **tool_kwargs)
response = await coro_with_context(coro, context)
if self.response_format == "content_and_artifact":
if not isinstance(response, tuple) or len(response) != 2:
msg = (
"Since response_format='content_and_artifact' "
"a two-tuple of the message content and raw tool output is "
f"expected. Instead generated response of type: "
f"{type(response)}."
)
msg = (
"Since response_format='content_and_artifact' "
"a two-tuple of the message content and raw tool output is "
f"expected. Instead, generated response is of type: "
f"{type(response)}."
)
if not isinstance(response, tuple):
error_to_raise = ValueError(msg)
else:
content, artifact = response
try:
content, artifact = response
except ValueError:
error_to_raise = ValueError(msg)
else:
content = response
except ValidationError as e:

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import functools
import textwrap
from collections.abc import Awaitable, Callable
from inspect import signature
@@ -21,10 +22,12 @@ from langchain_core.callbacks import (
)
from langchain_core.runnables import RunnableConfig, run_in_executor
from langchain_core.tools.base import (
_EMPTY_SET,
FILTERED_ARGS,
ArgsSchema,
BaseTool,
_get_runnable_config_param,
_is_injected_arg_type,
create_schema_from_function,
)
from langchain_core.utils.pydantic import is_basemodel_subclass
@@ -241,6 +244,17 @@ class StructuredTool(BaseTool):
**kwargs,
)
@functools.cached_property
def _injected_args_keys(self) -> frozenset[str]:
fn = self.func or self.coroutine
if fn is None:
return _EMPTY_SET
return frozenset(
k
for k, v in signature(fn).parameters.items()
if _is_injected_arg_type(v.annotation)
)
def _filter_schema_args(func: Callable) -> list[str]:
filter_args = list(FILTERED_ARGS)

View File

@@ -15,12 +15,6 @@ from typing import (
from langchain_core.exceptions import TracerException
from langchain_core.load import dumpd
from langchain_core.outputs import (
ChatGeneration,
ChatGenerationChunk,
GenerationChunk,
LLMResult,
)
from langchain_core.tracers.schemas import Run
if TYPE_CHECKING:
@@ -31,6 +25,12 @@ if TYPE_CHECKING:
from langchain_core.documents import Document
from langchain_core.messages import BaseMessage
from langchain_core.outputs import (
ChatGeneration,
ChatGenerationChunk,
GenerationChunk,
LLMResult,
)
logger = logging.getLogger(__name__)

View File

@@ -8,7 +8,6 @@ import logging
import types
import typing
import uuid
from collections.abc import Callable
from typing import (
TYPE_CHECKING,
Annotated,
@@ -33,6 +32,8 @@ from langchain_core.utils.json_schema import dereference_refs
from langchain_core.utils.pydantic import is_basemodel_subclass
if TYPE_CHECKING:
from collections.abc import Callable
from langchain_core.tools import BaseTool
logger = logging.getLogger(__name__)
@@ -351,7 +352,8 @@ def convert_to_openai_function(
Raises:
ValueError: If function is not in a supported format.
!!! warning "Behavior changed in 0.3.16"
!!! warning "Behavior changed in `langchain-core` 0.3.16"
`description` and `parameters` keys are now optional. Only `name` is
required and guaranteed to be part of the output.
"""
@@ -412,7 +414,7 @@ def convert_to_openai_function(
if strict is not None:
if "strict" in oai_function and oai_function["strict"] != strict:
msg = (
f"Tool/function already has a 'strict' key wth value "
f"Tool/function already has a 'strict' key with value "
f"{oai_function['strict']} which is different from the explicit "
f"`strict` arg received {strict=}."
)
@@ -475,16 +477,19 @@ def convert_to_openai_tool(
A dict version of the passed in tool which is compatible with the
OpenAI tool-calling API.
!!! warning "Behavior changed in 0.3.16"
!!! warning "Behavior changed in `langchain-core` 0.3.16"
`description` and `parameters` keys are now optional. Only `name` is
required and guaranteed to be part of the output.
!!! warning "Behavior changed in 0.3.44"
!!! warning "Behavior changed in `langchain-core` 0.3.44"
Return OpenAI Responses API-style tools unchanged. This includes
any dict with `"type"` in `"file_search"`, `"function"`,
`"computer_use_preview"`, `"web_search_preview"`.
!!! warning "Behavior changed in 0.3.63"
!!! warning "Behavior changed in `langchain-core` 0.3.63"
Added support for OpenAI's image generation built-in tool.
"""
# Import locally to prevent circular import
@@ -653,6 +658,9 @@ def tool_example_to_messages(
return messages
_MIN_DOCSTRING_BLOCKS = 2
def _parse_google_docstring(
docstring: str | None,
args: list[str],
@@ -671,7 +679,7 @@ def _parse_google_docstring(
arg for arg in args if arg not in {"run_manager", "callbacks", "return"}
}
if filtered_annotations and (
len(docstring_blocks) < 2
len(docstring_blocks) < _MIN_DOCSTRING_BLOCKS
or not any(block.startswith("Args:") for block in docstring_blocks[1:])
):
msg = "Found invalid Google-Style docstring."

View File

@@ -4,11 +4,13 @@ from __future__ import annotations
import json
import re
from collections.abc import Callable
from typing import Any
from typing import TYPE_CHECKING, Any
from langchain_core.exceptions import OutputParserException
if TYPE_CHECKING:
from collections.abc import Callable
def _replace_new_line(match: re.Match[str]) -> str:
value = match.group(2)

View File

@@ -170,28 +170,33 @@ def dereference_refs(
full_schema: dict | None = None,
skip_keys: Sequence[str] | None = None,
) -> dict:
"""Resolve and inline JSON Schema $ref references in a schema object.
"""Resolve and inline JSON Schema `$ref` references in a schema object.
This function processes a JSON Schema and resolves all $ref references by replacing
them with the actual referenced content. It handles both simple references and
complex cases like circular references and mixed $ref objects that contain
additional properties alongside the $ref.
This function processes a JSON Schema and resolves all `$ref` references by
replacing them with the actual referenced content.
Handles both simple references and complex cases like circular references and mixed
`$ref` objects that contain additional properties alongside the `$ref`.
Args:
schema_obj: The JSON Schema object or fragment to process. This can be a
complete schema or just a portion of one.
full_schema: The complete schema containing all definitions that $refs might
point to. If not provided, defaults to schema_obj (useful when the
schema is self-contained).
skip_keys: Controls recursion behavior and reference resolution depth:
- If `None` (Default): Only recurse under '$defs' and use shallow reference
resolution (break cycles but don't deep-inline nested refs)
- If provided (even as []): Recurse under all keys and use deep reference
resolution (fully inline all nested references)
schema_obj: The JSON Schema object or fragment to process.
This can be a complete schema or just a portion of one.
full_schema: The complete schema containing all definitions that `$refs` might
point to.
If not provided, defaults to `schema_obj` (useful when the schema is
self-contained).
skip_keys: Controls recursion behavior and reference resolution depth.
- If `None` (Default): Only recurse under `'$defs'` and use shallow
reference resolution (break cycles but don't deep-inline nested refs)
- If provided (even as `[]`): Recurse under all keys and use deep reference
resolution (fully inline all nested references)
Returns:
A new dictionary with all $ref references resolved and inlined. The original
schema_obj is not modified.
A new dictionary with all $ref references resolved and inlined.
The original `schema_obj` is not modified.
Examples:
Basic reference resolution:
@@ -203,7 +208,8 @@ def dereference_refs(
>>> result = dereference_refs(schema)
>>> result["properties"]["name"] # {"type": "string"}
Mixed $ref with additional properties:
Mixed `$ref` with additional properties:
>>> schema = {
... "properties": {
... "name": {"$ref": "#/$defs/base", "description": "User name"}
@@ -215,6 +221,7 @@ def dereference_refs(
# {"type": "string", "minLength": 1, "description": "User name"}
Handling circular references:
>>> schema = {
... "properties": {"user": {"$ref": "#/$defs/User"}},
... "$defs": {
@@ -227,10 +234,11 @@ def dereference_refs(
>>> result = dereference_refs(schema) # Won't cause infinite recursion
!!! note
- Circular references are handled gracefully by breaking cycles
- Mixed $ref objects (with both $ref and other properties) are supported
- Additional properties in mixed $refs override resolved properties
- The $defs section is preserved in the output by default
- Mixed `$ref` objects (with both `$ref` and other properties) are supported
- Additional properties in mixed `$refs` override resolved properties
- The `$defs` section is preserved in the output by default
"""
full = full_schema or schema_obj
keys_to_skip = list(skip_keys) if skip_keys is not None else ["$defs"]

View File

@@ -374,15 +374,29 @@ def _get_key(
if resolved_scope in (0, False):
return resolved_scope
# Move into the scope
try:
# Try subscripting (Normal dictionaries)
resolved_scope = cast("dict[str, Any]", resolved_scope)[child]
except (TypeError, AttributeError):
if isinstance(resolved_scope, dict):
try:
resolved_scope = getattr(resolved_scope, child)
except (TypeError, AttributeError):
# Try as a list
resolved_scope = resolved_scope[int(child)] # type: ignore[index]
resolved_scope = resolved_scope[child]
except (KeyError, TypeError):
# Key not found - will be caught by outer try-except
msg = f"Key {child!r} not found in dict"
raise KeyError(msg) from None
elif isinstance(resolved_scope, (list, tuple)):
try:
resolved_scope = resolved_scope[int(child)]
except (ValueError, IndexError, TypeError):
# Invalid index - will be caught by outer try-except
msg = f"Invalid index {child!r} for list/tuple"
raise IndexError(msg) from None
else:
# Reject everything else for security
# This prevents traversing into arbitrary Python objects
msg = (
f"Cannot traverse into {type(resolved_scope).__name__}. "
"Mustache templates only support dict, list, and tuple. "
f"Got: {type(resolved_scope)}"
)
raise TypeError(msg) # noqa: TRY301
try:
# This allows for custom falsy data types
@@ -393,8 +407,9 @@ def _get_key(
if resolved_scope in (0, False):
return resolved_scope
return resolved_scope or ""
except (AttributeError, KeyError, IndexError, ValueError):
except (AttributeError, KeyError, IndexError, ValueError, TypeError):
# We couldn't find the key in the current scope
# TypeError: Attempted to traverse into non-dict/list type
# We'll try again on the next pass
pass

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
import inspect
import textwrap
import warnings
from collections.abc import Callable
from contextlib import nullcontext
from functools import lru_cache, wraps
from types import GenericAlias
@@ -41,10 +40,12 @@ from pydantic.json_schema import (
)
from pydantic.v1 import BaseModel as BaseModelV1
from pydantic.v1 import create_model as create_model_v1
from pydantic.v1.fields import ModelField
from typing_extensions import deprecated, override
if TYPE_CHECKING:
from collections.abc import Callable
from pydantic.v1.fields import ModelField
from pydantic_core import core_schema
PYDANTIC_VERSION = version.parse(pydantic.__version__)
@@ -65,8 +66,8 @@ def get_pydantic_major_version() -> int:
PYDANTIC_MAJOR_VERSION = PYDANTIC_VERSION.major
PYDANTIC_MINOR_VERSION = PYDANTIC_VERSION.minor
IS_PYDANTIC_V1 = PYDANTIC_VERSION.major == 1
IS_PYDANTIC_V2 = PYDANTIC_VERSION.major == 2
IS_PYDANTIC_V1 = False
IS_PYDANTIC_V2 = True
PydanticBaseModel = BaseModel
TypeBaseModel = type[BaseModel]

View File

@@ -11,7 +11,6 @@ import logging
import math
import warnings
from abc import ABC, abstractmethod
from collections.abc import Callable
from itertools import cycle
from typing import (
TYPE_CHECKING,
@@ -29,7 +28,7 @@ from langchain_core.retrievers import BaseRetriever, LangSmithRetrieverParams
from langchain_core.runnables.config import run_in_executor
if TYPE_CHECKING:
from collections.abc import Collection, Iterable, Iterator, Sequence
from collections.abc import Callable, Collection, Iterable, Iterator, Sequence
from langchain_core.callbacks.manager import (
AsyncCallbackManagerForRetrieverRun,
@@ -295,8 +294,9 @@ class VectorStore(ABC):
Args:
query: Input text.
search_type: Type of search to perform. Can be `'similarity'`, `'mmr'`, or
`'similarity_score_threshold'`.
search_type: Type of search to perform.
Can be `'similarity'`, `'mmr'`, or `'similarity_score_threshold'`.
**kwargs: Arguments to pass to the search method.
Returns:
@@ -329,8 +329,9 @@ class VectorStore(ABC):
Args:
query: Input text.
search_type: Type of search to perform. Can be `'similarity'`, `'mmr'`, or
`'similarity_score_threshold'`.
search_type: Type of search to perform.
Can be `'similarity'`, `'mmr'`, or `'similarity_score_threshold'`.
**kwargs: Arguments to pass to the search method.
Returns:
@@ -461,9 +462,10 @@ class VectorStore(ABC):
Args:
query: Input text.
k: Number of `Document` objects to return.
**kwargs: kwargs to be passed to similarity search. Should include
`score_threshold`, An optional floating point value between `0` to `1`
to filter the resulting set of retrieved docs
**kwargs: Kwargs to be passed to similarity search.
Should include `score_threshold`, an optional floating point value
between `0` to `1` to filter the resulting set of retrieved docs.
Returns:
List of tuples of `(doc, similarity_score)`
@@ -488,9 +490,10 @@ class VectorStore(ABC):
Args:
query: Input text.
k: Number of `Document` objects to return.
**kwargs: kwargs to be passed to similarity search. Should include
`score_threshold`, An optional floating point value between `0` to `1`
to filter the resulting set of retrieved docs
**kwargs: Kwargs to be passed to similarity search.
Should include `score_threshold`, an optional floating point value
between `0` to `1` to filter the resulting set of retrieved docs.
Returns:
List of tuples of `(doc, similarity_score)`
@@ -512,9 +515,10 @@ class VectorStore(ABC):
Args:
query: Input text.
k: Number of `Document` objects to return.
**kwargs: kwargs to be passed to similarity search. Should include
`score_threshold`, An optional floating point value between `0` to `1`
to filter the resulting set of retrieved docs
**kwargs: Kwargs to be passed to similarity search.
Should include `score_threshold`, an optional floating point value
between `0` to `1` to filter the resulting set of retrieved docs.
Returns:
List of tuples of `(doc, similarity_score)`.
@@ -561,9 +565,10 @@ class VectorStore(ABC):
Args:
query: Input text.
k: Number of `Document` objects to return.
**kwargs: kwargs to be passed to similarity search. Should include
`score_threshold`, An optional floating point value between `0` to `1`
to filter the resulting set of retrieved docs
**kwargs: Kwargs to be passed to similarity search.
Should include `score_threshold`, an optional floating point value
between `0` to `1` to filter the resulting set of retrieved docs.
Returns:
List of tuples of `(doc, similarity_score)`
@@ -901,13 +906,15 @@ class VectorStore(ABC):
Args:
**kwargs: Keyword arguments to pass to the search function.
Can include:
* `search_type`: Defines the type of search that the Retriever should
perform. Can be `'similarity'` (default), `'mmr'`, or
`'similarity_score_threshold'`.
* `search_kwargs`: Keyword arguments to pass to the search function. Can
include things like:
* `search_kwargs`: Keyword arguments to pass to the search function.
Can include things like:
* `k`: Amount of documents to return (Default: `4`)
* `score_threshold`: Minimum relevance threshold

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
import json
import uuid
from collections.abc import Callable
from pathlib import Path
from typing import (
TYPE_CHECKING,
@@ -20,7 +19,7 @@ from langchain_core.vectorstores.utils import _cosine_similarity as cosine_simil
from langchain_core.vectorstores.utils import maximal_marginal_relevance
if TYPE_CHECKING:
from collections.abc import Iterator, Sequence
from collections.abc import Callable, Iterator, Sequence
from langchain_core.embeddings import Embeddings

View File

@@ -1,3 +1,3 @@
"""langchain-core version information and utilities."""
VERSION = "1.0.3"
VERSION = "1.1.0"

View File

@@ -3,8 +3,13 @@ requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
authors = []
name = "langchain-core"
description = "Building applications with LLMs through composability"
license = {text = "MIT"}
readme = "README.md"
authors = []
version = "1.1.0"
requires-python = ">=3.10.0,<4.0.0"
dependencies = [
"langsmith>=0.3.45,<1.0.0",
@@ -15,10 +20,6 @@ dependencies = [
"packaging>=23.2.0,<26.0.0",
"pydantic>=2.7.4,<3.0.0",
]
name = "langchain-core"
version = "1.0.3"
description = "Building applications with LLMs through composability"
readme = "README.md"
[project.urls]
Homepage = "https://docs.langchain.com/"
@@ -35,7 +36,6 @@ typing = [
"mypy>=1.18.1,<1.19.0",
"types-pyyaml>=6.0.12.2,<7.0.0.0",
"types-requests>=2.28.11.5,<3.0.0.0",
"langchain-model-profiles",
"langchain-text-splitters",
]
dev = [
@@ -57,7 +57,6 @@ test = [
"blockbuster>=1.5.18,<1.6.0",
"numpy>=1.26.4; python_version<'3.13'",
"numpy>=2.1.0; python_version>='3.13'",
"langchain-model-profiles",
"langchain-tests",
"pytest-benchmark",
"pytest-codspeed",
@@ -65,7 +64,6 @@ test = [
test_integration = []
[tool.uv.sources]
langchain-model-profiles = { path = "../model-profiles" }
langchain-tests = { path = "../standard-tests" }
langchain-text-splitters = { path = "../text-splitters" }
@@ -104,7 +102,6 @@ ignore = [
"ANN401", # No Any types
"BLE", # Blind exceptions
"ERA", # No commented-out code
"PLR2004", # Comparison to magic number
]
unfixable = [
"B028", # People should intentionally tune the stacklevel
@@ -125,7 +122,7 @@ ignore-var-parameters = true # ignore missing documentation for *args and **kwa
"langchain_core/utils/mustache.py" = [ "PLW0603",]
"langchain_core/sys_info.py" = [ "T201",]
"tests/unit_tests/test_tools.py" = [ "ARG",]
"tests/**" = [ "D1", "S", "SLF",]
"tests/**" = [ "D1", "PLR2004", "S", "SLF",]
"scripts/**" = [ "INP", "S",]
[tool.coverage.run]
@@ -133,7 +130,10 @@ omit = [ "tests/*",]
[tool.pytest.ini_options]
addopts = "--snapshot-warn-unused --strict-markers --strict-config --durations=5"
markers = [ "requires: mark tests as requiring a specific library", "compile: mark placeholder test used to compile integration tests without running them", ]
markers = [
"requires: mark tests as requiring a specific library",
"compile: mark placeholder test used to compile integration tests without running them",
]
asyncio_mode = "auto"
filterwarnings = [ "ignore::langchain_core._api.beta_decorator.LangChainBetaWarning",]
asyncio_default_fixture_loop_scope = "function"
filterwarnings = [ "ignore::langchain_core._api.beta_decorator.LangChainBetaWarning",]

View File

@@ -148,4 +148,65 @@ async def test_inline_handlers_share_parent_context_multiple() -> None:
2,
3,
3,
], f"Expected order of states was broken due to context loss. Got {states}"
]
async def test_shielded_callback_context_preservation() -> None:
"""Verify that shielded callbacks preserve context variables.
This test specifically addresses the issue where async callbacks decorated
with @shielded do not properly preserve context variables, breaking
instrumentation and other context-dependent functionality.
The issue manifests in callbacks that use the @shielded decorator:
* on_llm_end
* on_llm_error
* on_chain_end
* on_chain_error
* And other shielded callback methods
"""
context_var: contextvars.ContextVar[str] = contextvars.ContextVar("test_context")
class ContextTestHandler(AsyncCallbackHandler):
"""Handler that reads context variables in shielded callbacks."""
def __init__(self) -> None:
self.run_inline = False
self.context_values: list[str] = []
@override
async def on_llm_end(self, response: Any, **kwargs: Any) -> None:
"""This method is decorated with @shielded in the run manager."""
# This should preserve the context variable value
self.context_values.append(context_var.get("not_found"))
@override
async def on_chain_end(self, outputs: Any, **kwargs: Any) -> None:
"""This method is decorated with @shielded in the run manager."""
# This should preserve the context variable value
self.context_values.append(context_var.get("not_found"))
# Set up the test context
context_var.set("test_value")
handler = ContextTestHandler()
manager = AsyncCallbackManager(handlers=[handler])
# Create run managers that have the shielded methods
llm_managers = await manager.on_llm_start({}, ["test prompt"])
llm_run_manager = llm_managers[0]
chain_run_manager = await manager.on_chain_start({}, {"test": "input"})
# Test LLM end callback (which is shielded)
await llm_run_manager.on_llm_end({"response": "test"}) # type: ignore[arg-type]
# Test Chain end callback (which is shielded)
await chain_run_manager.on_chain_end({"output": "test"})
# The context should be preserved in shielded callbacks
# This was the main issue - shielded decorators were not preserving context
assert handler.context_values == ["test_value", "test_value"], (
f"Expected context values ['test_value', 'test_value'], "
f"but got {handler.context_values}. "
f"This indicates the shielded decorator is not preserving context variables."
)

View File

@@ -33,7 +33,7 @@ def test_hashing() -> None:
# hash should be deterministic
assert hashed_document.id == "fd1dc827-051b-537d-a1fe-1fa043e8b276"
# Verify that hashing with sha1 is determinstic
# Verify that hashing with sha1 is deterministic
another_hashed_document = _get_document_with_hash(document, key_encoder="sha1")
assert another_hashed_document.id == hashed_document.id

View File

@@ -18,6 +18,7 @@ from langchain_core.language_models import (
ParrotFakeChatModel,
)
from langchain_core.language_models._utils import _normalize_messages
from langchain_core.language_models.chat_models import _generate_response_from_error
from langchain_core.language_models.fake_chat_models import (
FakeListChatModelError,
GenericFakeChatModel,
@@ -1221,16 +1222,99 @@ def test_get_ls_params() -> None:
def test_model_profiles() -> None:
model = GenericFakeChatModel(messages=iter([]))
profile = model.profile
assert profile == {}
assert model.profile is None
class MyModel(GenericFakeChatModel):
model: str = "gpt-5"
model_with_profile = GenericFakeChatModel(
messages=iter([]), profile={"max_input_tokens": 100}
)
assert model_with_profile.profile == {"max_input_tokens": 100}
@property
def _llm_type(self) -> str:
return "openai-chat"
model = MyModel(messages=iter([]))
profile = model.profile
assert profile
class MockResponse:
"""Mock response for testing _generate_response_from_error."""
def __init__(
self,
status_code: int = 400,
headers: dict[str, str] | None = None,
json_data: dict[str, Any] | None = None,
json_raises: type[Exception] | None = None,
text_raises: type[Exception] | None = None,
):
self.status_code = status_code
self.headers = headers or {}
self._json_data = json_data
self._json_raises = json_raises
self._text_raises = text_raises
def json(self) -> dict[str, Any]:
if self._json_raises:
msg = "JSON parsing failed"
raise self._json_raises(msg)
return self._json_data or {}
@property
def text(self) -> str:
if self._text_raises:
msg = "Text access failed"
raise self._text_raises(msg)
return ""
class MockAPIError(Exception):
"""Mock API error with response attribute."""
def __init__(self, message: str, response: MockResponse | None = None):
super().__init__(message)
self.message = message
if response is not None:
self.response = response
def test_generate_response_from_error_with_valid_json() -> None:
"""Test `_generate_response_from_error` with valid JSON response."""
response = MockResponse(
status_code=400,
headers={"content-type": "application/json"},
json_data={"error": {"message": "Bad request", "type": "invalid_request"}},
)
error = MockAPIError("API Error", response=response)
generations = _generate_response_from_error(error)
assert len(generations) == 1
generation = generations[0]
assert isinstance(generation, ChatGeneration)
assert isinstance(generation.message, AIMessage)
assert generation.message.content == ""
metadata = generation.message.response_metadata
assert metadata["body"] == {
"error": {"message": "Bad request", "type": "invalid_request"}
}
assert metadata["headers"] == {"content-type": "application/json"}
assert metadata["status_code"] == 400
def test_generate_response_from_error_handles_streaming_response_failure() -> None:
# Simulates scenario where accessing response.json() or response.text
# raises ResponseNotRead on streaming responses
response = MockResponse(
status_code=400,
headers={"content-type": "application/json"},
json_raises=Exception, # Simulates ResponseNotRead or similar
text_raises=Exception,
)
error = MockAPIError("API Error", response=response)
# This should NOT raise an exception, but should handle it gracefully
generations = _generate_response_from_error(error)
assert len(generations) == 1
generation = generations[0]
metadata = generation.message.response_metadata
# When both fail, body should be None instead of raising an exception
assert metadata["body"] is None
assert metadata["headers"] == {"content-type": "application/json"}
assert metadata["status_code"] == 400

View File

@@ -18,6 +18,8 @@ EXPECTED_ALL = [
"FakeStreamingListLLM",
"FakeListLLM",
"ParrotFakeChatModel",
"ModelProfile",
"ModelProfileRegistry",
"is_openai_data_block",
]

View File

@@ -0,0 +1,140 @@
"""Test groq block translator."""
from typing import cast
import pytest
from langchain_core.messages import AIMessage
from langchain_core.messages import content as types
from langchain_core.messages.base import _extract_reasoning_from_additional_kwargs
from langchain_core.messages.block_translators import PROVIDER_TRANSLATORS
from langchain_core.messages.block_translators.groq import (
_parse_code_json,
translate_content,
)
def test_groq_translator_registered() -> None:
"""Test that groq translator is properly registered."""
assert "groq" in PROVIDER_TRANSLATORS
assert "translate_content" in PROVIDER_TRANSLATORS["groq"]
assert "translate_content_chunk" in PROVIDER_TRANSLATORS["groq"]
def test_extract_reasoning_from_additional_kwargs_exists() -> None:
"""Test that _extract_reasoning_from_additional_kwargs can be imported."""
# Verify it's callable
assert callable(_extract_reasoning_from_additional_kwargs)
def test_groq_translate_content_basic() -> None:
"""Test basic groq content translation."""
# Test with simple text message
message = AIMessage(content="Hello world")
blocks = translate_content(message)
assert isinstance(blocks, list)
assert len(blocks) == 1
assert blocks[0]["type"] == "text"
assert blocks[0]["text"] == "Hello world"
def test_groq_translate_content_with_reasoning() -> None:
"""Test groq content translation with reasoning content."""
# Test with reasoning content in additional_kwargs
message = AIMessage(
content="Final answer",
additional_kwargs={"reasoning_content": "Let me think about this..."},
)
blocks = translate_content(message)
assert isinstance(blocks, list)
assert len(blocks) == 2
# First block should be reasoning
assert blocks[0]["type"] == "reasoning"
assert blocks[0]["reasoning"] == "Let me think about this..."
# Second block should be text
assert blocks[1]["type"] == "text"
assert blocks[1]["text"] == "Final answer"
def test_groq_translate_content_with_tool_calls() -> None:
"""Test groq content translation with tool calls."""
# Test with tool calls
message = AIMessage(
content="",
tool_calls=[
{
"name": "search",
"args": {"query": "test"},
"id": "call_123",
}
],
)
blocks = translate_content(message)
assert isinstance(blocks, list)
assert len(blocks) == 1
assert blocks[0]["type"] == "tool_call"
assert blocks[0]["name"] == "search"
assert blocks[0]["args"] == {"query": "test"}
assert blocks[0]["id"] == "call_123"
def test_groq_translate_content_with_executed_tools() -> None:
"""Test groq content translation with executed tools (built-in tools)."""
# Test with executed_tools in additional_kwargs (Groq built-in tools)
message = AIMessage(
content="",
additional_kwargs={
"executed_tools": [
{
"type": "python",
"arguments": '{"code": "print(\\"hello\\")"}',
"output": "hello\\n",
}
]
},
)
blocks = translate_content(message)
assert isinstance(blocks, list)
# Should have server_tool_call and server_tool_result
assert len(blocks) >= 2
# Check for server_tool_call
tool_call_blocks = [
cast("types.ServerToolCall", b)
for b in blocks
if b.get("type") == "server_tool_call"
]
assert len(tool_call_blocks) == 1
assert tool_call_blocks[0]["name"] == "code_interpreter"
assert "code" in tool_call_blocks[0]["args"]
# Check for server_tool_result
tool_result_blocks = [
cast("types.ServerToolResult", b)
for b in blocks
if b.get("type") == "server_tool_result"
]
assert len(tool_result_blocks) == 1
assert tool_result_blocks[0]["output"] == "hello\\n"
assert tool_result_blocks[0]["status"] == "success"
def test_parse_code_json() -> None:
"""Test the _parse_code_json helper function."""
# Test valid code JSON
result = _parse_code_json('{"code": "print(\'hello\')"}')
assert result == {"code": "print('hello')"}
# Test code with unescaped quotes (Groq format)
result = _parse_code_json('{"code": "print("hello")"}')
assert result == {"code": 'print("hello")'}
# Test invalid format raises ValueError
with pytest.raises(ValueError, match="Could not extract Python code"):
_parse_code_json('{"invalid": "format"}')

View File

@@ -1,3 +1,4 @@
import sys
from collections.abc import AsyncIterator, Iterator
from typing import Any
@@ -886,3 +887,461 @@ def test_max_tokens_error(caplog: Any) -> None:
"`max_tokens` stop reason" in msg and record.levelname == "ERROR"
for record, msg in zip(caplog.records, caplog.messages, strict=False)
)
def test_pydantic_tools_parser_with_mixed_pydantic_versions() -> None:
"""Test PydanticToolsParser with both Pydantic v1 and v2 models."""
# For Python 3.14+ compatibility, use create_model for Pydantic v1
if sys.version_info >= (3, 14):
WeatherV1 = pydantic.v1.create_model( # noqa: N806
"WeatherV1",
__doc__="Weather information using Pydantic v1.",
temperature=(int, ...),
conditions=(str, ...),
)
else:
class WeatherV1(pydantic.v1.BaseModel):
"""Weather information using Pydantic v1."""
temperature: int
conditions: str
class LocationV2(BaseModel):
"""Location information using Pydantic v2."""
city: str
country: str
# Test with Pydantic v1 model
parser_v1 = PydanticToolsParser(tools=[WeatherV1])
message_v1 = AIMessage(
content="",
tool_calls=[
{
"id": "call_weather",
"name": "WeatherV1",
"args": {"temperature": 25, "conditions": "sunny"},
}
],
)
generation_v1 = ChatGeneration(message=message_v1)
result_v1 = parser_v1.parse_result([generation_v1])
assert len(result_v1) == 1
assert isinstance(result_v1[0], WeatherV1)
assert result_v1[0].temperature == 25 # type: ignore[attr-defined,unused-ignore]
assert result_v1[0].conditions == "sunny" # type: ignore[attr-defined,unused-ignore]
# Test with Pydantic v2 model
parser_v2 = PydanticToolsParser(tools=[LocationV2])
message_v2 = AIMessage(
content="",
tool_calls=[
{
"id": "call_location",
"name": "LocationV2",
"args": {"city": "Paris", "country": "France"},
}
],
)
generation_v2 = ChatGeneration(message=message_v2)
result_v2 = parser_v2.parse_result([generation_v2])
assert len(result_v2) == 1
assert isinstance(result_v2[0], LocationV2)
assert result_v2[0].city == "Paris"
assert result_v2[0].country == "France"
# Test with both v1 and v2 models
parser_mixed = PydanticToolsParser(tools=[WeatherV1, LocationV2])
message_mixed = AIMessage(
content="",
tool_calls=[
{
"id": "call_weather",
"name": "WeatherV1",
"args": {"temperature": 20, "conditions": "cloudy"},
},
{
"id": "call_location",
"name": "LocationV2",
"args": {"city": "London", "country": "UK"},
},
],
)
generation_mixed = ChatGeneration(message=message_mixed)
result_mixed = parser_mixed.parse_result([generation_mixed])
assert len(result_mixed) == 2
assert isinstance(result_mixed[0], WeatherV1)
assert result_mixed[0].temperature == 20 # type: ignore[attr-defined,unused-ignore]
assert isinstance(result_mixed[1], LocationV2)
assert result_mixed[1].city == "London"
def test_pydantic_tools_parser_with_custom_title() -> None:
"""Test PydanticToolsParser with Pydantic v2 model using custom title."""
class CustomTitleTool(BaseModel):
"""Tool with custom title in model config."""
model_config = {"title": "MyCustomToolName"}
value: int
description: str
# Test with custom title - tool should be callable by custom name
parser = PydanticToolsParser(tools=[CustomTitleTool])
message = AIMessage(
content="",
tool_calls=[
{
"id": "call_custom",
"name": "MyCustomToolName",
"args": {"value": 42, "description": "test"},
}
],
)
generation = ChatGeneration(message=message)
result = parser.parse_result([generation])
assert len(result) == 1
assert isinstance(result[0], CustomTitleTool)
assert result[0].value == 42
assert result[0].description == "test"
def test_pydantic_tools_parser_name_dict_fallback() -> None:
"""Test that name_dict properly falls back to __name__ when title is None."""
class ToolWithoutTitle(BaseModel):
"""Tool without explicit title."""
data: str
# Ensure model_config doesn't have a title or it's None
# (This is the default behavior)
parser = PydanticToolsParser(tools=[ToolWithoutTitle])
message = AIMessage(
content="",
tool_calls=[
{
"id": "call_no_title",
"name": "ToolWithoutTitle",
"args": {"data": "test_data"},
}
],
)
generation = ChatGeneration(message=message)
result = parser.parse_result([generation])
assert len(result) == 1
assert isinstance(result[0], ToolWithoutTitle)
assert result[0].data == "test_data"
def test_pydantic_tools_parser_with_nested_models() -> None:
"""Test PydanticToolsParser with nested Pydantic v1 and v2 models."""
# Nested v1 models
if sys.version_info >= (3, 14):
AddressV1 = pydantic.v1.create_model( # noqa: N806
"AddressV1",
__doc__="Address using Pydantic v1.",
street=(str, ...),
city=(str, ...),
zip_code=(str, ...),
)
PersonV1 = pydantic.v1.create_model( # noqa: N806
"PersonV1",
__doc__="Person with nested address using Pydantic v1.",
name=(str, ...),
age=(int, ...),
address=(AddressV1, ...),
)
else:
class AddressV1(pydantic.v1.BaseModel):
"""Address using Pydantic v1."""
street: str
city: str
zip_code: str
class PersonV1(pydantic.v1.BaseModel):
"""Person with nested address using Pydantic v1."""
name: str
age: int
address: AddressV1
# Nested v2 models
class CoordinatesV2(BaseModel):
"""Coordinates using Pydantic v2."""
latitude: float
longitude: float
class LocationV2(BaseModel):
"""Location with nested coordinates using Pydantic v2."""
name: str
coordinates: CoordinatesV2
# Test with nested Pydantic v1 model
parser_v1 = PydanticToolsParser(tools=[PersonV1])
message_v1 = AIMessage(
content="",
tool_calls=[
{
"id": "call_person",
"name": "PersonV1",
"args": {
"name": "Alice",
"age": 30,
"address": {
"street": "123 Main St",
"city": "Springfield",
"zip_code": "12345",
},
},
}
],
)
generation_v1 = ChatGeneration(message=message_v1)
result_v1 = parser_v1.parse_result([generation_v1])
assert len(result_v1) == 1
assert isinstance(result_v1[0], PersonV1)
assert result_v1[0].name == "Alice" # type: ignore[attr-defined,unused-ignore]
assert result_v1[0].age == 30 # type: ignore[attr-defined,unused-ignore]
assert isinstance(result_v1[0].address, AddressV1) # type: ignore[attr-defined,unused-ignore]
assert result_v1[0].address.street == "123 Main St" # type: ignore[attr-defined,unused-ignore]
assert result_v1[0].address.city == "Springfield" # type: ignore[attr-defined,unused-ignore]
# Test with nested Pydantic v2 model
parser_v2 = PydanticToolsParser(tools=[LocationV2])
message_v2 = AIMessage(
content="",
tool_calls=[
{
"id": "call_location",
"name": "LocationV2",
"args": {
"name": "Eiffel Tower",
"coordinates": {"latitude": 48.8584, "longitude": 2.2945},
},
}
],
)
generation_v2 = ChatGeneration(message=message_v2)
result_v2 = parser_v2.parse_result([generation_v2])
assert len(result_v2) == 1
assert isinstance(result_v2[0], LocationV2)
assert result_v2[0].name == "Eiffel Tower"
assert isinstance(result_v2[0].coordinates, CoordinatesV2)
assert result_v2[0].coordinates.latitude == 48.8584
assert result_v2[0].coordinates.longitude == 2.2945
# Test with both nested models in one message
parser_mixed = PydanticToolsParser(tools=[PersonV1, LocationV2])
message_mixed = AIMessage(
content="",
tool_calls=[
{
"id": "call_person",
"name": "PersonV1",
"args": {
"name": "Bob",
"age": 25,
"address": {
"street": "456 Oak Ave",
"city": "Portland",
"zip_code": "97201",
},
},
},
{
"id": "call_location",
"name": "LocationV2",
"args": {
"name": "Golden Gate Bridge",
"coordinates": {"latitude": 37.8199, "longitude": -122.4783},
},
},
],
)
generation_mixed = ChatGeneration(message=message_mixed)
result_mixed = parser_mixed.parse_result([generation_mixed])
assert len(result_mixed) == 2
assert isinstance(result_mixed[0], PersonV1)
assert result_mixed[0].name == "Bob" # type: ignore[attr-defined,unused-ignore]
assert result_mixed[0].address.city == "Portland" # type: ignore[attr-defined,unused-ignore]
assert isinstance(result_mixed[1], LocationV2)
assert result_mixed[1].name == "Golden Gate Bridge"
assert result_mixed[1].coordinates.latitude == 37.8199
def test_pydantic_tools_parser_with_optional_fields() -> None:
"""Test PydanticToolsParser with optional fields in v1 and v2 models."""
if sys.version_info >= (3, 14):
ProductV1 = pydantic.v1.create_model( # noqa: N806
"ProductV1",
__doc__="Product with optional fields using Pydantic v1.",
name=(str, ...),
price=(float, ...),
description=(str | None, None),
stock=(int, 0),
)
else:
class ProductV1(pydantic.v1.BaseModel):
"""Product with optional fields using Pydantic v1."""
name: str
price: float
description: str | None = None
stock: int = 0
# v2 model with optional fields
class UserV2(BaseModel):
"""User with optional fields using Pydantic v2."""
username: str
email: str
bio: str | None = None
age: int | None = None
# Test v1 with all fields provided
parser_v1_full = PydanticToolsParser(tools=[ProductV1])
message_v1_full = AIMessage(
content="",
tool_calls=[
{
"id": "call_product_full",
"name": "ProductV1",
"args": {
"name": "Laptop",
"price": 999.99,
"description": "High-end laptop",
"stock": 50,
},
}
],
)
generation_v1_full = ChatGeneration(message=message_v1_full)
result_v1_full = parser_v1_full.parse_result([generation_v1_full])
assert len(result_v1_full) == 1
assert isinstance(result_v1_full[0], ProductV1)
assert result_v1_full[0].name == "Laptop" # type: ignore[attr-defined,unused-ignore]
assert result_v1_full[0].price == 999.99 # type: ignore[attr-defined,unused-ignore]
assert result_v1_full[0].description == "High-end laptop" # type: ignore[attr-defined,unused-ignore]
assert result_v1_full[0].stock == 50 # type: ignore[attr-defined,unused-ignore]
# Test v1 with only required fields
parser_v1_minimal = PydanticToolsParser(tools=[ProductV1])
message_v1_minimal = AIMessage(
content="",
tool_calls=[
{
"id": "call_product_minimal",
"name": "ProductV1",
"args": {"name": "Mouse", "price": 29.99},
}
],
)
generation_v1_minimal = ChatGeneration(message=message_v1_minimal)
result_v1_minimal = parser_v1_minimal.parse_result([generation_v1_minimal])
assert len(result_v1_minimal) == 1
assert isinstance(result_v1_minimal[0], ProductV1)
assert result_v1_minimal[0].name == "Mouse" # type: ignore[attr-defined,unused-ignore]
assert result_v1_minimal[0].price == 29.99 # type: ignore[attr-defined,unused-ignore]
assert result_v1_minimal[0].description is None # type: ignore[attr-defined,unused-ignore]
assert result_v1_minimal[0].stock == 0 # type: ignore[attr-defined,unused-ignore]
# Test v2 with all fields provided
parser_v2_full = PydanticToolsParser(tools=[UserV2])
message_v2_full = AIMessage(
content="",
tool_calls=[
{
"id": "call_user_full",
"name": "UserV2",
"args": {
"username": "john_doe",
"email": "john@example.com",
"bio": "Software developer",
"age": 28,
},
}
],
)
generation_v2_full = ChatGeneration(message=message_v2_full)
result_v2_full = parser_v2_full.parse_result([generation_v2_full])
assert len(result_v2_full) == 1
assert isinstance(result_v2_full[0], UserV2)
assert result_v2_full[0].username == "john_doe"
assert result_v2_full[0].email == "john@example.com"
assert result_v2_full[0].bio == "Software developer"
assert result_v2_full[0].age == 28
# Test v2 with only required fields
parser_v2_minimal = PydanticToolsParser(tools=[UserV2])
message_v2_minimal = AIMessage(
content="",
tool_calls=[
{
"id": "call_user_minimal",
"name": "UserV2",
"args": {"username": "jane_smith", "email": "jane@example.com"},
}
],
)
generation_v2_minimal = ChatGeneration(message=message_v2_minimal)
result_v2_minimal = parser_v2_minimal.parse_result([generation_v2_minimal])
assert len(result_v2_minimal) == 1
assert isinstance(result_v2_minimal[0], UserV2)
assert result_v2_minimal[0].username == "jane_smith"
assert result_v2_minimal[0].email == "jane@example.com"
assert result_v2_minimal[0].bio is None
assert result_v2_minimal[0].age is None
# Test mixed v1 and v2 with partial optional fields
parser_mixed = PydanticToolsParser(tools=[ProductV1, UserV2])
message_mixed = AIMessage(
content="",
tool_calls=[
{
"id": "call_product",
"name": "ProductV1",
"args": {"name": "Keyboard", "price": 79.99, "stock": 100},
},
{
"id": "call_user",
"name": "UserV2",
"args": {
"username": "alice",
"email": "alice@example.com",
"age": 35,
},
},
],
)
generation_mixed = ChatGeneration(message=message_mixed)
result_mixed = parser_mixed.parse_result([generation_mixed])
assert len(result_mixed) == 2
assert isinstance(result_mixed[0], ProductV1)
assert result_mixed[0].name == "Keyboard" # type: ignore[attr-defined,unused-ignore]
assert result_mixed[0].description is None # type: ignore[attr-defined,unused-ignore]
assert result_mixed[0].stock == 100 # type: ignore[attr-defined,unused-ignore]
assert isinstance(result_mixed[1], UserV2)
assert result_mixed[1].username == "alice"
assert result_mixed[1].bio is None
assert result_mixed[1].age == 35

View File

@@ -682,7 +682,7 @@
May also hold extra provider-specific keys.
!!! version-added "Added in version 0.3.9"
!!! version-added "Added in `langchain-core` 0.3.9"
''',
'properties': dict({
'audio': dict({
@@ -800,7 +800,7 @@
May also hold extra provider-specific keys.
!!! version-added "Added in version 0.3.9"
!!! version-added "Added in `langchain-core` 0.3.9"
''',
'properties': dict({
'audio': dict({
@@ -1319,10 +1319,12 @@
}
```
!!! warning "Behavior changed in 0.3.9"
!!! warning "Behavior changed in `langchain-core` 0.3.9"
Added `input_token_details` and `output_token_details`.
!!! note "LangSmith SDK"
The LangSmith SDK also has a `UsageMetadata` class. While the two share fields,
LangSmith's `UsageMetadata` has additional fields to capture cost information
used by the LangSmith platform.
@@ -2096,7 +2098,7 @@
May also hold extra provider-specific keys.
!!! version-added "Added in version 0.3.9"
!!! version-added "Added in `langchain-core` 0.3.9"
''',
'properties': dict({
'audio': dict({
@@ -2214,7 +2216,7 @@
May also hold extra provider-specific keys.
!!! version-added "Added in version 0.3.9"
!!! version-added "Added in `langchain-core` 0.3.9"
''',
'properties': dict({
'audio': dict({
@@ -2733,10 +2735,12 @@
}
```
!!! warning "Behavior changed in 0.3.9"
!!! warning "Behavior changed in `langchain-core` 0.3.9"
Added `input_token_details` and `output_token_details`.
!!! note "LangSmith SDK"
The LangSmith SDK also has a `UsageMetadata` class. While the two share fields,
LangSmith's `UsageMetadata` has additional fields to capture cost information
used by the LangSmith platform.

View File

@@ -1193,3 +1193,511 @@ def test_dict_message_prompt_template_errors_on_jinja2() -> None:
_ = ChatPromptTemplate.from_messages(
[("human", [prompt])], template_format="jinja2"
)
def test_rendering_prompt_with_conditionals_no_empty_text_blocks() -> None:
manifest = {
"lc": 1,
"type": "constructor",
"id": ["langchain_core", "prompts", "chat", "ChatPromptTemplate"],
"kwargs": {
"messages": [
{
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"chat",
"SystemMessagePromptTemplate",
],
"kwargs": {
"prompt": {
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"prompt",
"PromptTemplate",
],
"kwargs": {
"input_variables": [],
"template_format": "mustache",
"template": "Always echo back whatever I send you.",
},
},
},
},
{
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"chat",
"HumanMessagePromptTemplate",
],
"kwargs": {
"prompt": [
{
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"prompt",
"PromptTemplate",
],
"kwargs": {
"input_variables": [],
"template_format": "mustache",
"template": "Here is the teacher's prompt:",
"additional_content_fields": {
"text": "Here is the teacher's prompt:",
},
},
},
{
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"prompt",
"PromptTemplate",
],
"kwargs": {
"input_variables": ["promptDescription"],
"template_format": "mustache",
"template": '"{{promptDescription}}"\n',
"additional_content_fields": {
"text": '"{{promptDescription}}"\n',
},
},
},
{
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"prompt",
"PromptTemplate",
],
"kwargs": {
"input_variables": [],
"template_format": "mustache",
"template": "Here is the expected answer or success criteria given by the teacher:", # noqa: E501
"additional_content_fields": {
"text": "Here is the expected answer or success criteria given by the teacher:", # noqa: E501
},
},
},
{
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"prompt",
"PromptTemplate",
],
"kwargs": {
"input_variables": ["expectedResponse"],
"template_format": "mustache",
"template": '"{{expectedResponse}}"\n',
"additional_content_fields": {
"text": '"{{expectedResponse}}"\n',
},
},
},
{
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"prompt",
"PromptTemplate",
],
"kwargs": {
"input_variables": [],
"template_format": "mustache",
"template": "Note: This may be just one example of many possible correct ways for the student to respond.\n", # noqa: E501
"additional_content_fields": {
"text": "Note: This may be just one example of many possible correct ways for the student to respond.\n", # noqa: E501
},
},
},
{
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"prompt",
"PromptTemplate",
],
"kwargs": {
"input_variables": [],
"template_format": "mustache",
"template": "For your evaluation of the student's response:\n", # noqa: E501
"additional_content_fields": {
"text": "For your evaluation of the student's response:\n", # noqa: E501
},
},
},
{
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"prompt",
"PromptTemplate",
],
"kwargs": {
"input_variables": [],
"template_format": "mustache",
"template": "Here is a transcript of the student's explanation:", # noqa: E501
"additional_content_fields": {
"text": "Here is a transcript of the student's explanation:", # noqa: E501
},
},
},
{
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"prompt",
"PromptTemplate",
],
"kwargs": {
"input_variables": ["responseTranscript"],
"template_format": "mustache",
"template": '"{{responseTranscript}}"\n',
"additional_content_fields": {
"text": '"{{responseTranscript}}"\n',
},
},
},
{
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"prompt",
"PromptTemplate",
],
"kwargs": {
"input_variables": ["readingFluencyAnalysis"],
"template_format": "mustache",
"template": "{{#readingFluencyAnalysis}} For this task, the student's reading pronunciation and fluency were important. Here is analysis of the student's oral response: \"{{readingFluencyAnalysis}}\" {{/readingFluencyAnalysis}}", # noqa: E501
"additional_content_fields": {
"text": "{{#readingFluencyAnalysis}} For this task, the student's reading pronunciation and fluency were important. Here is analysis of the student's oral response: \"{{readingFluencyAnalysis}}\" {{/readingFluencyAnalysis}}", # noqa: E501
},
},
},
{
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"prompt",
"PromptTemplate",
],
"kwargs": {
"input_variables": ["readingFluencyAnalysis"],
"template_format": "mustache",
"template": "{{#readingFluencyAnalysis}}Root analysis of the student's response (step 3) in this oral analysis rather than inconsistencies in the transcript.{{/readingFluencyAnalysis}}", # noqa: E501
"additional_content_fields": {
"text": "{{#readingFluencyAnalysis}}Root analysis of the student's response (step 3) in this oral analysis rather than inconsistencies in the transcript.{{/readingFluencyAnalysis}}", # noqa: E501
},
},
},
{
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"prompt",
"PromptTemplate",
],
"kwargs": {
"input_variables": ["readingFluencyAnalysis"],
"template_format": "mustache",
"template": "{{#readingFluencyAnalysis}}Remember this is a student, so we care about general fluency - not voice acting. {{/readingFluencyAnalysis}}\n", # noqa: E501
"additional_content_fields": {
"text": "{{#readingFluencyAnalysis}}Remember this is a student, so we care about general fluency - not voice acting. {{/readingFluencyAnalysis}}\n", # noqa: E501
},
},
},
{
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"prompt",
"PromptTemplate",
],
"kwargs": {
"input_variables": ["multipleChoiceAnalysis"],
"template_format": "mustache",
"template": "{{#multipleChoiceAnalysis}}Here is an analysis of the student's multiple choice response: {{multipleChoiceAnalysis}}{{/multipleChoiceAnalysis}}\n", # noqa: E501
"additional_content_fields": {
"text": "{{#multipleChoiceAnalysis}}Here is an analysis of the student's multiple choice response: {{multipleChoiceAnalysis}}{{/multipleChoiceAnalysis}}\n", # noqa: E501
},
},
},
{
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"prompt",
"PromptTemplate",
],
"kwargs": {
"input_variables": [],
"template_format": "mustache",
"template": "Here is the student's whiteboard:\n",
"additional_content_fields": {
"text": "Here is the student's whiteboard:\n",
},
},
},
{
"lc": 1,
"type": "constructor",
"id": [
"langchain_core",
"prompts",
"image",
"ImagePromptTemplate",
],
"kwargs": {
"template": {
"url": "{{whiteboard}}",
},
"input_variables": ["whiteboard"],
"template_format": "mustache",
"additional_content_fields": {
"image_url": {
"url": "{{whiteboard}}",
},
},
},
},
],
"additional_options": {},
},
},
],
"input_variables": [
"promptDescription",
"expectedResponse",
"responseTranscript",
"readingFluencyAnalysis",
"readingFluencyAnalysis",
"readingFluencyAnalysis",
"multipleChoiceAnalysis",
"whiteboard",
],
"template_format": "mustache",
"metadata": {
"lc_hub_owner": "jacob",
"lc_hub_repo": "mustache-conditionals",
"lc_hub_commit_hash": "836ad82d512409ea6024fb760b76a27ba58fc68b1179656c0ba2789778686d46", # noqa: E501
},
},
}
# Load the ChatPromptTemplate from the manifest
template = load(manifest)
# Format with conditional data - rules is empty, so mustache conditionals
# should not render
result = template.invoke(
{
"promptDescription": "What is the capital of the USA?",
"expectedResponse": "Washington, D.C.",
"responseTranscript": "Washington, D.C.",
"readingFluencyAnalysis": None,
"multipleChoiceAnalysis": "testing2",
"whiteboard": "https://foo.com/bar.png",
}
)
content = result.messages[1].content
assert isinstance(content, list)
assert not [
block for block in content if block["type"] == "text" and block["text"] == ""
]
def test_fstring_rejects_invalid_identifier_variable_names() -> None:
"""Test that f-string templates block attribute access, indexing.
This validation prevents template injection attacks by blocking:
- Attribute access like {msg.__class__}
- Indexing like {msg[0]}
- All-digit variable names like {0} or {100} (interpreted as positional args)
While allowing any other field names that Python's Formatter accepts.
"""
# Test that attribute access and indexing are blocked (security issue)
invalid_templates = [
"{msg.__class__}", # Attribute access with dunder
"{msg.__class__.__name__}", # Multiple dunders
"{msg.content}", # Attribute access
"{msg[0]}", # Item access
"{0}", # All-digit variable name (positional argument)
"{100}", # All-digit variable name (positional argument)
"{42}", # All-digit variable name (positional argument)
]
for template_str in invalid_templates:
with pytest.raises(ValueError, match="Invalid variable name") as exc_info:
ChatPromptTemplate.from_messages(
[("human", template_str)],
template_format="f-string",
)
error_msg = str(exc_info.value)
assert "Invalid variable name" in error_msg
# Check for any of the expected error message parts
assert (
"attribute access" in error_msg
or "indexing" in error_msg
or "positional arguments" in error_msg
)
# Valid templates - Python's Formatter accepts non-identifier field names
valid_templates = [
(
"Hello {name} and {user_id}",
{"name": "Alice", "user_id": "123"},
"Hello Alice and 123",
),
("User: {user-name}", {"user-name": "Bob"}, "User: Bob"), # Hyphen allowed
(
"Value: {2fast}",
{"2fast": "Charlie"},
"Value: Charlie",
), # Starts with digit allowed
("Data: {my var}", {"my var": "Dave"}, "Data: Dave"), # Space allowed
]
for template_str, kwargs, expected in valid_templates:
template = ChatPromptTemplate.from_messages(
[("human", template_str)],
template_format="f-string",
)
result = template.invoke(kwargs)
assert result.messages[0].content == expected # type: ignore[attr-defined]
def test_mustache_template_attribute_access_vulnerability() -> None:
"""Test that Mustache template injection is blocked.
Verify the fix for security vulnerability GHSA-6qv9-48xg-fc7f
Previously, Mustache used getattr() as a fallback, allowing access to
dangerous attributes like __class__, __globals__, etc.
The fix adds isinstance checks that reject non-dict/list types.
When templates try to traverse Python objects, they get empty string
per Mustache spec (better than the previous behavior of exposing internals).
"""
msg = HumanMessage("howdy")
# Template tries to access attributes on a Python object
prompt = ChatPromptTemplate.from_messages(
[("human", "{{question.__class__.__name__}}")],
template_format="mustache",
)
# After the fix: returns empty string (attack blocked!)
# Previously would return "HumanMessage" via getattr()
result = prompt.invoke({"question": msg})
assert result.messages[0].content == "" # type: ignore[attr-defined]
# Mustache still works correctly with actual dicts
prompt_dict = ChatPromptTemplate.from_messages(
[("human", "{{person.name}}")],
template_format="mustache",
)
result_dict = prompt_dict.invoke({"person": {"name": "Alice"}})
assert result_dict.messages[0].content == "Alice" # type: ignore[attr-defined]
@pytest.mark.requires("jinja2")
def test_jinja2_template_attribute_access_is_blocked() -> None:
"""Test that Jinja2 SandboxedEnvironment blocks dangerous attribute access.
This test verifies that Jinja2's sandbox successfully blocks access to
dangerous dunder attributes like __class__, unlike Mustache.
GOOD: Jinja2 SandboxedEnvironment raises SecurityError when attempting
to access __class__, __globals__, etc. This is expected behavior.
"""
msg = HumanMessage("howdy")
# Create a Jinja2 template that attempts to access __class__.__name__
prompt = ChatPromptTemplate.from_messages(
[("human", "{{question.__class__.__name__}}")],
template_format="jinja2",
)
# Jinja2 sandbox should block this with SecurityError
with pytest.raises(Exception, match="attribute") as exc_info:
prompt.invoke(
{"question": msg, "question.__class__.__name__": "safe_placeholder"}
)
# Verify it's a SecurityError from Jinja2 blocking __class__ access
error_msg = str(exc_info.value)
assert (
"SecurityError" in str(type(exc_info.value))
or "access to attribute '__class__'" in error_msg
), f"Expected SecurityError blocking __class__, got: {error_msg}"
@pytest.mark.requires("jinja2")
def test_jinja2_blocks_all_attribute_access() -> None:
"""Test that Jinja2 now blocks ALL attribute/method access for security.
After the fix, Jinja2 uses _RestrictedSandboxedEnvironment which blocks
ALL attribute access, not just dunder attributes. This prevents the
parse_raw() vulnerability.
"""
msg = HumanMessage("test content")
# Test 1: Simple variable access should still work
prompt_simple = ChatPromptTemplate.from_messages(
[("human", "Message: {{message}}")],
template_format="jinja2",
)
result = prompt_simple.invoke({"message": "hello world"})
assert "hello world" in result.messages[0].content # type: ignore[attr-defined]
# Test 2: Attribute access should now be blocked (including safe attributes)
prompt_attr = ChatPromptTemplate.from_messages(
[("human", "Content: {{msg.content}}")],
template_format="jinja2",
)
with pytest.raises(Exception, match="attribute") as exc_info:
prompt_attr.invoke({"msg": msg})
error_msg = str(exc_info.value)
assert (
"SecurityError" in str(type(exc_info.value))
or "Access to attributes is not allowed" in error_msg
), f"Expected SecurityError blocking attribute access, got: {error_msg}"

View File

@@ -125,7 +125,9 @@ def test_structured_prompt_kwargs() -> None:
def test_structured_prompt_template_format() -> None:
prompt = StructuredPrompt(
[("human", "hi {{person.name}}")], schema={}, template_format="mustache"
[("human", "hi {{person.name}}")],
schema={"type": "object", "properties": {}, "title": "foo"},
template_format="mustache",
)
assert prompt.messages[0].prompt.template_format == "mustache" # type: ignore[union-attr, union-attr]
assert prompt.input_variables == ["person"]
@@ -136,4 +138,8 @@ def test_structured_prompt_template_format() -> None:
def test_structured_prompt_template_empty_vars() -> None:
with pytest.raises(ChevronError, match="empty tag"):
StructuredPrompt([("human", "hi {{}}")], schema={}, template_format="mustache")
StructuredPrompt(
[("human", "hi {{}}")],
schema={"type": "object", "properties": {}, "title": "foo"},
template_format="mustache",
)

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