Compare commits

..

76 Commits

Author SHA1 Message Date
Sydney Runkle
286039788e linting 2025-09-29 16:12:34 -07:00
Sydney Runkle
41e9d479f5 cleaning up types theoretically 2025-09-29 16:10:06 -07:00
Sydney Runkle
1526c419d8 initial pass at async impl 2025-09-29 14:15:23 -07:00
Mason Daugherty
3325196be1 fix(langchain): handle gpt-5 model name in init_chat_model (#33148)
expand to match any `gpt-*` model to openai
2025-09-29 16:16:17 -04:00
Mason Daugherty
f402fdcea3 fix(langchain): add context_management to Anthropic chat model init (#33150) 2025-09-29 16:13:47 -04:00
ccurme
ca9217c02d release(anthropic): 0.3.21 (#33147) 2025-09-29 19:56:28 +00:00
ccurme
f9bae40475 feat(anthropic): support memory and context management features (#33146)
https://docs.claude.com/en/docs/build-with-claude/context-editing

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-09-29 15:42:38 -04:00
ccurme
839a18e112 fix(openai): remove __future__.annotations import from test files (#33144)
Breaks schema conversion in places.
2025-09-29 16:23:32 +00:00
Mohammad Mohtashim
33a6def762 fix(core): Support of 'reasoning' type in 'convert_to_openai_messages' (#33050) 2025-09-29 09:17:05 -04:00
nhuang-lc
c456c8ae51 fix(langchain): fix response action for HITL (#33131)
Multiple improvements to HITL flow:

* On a `response` type resume, we should still append the tool call to
the last AIMessage (otherwise we have a ToolResult without a
corresponding ToolCall)
* When all interrupts have `response` types (so there's no pending tool
calls), we should jump back to the first node (instead of end) as we
enforced in the previous `post_model_hook_router`
* Added comments to `model_to_tools` router so clarify all of the
potential exit conditions

Additionally:
* Lockfile update to use latest LG alpha release
* Added test for `jump_to` behaving ephemerally, this was fixed in LG
but surfaced as a bug w/ `jump_to`.
* Bump version to v1.0.0a10 to prep for alpha release

---------

Co-authored-by: Sydney Runkle <sydneymarierunkle@gmail.com>
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
2025-09-29 13:08:18 +00:00
Eugene Yurtsev
54ea62050b chore(langchain_v1): move tool node to tools namespace (#33132)
* Move ToolNode to tools namespace
* Expose injected variable as well in tools namespace
* Update doc-strings throughout
2025-09-26 15:23:57 -04:00
Mason Daugherty
986302322f docs: more standardization (#33124) 2025-09-25 20:46:20 -04:00
Mason Daugherty
a5137b0a3e refactor(langchain): resolve pydantic deprecation warnings (#33125) 2025-09-25 17:33:18 -04:00
Mason Daugherty
5bea28393d docs: standardize .. code-block directive usage (#33122)
and fix typos
2025-09-25 16:49:56 -04:00
Mason Daugherty
c3fed20940 docs: correct ported over directives (#33121)
Match rest of repo
2025-09-25 15:54:54 -04:00
Mason Daugherty
6d418ba983 test(mistralai): add xfail for structured output test (#33119)
In rare cases (difficult to reproduce), Mistral's API fails to return
valid bodies, leading to failures from `PydanticToolsParser`
2025-09-25 13:05:31 -04:00
Mason Daugherty
12daba63ff test(openai): raise token limit for o1 test (#33118)
`test_o1[False-False]` was sometimes failing because the OpenAI o1 model
was hitting a token limit with only 100 tokens
2025-09-25 12:57:33 -04:00
Christophe Bornet
eaf8dce7c2 chore: bump ruff version to 0.13 (#33043)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-09-25 12:27:39 -04:00
Mason Daugherty
f82de1a8d7 chore: bump locks (#33114) 2025-09-25 01:46:01 -04:00
Mason Daugherty
e3efd1e891 test(text-splitters): capture beta warnings (#33113) 2025-09-25 01:30:20 -04:00
Mason Daugherty
d6769cf032 test(text-splitters): resolve pytest marker warning (#33112)
#33111
2025-09-25 01:29:42 -04:00
Mason Daugherty
7ab2e0dd3b test(core): resolve pytest marker warning (#33111)
Remove redundant/outdated `@pytest.mark.requires("jinja2")` decorator

Pytest marks (like `@pytest.mark.requires(...)`) applied directly to
fixtures have no effect and are deprecated.
2025-09-25 01:08:54 -04:00
Mason Daugherty
81319ad3f0 test(core): resolve pydantic_v1 deprecation warning (#33110)
Excluded pydantic_v1 module from import testing

Acceptable since this pydantic_v1 is explicitly deprecated. Testing its
importability at this stage serves little purpose since users should
migrate away from it.
2025-09-25 01:08:03 -04:00
Mason Daugherty
e3f3c13b75 refactor(core): use aadd_documents in vectorstores unit tests (#33109)
Don't use the deprecated `upsert()` and `aupsert()`

Instead use the recommended alternatives
2025-09-25 00:57:08 -04:00
Mason Daugherty
c30844fce4 fix(core): use version agnostic get_fields (#33108)
Resolves a warning
2025-09-25 00:54:29 -04:00
Mason Daugherty
c9eb3bdb2d test(core): use secure hash algorithm in indexing test to eliminate SHA-1 warning (#33107)
Finish work from #33101
2025-09-25 00:49:11 -04:00
Mason Daugherty
e97baeb9a6 test(core): suppress pydantic_v1 deprecation warnings during import tests (#33106)
We intentionally import these. Hide warnings to reduce testing noise.
2025-09-25 00:37:40 -04:00
Mason Daugherty
3a6046b157 test(core): don't use deprecated input_variables param in from_file (#33105)
finish #33104
2025-09-25 04:29:31 +00:00
Mason Daugherty
8fdc619f75 refactor(core): don't use deprecated input_variables param in from_file (#33104)
Missed awhile back; causes warnings during tests
2025-09-25 00:14:17 -04:00
Ali Ismail
729bfe8369 test(core): enhance stringify_value test coverage for nested structures (#33099)
## Summary
Adds test coverage for the `stringify_value` utility function to handle
complex nested data structures that weren't previously tested.

## Changes
- Added `test_stringify_value_nested_structures()` to `test_strings.py`
- Tests nested dictionaries within lists
- Tests mixed-type lists with various data types
- Verifies proper stringification of complex nested structures

## Why This Matters
- Fills a gap in test coverage for edge cases
- Ensures `stringify_value` handles complex data structures correctly  
- Improves confidence in string utility functions used throughout the
codebase
- Low risk addition that strengthens existing test suite

## Testing
```bash
uv run --group test pytest libs/core/tests/unit_tests/utils/test_strings.py::test_stringify_value_nested_structures -v
```

This test addition follows the project's testing patterns and adds
meaningful coverage without introducing any breaking changes.

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-09-25 00:04:47 -04:00
Mason Daugherty
9b624a79b2 test(core): suppress deprecation warnings in PipelinePromptTemplate (#33102)
We're intentionally testing this still so as not to regress. Reduce
warning noise.
2025-09-25 04:03:27 +00:00
Mason Daugherty
c60c5a91cb fix(core): use secure hash algorithm in indexing test to eliminate SHA-1 warning (#33101)
Use SHA-256 (collision-resistant) instead of the default SHA-1. No
functional changes to test behavior.
2025-09-25 00:02:11 -04:00
Mason Daugherty
d9e0c212e0 chore(infra): add tests to label mapping (#33103) 2025-09-25 00:01:53 -04:00
Sydney Runkle
f015526e42 release(langchain): v1.0.0a9 (#33098) 2025-09-24 21:02:53 +00:00
Sydney Runkle
57d931532f fix(langchain): extra arg for anthropic caching, __end__ -> end for jump_to (#33097)
Also updating `jump_to` to use `end` instead of `__end__`
2025-09-24 17:00:40 -04:00
Mason Daugherty
50012d95e2 chore: update pull_request_target types, harden (#33096)
Enhance the pull request workflows by updating the `pull_request_target`
types and ensuring safety by avoiding checkout of the PR's head. Update
the action to use a specific commit from the archived repository.
2025-09-24 16:37:16 -04:00
Mason Daugherty
33f06875cb fix(langchain_v1): version equality check (#33095) 2025-09-24 16:27:55 -04:00
dependabot[bot]
e5730307e7 chore: bump actions/setup-node from 4 to 5 (#32952)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4
to 5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/setup-node/releases">actions/setup-node's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<h3>Breaking Changes</h3>
<ul>
<li>Enhance caching in setup-node with automatic package manager
detection by <a
href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1348">actions/setup-node#1348</a></li>
</ul>
<p>This update, introduces automatic caching when a valid
<code>packageManager</code> field is present in your
<code>package.json</code>. This aims to improve workflow performance and
make dependency management more seamless.
To disable this automatic caching, set <code>package-manager-cache:
false</code></p>
<pre lang="yaml"><code>steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
  with:
    package-manager-cache: false
</code></pre>
<ul>
<li>Upgrade action to use node24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1325">actions/setup-node#1325</a></li>
</ul>
<p>Make sure your runner is on version v2.327.1 or later to ensure
compatibility with this release. <a
href="https://github.com/actions/runner/releases/tag/v2.327.1">See
Release Notes</a></p>
<h3>Dependency Upgrades</h3>
<ul>
<li>Upgrade <code>@​octokit/request-error</code> and
<code>@​actions/github</code> by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-node/pull/1227">actions/setup-node#1227</a></li>
<li>Upgrade uuid from 9.0.1 to 11.1.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-node/pull/1273">actions/setup-node#1273</a></li>
<li>Upgrade undici from 5.28.5 to 5.29.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-node/pull/1295">actions/setup-node#1295</a></li>
<li>Upgrade form-data to bring in fix for critical vulnerability by <a
href="https://github.com/gowridurgad"><code>@​gowridurgad</code></a> in
<a
href="https://redirect.github.com/actions/setup-node/pull/1332">actions/setup-node#1332</a></li>
<li>Upgrade actions/checkout from 4 to 5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/setup-node/pull/1345">actions/setup-node#1345</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/priya-kinthali"><code>@​priya-kinthali</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1348">actions/setup-node#1348</a></li>
<li><a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1325">actions/setup-node#1325</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v4...v5.0.0">https://github.com/actions/setup-node/compare/v4...v5.0.0</a></p>
<h2>v4.4.0</h2>
<h2>What's Changed</h2>
<h3>Bug fixes:</h3>
<ul>
<li>Make eslint-compact matcher compatible with Stylelint by <a
href="https://github.com/FloEdelmann"><code>@​FloEdelmann</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/98">actions/setup-node#98</a></li>
<li>Add support for indented eslint output by <a
href="https://github.com/fregante"><code>@​fregante</code></a> in <a
href="https://redirect.github.com/actions/setup-node/pull/1245">actions/setup-node#1245</a></li>
</ul>
<h3>Enhancement:</h3>
<ul>
<li>Support private mirrors by <a
href="https://github.com/marco-ippolito"><code>@​marco-ippolito</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1240">actions/setup-node#1240</a></li>
</ul>
<h3>Dependency update:</h3>
<ul>
<li>Upgrade <code>@​action/cache</code> from 4.0.2 to 4.0.3 by <a
href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a>
in <a
href="https://redirect.github.com/actions/setup-node/pull/1262">actions/setup-node#1262</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/FloEdelmann"><code>@​FloEdelmann</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/98">actions/setup-node#98</a></li>
<li><a href="https://github.com/fregante"><code>@​fregante</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1245">actions/setup-node#1245</a></li>
<li><a
href="https://github.com/marco-ippolito"><code>@​marco-ippolito</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/setup-node/pull/1240">actions/setup-node#1240</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/setup-node/compare/v4...v4.4.0">https://github.com/actions/setup-node/compare/v4...v4.4.0</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="a0853c2454"><code>a0853c2</code></a>
Bump actions/checkout from 4 to 5 (<a
href="https://redirect.github.com/actions/setup-node/issues/1345">#1345</a>)</li>
<li><a
href="b7234cc9fe"><code>b7234cc</code></a>
Upgrade action to use node24 (<a
href="https://redirect.github.com/actions/setup-node/issues/1325">#1325</a>)</li>
<li><a
href="d7a11313b5"><code>d7a1131</code></a>
Enhance caching in setup-node with automatic package manager detection
(<a
href="https://redirect.github.com/actions/setup-node/issues/1348">#1348</a>)</li>
<li><a
href="5e2628c959"><code>5e2628c</code></a>
Bumps form-data (<a
href="https://redirect.github.com/actions/setup-node/issues/1332">#1332</a>)</li>
<li><a
href="65beceff8e"><code>65becef</code></a>
Bump undici from 5.28.5 to 5.29.0 (<a
href="https://redirect.github.com/actions/setup-node/issues/1295">#1295</a>)</li>
<li><a
href="7e24a656e1"><code>7e24a65</code></a>
Bump uuid from 9.0.1 to 11.1.0 (<a
href="https://redirect.github.com/actions/setup-node/issues/1273">#1273</a>)</li>
<li><a
href="08f58d1471"><code>08f58d1</code></a>
Bump <code>@​octokit/request-error</code> and
<code>@​actions/github</code> (<a
href="https://redirect.github.com/actions/setup-node/issues/1227">#1227</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/setup-node/compare/v4...v5">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-24 16:26:05 -04:00
Mason Daugherty
4783a9c18e style: update workflow name for version equality check (#33094) 2025-09-24 20:11:30 +00:00
Mason Daugherty
ee4d84de7c style(core): typo/docs lint pass (#33093) 2025-09-24 16:11:21 -04:00
Mason Daugherty
092dd5e174 chore: update link for monorepo structure (#33091) 2025-09-24 19:55:00 +00:00
Sydney Runkle
dd81e1c3fb release(langchain): 1.0.0a8 (#33090) 2025-09-24 15:31:29 -04:00
Sydney Runkle
135a5b97e6 feat(langchain): improvements to anthropic prompt caching (#33058)
Adding an `unsupported_model_behavior` arg that can be `'ignore'`,
`'warn'`, or `'raise'`. Defaults to `'warn'`.
2025-09-24 15:28:49 -04:00
Mason Daugherty
b92b394804 style: repo linting pass (#33089)
enable docstring-code-format
2025-09-24 15:25:55 -04:00
Sydney Runkle
083bb3cdd7 fix(langchain): need to inject all state for tools registered by middleware (#33087)
Type hints matter for conditional edges!
2025-09-24 15:25:51 -04:00
Mason Daugherty
2e9291cdd7 fix: lift openai version constraints across packages (#33088)
re: #33038 and https://github.com/openai/openai-python/issues/2644
2025-09-24 15:25:10 -04:00
Sydney Runkle
4f8a76b571 chore(langchain): renaming for HITL (#33067) 2025-09-24 07:19:44 -04:00
Mason Daugherty
05ba941230 style(cli): linting pass (#33078) 2025-09-24 01:24:52 -04:00
Mason Daugherty
ae4976896e chore: delete erroneous .readthedocs.yaml (#33079)
From the legacy docs/not needed here
2025-09-24 01:24:42 -04:00
Mason Daugherty
504ef96500 chore: add commit message generation instructions for VSCode (#33077) 2025-09-24 05:06:43 +00:00
Mason Daugherty
d99a02bb27 chore: add AGENTS.md (#33076)
it would be super cool if Anthropic supported this instead of
`CLAUDE.md` :/

https://agents.md/
2025-09-24 05:02:14 +00:00
Mason Daugherty
793de80429 chore: update label mapping in PR title labeler configuration (#33075) 2025-09-24 01:00:14 -04:00
Mason Daugherty
7d4e9d8cda revert(infra): put SECURITY.md at root (#33074) 2025-09-24 00:54:37 -04:00
Mason Daugherty
54dca494cf chore: delete erroneous poetry.toml configuration file (#33073)
- Not used by the current build system
- Potentially confusing for new contributors
- A leftover artifact from the Poetry to uv migration
2025-09-24 04:40:17 +00:00
Mason Daugherty
7b30e58386 chore: delete erroneous yarn.lock in root (#33072)
Appears to have had no purpose/was added by mistake and nobody
questioned it
2025-09-24 04:35:00 +00:00
Mason Daugherty
e62b541dfd chore(infra): move SECURITY.md to .github (#33071)
cleaning up top-level. `.github` folder placement will continue to show
on repo homepage:
https://docs.github.com/en/code-security/getting-started/adding-a-security-policy-to-your-repository#about-security-policies
2025-09-24 00:27:48 -04:00
Mason Daugherty
8699980d09 chore(scripts): remove obsolete release and mypy/ruff update scripts (#33070)
Outdated scripts related to release management and mypy/ruff updates

Cleaning up the root-level
2025-09-24 04:24:38 +00:00
Mason Daugherty
79e536b0d6 chore(infra): further docs build cleanup (#33057)
Reorganize the requirements for better clarity and consistency. Improve
documentation on scripts and workflows.
2025-09-23 17:29:58 -04:00
Sydney Runkle
b5720ff17a chore(langchain): simplifying HITL condition (#33065)
Simplifying condition
2025-09-23 21:24:14 +00:00
nhuang-lc
48b05224ad fix(langchain_v1): only interrupt if at least one ToolConfig value is True (#33064)
**Description:** Right now, we interrupt even if the provided ToolConfig
has all false values. We should ignore ToolConfigs which do not have at
least one value marked as true (just as we would if tool_name: False was
passed into the dict).
2025-09-23 17:20:34 -04:00
Sydney Runkle
89079ad411 feat(langchain): new decorator pattern for dynamically generated middleware (#33053)
# Main Changes

1. Adding decorator utilities for dynamically defining middleware with
single hook functions (see an example below for dynamic system prompt)
2. Adding better conditional edge drawing with jump configuration
attached to middleware. Can be registered w/ the decorator new
decorator!

## Decorator Utilities

```py
from langchain.agents.middleware_agent import create_agent, AgentState, ModelRequest
from langchain.agents.middleware.types import modify_model_request
from langchain_core.messages import HumanMessage
from langgraph.checkpoint.memory import InMemorySaver


@modify_model_request
def modify_system_prompt(request: ModelRequest, state: AgentState) -> ModelRequest:
    request.system_prompt = (
        "You are a helpful assistant."
        f"Please record the number of previous messages in your response: {len(state['messages'])}"
    )
    return request

agent = create_agent(
    model="openai:gpt-4o-mini", 
    middleware=[modify_system_prompt]
).compile(checkpointer=InMemorySaver())
```

## Visualization and Routing improvements

We now require that middlewares define the valid jumps for each hook.

If using the new decorator syntax, this can be done with:

```py
@before_model(jump_to=["__end__"])
@after_model(jump_to=["tools", "__end__"])
```

If using the subclassing syntax, you can use these two class vars:

```py
class MyMiddlewareAgentMiddleware):
    before_model_jump_to = ["__end__"]
    after_model_jump_to = ["tools", "__end__"]
```

Open for debate if we want to bundle these in a single jump map / config
for a middleware. Easy to migrate later if we decide to add more hooks.

We will need to **really clearly document** that these must be
explicitly set in order to enable conditional edges.

Notice for the below case, `Middleware2` does actually enable jumps.

<table>
  <thead>
    <tr>
      <th>Before (broken), adding conditional edges unconditionally</th>
      <th>After (fixed), adding conditional edges sparingly</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>
<img width="619" height="508" alt="Screenshot 2025-09-23 at 10 23 23 AM"
src="https://github.com/user-attachments/assets/bba2d098-a839-4335-8e8c-b50dd8090959"
/>
      </td>
      <td>
<img width="469" height="490" alt="Screenshot 2025-09-23 at 10 23 13 AM"
src="https://github.com/user-attachments/assets/717abf0b-fc73-4d5f-9313-b81247d8fe26"
/>
      </td>
    </tr>
  </tbody>
</table>

<details>
<summary>Snippet for the above</summary>

```py
from typing import Any
from langchain.agents.tool_node import InjectedState
from langgraph.runtime import Runtime
from langchain.agents.middleware.types import AgentMiddleware, AgentState
from langchain.agents.middleware_agent import create_agent
from langchain_core.tools import tool
from typing import Annotated
from langchain_core.messages import HumanMessage
from typing_extensions import NotRequired

@tool
def simple_tool(input: str) -> str:
    """A simple tool."""
    return "successful tool call"


class Middleware1(AgentMiddleware):
    """Custom middleware that adds a simple tool."""

    tools = [simple_tool]

    def before_model(self, state: AgentState, runtime: Runtime) -> None:
        return None

    def after_model(self, state: AgentState, runtime: Runtime) -> None:
        return None

class Middleware2(AgentMiddleware):

    before_model_jump_to = ["tools", "__end__"]

    def before_model(self, state: AgentState, runtime: Runtime) -> None:
        return None

    def after_model(self, state: AgentState, runtime: Runtime) -> None:
        return None

class Middleware3(AgentMiddleware):

    def before_model(self, state: AgentState, runtime: Runtime) -> None:
        return None

    def after_model(self, state: AgentState, runtime: Runtime) -> None:
        return None

builder = create_agent(
    model="openai:gpt-4o-mini",
    middleware=[Middleware1(), Middleware2(), Middleware3()],
    system_prompt="You are a helpful assistant.",
)
agent = builder.compile()
```

</details>

## More Examples

### Guardrails `after_model`

<img width="379" height="335" alt="Screenshot 2025-09-23 at 10 40 09 AM"
src="https://github.com/user-attachments/assets/45bac7dd-398e-45d1-ae58-6ecfa27dfc87"
/>

<details>
<summary>Code</summary>

```py
from langchain.agents.middleware_agent import create_agent, AgentState, ModelRequest
from langchain.agents.middleware.types import after_model
from langchain_core.messages import HumanMessage, AIMessage
from langgraph.checkpoint.memory import InMemorySaver
from typing import cast, Any

@after_model(jump_to=["model", "__end__"])
def after_model_hook(state: AgentState) -> dict[str, Any]:
    """Check the last AI message for safety violations."""
    last_message_content = cast(AIMessage, state["messages"][-1]).content.lower()
    print(last_message_content)

    unsafe_keywords = ["pineapple"]
    if any(keyword in last_message_content for keyword in unsafe_keywords):

        # Jump back to model to regenerate response
        return {"jump_to": "model", "messages": [HumanMessage("Please regenerate your response, and don't talk about pineapples. You can talk about apples instead.")]}

    return {"jump_to": "__end__"}

# Create agent with guardrails middleware
agent = create_agent(
    model="openai:gpt-4o-mini",
    middleware=[after_model_hook],
    system_prompt="Keep your responses to one sentence please!"
).compile()

# Test with potentially unsafe input
result = agent.invoke(
    {"messages": [HumanMessage("Tell me something about pineapples")]},
)

for msg in result["messages"]:
    print(msg.pretty_print())

"""
================================ Human Message =================================

Tell me something about pineapples
None
================================== Ai Message ==================================

Pineapples are tropical fruits known for their sweet, tangy flavor and distinctive spiky exterior.
None
================================ Human Message =================================

Please regenerate your response, and don't talk about pineapples. You can talk about apples instead.
None
================================== Ai Message ==================================

Apples are popular fruits that come in various varieties, known for their crisp texture and sweetness, and are often used in cooking and baking.
None
"""
```

</details>
2025-09-23 13:25:55 -04:00
Mason Daugherty
2c95586f2a chore(infra): audit workflows, scripts (#33055)
Mostly adding a descriptive frontmatter to workflow files. Also address
some formatting and outdated artifacts

No functional changes outside of
[d5457c3](d5457c39ee),
[90708a0](90708a0d99),
and
[338c82d](338c82d21e)
2025-09-23 17:08:19 +00:00
Mason Daugherty
9c1285cf5b chore(infra): fix ping pong pr labeler config (#33054)
The title-based labeler was clearing all pre-existing labels (including
the file-based ones) before adding its semantic labels.
2025-09-22 21:19:53 -04:00
Sydney Runkle
c3be45bf14 fix(langchain): HITL bug causing dupe interrupt (#33052)
Need to find **last** AI msg (not first). Getting too creative w/
generators.
2025-09-22 20:09:12 -04:00
Arman Tsaturian
8f488d62b2 docs: fix stripe toolkit import in the guide (#33044)
**Description:**
Stripe tools integration guide incorrectly referenced the `crewai`
toolkit. Updated the import to use the correct `langchain` toolkit.

Stripe docs reference:
https://docs.stripe.com/agents?framework=langchain&lang=python
2025-09-22 15:17:09 -04:00
Mason Daugherty
cdae9e4942 fix(infra): prevent labeler workflow from adding/removing same labels (#33039)
The file-based and title-based labeler workflows were conflicting,
causing the bot to add and remove identical labels in the same
operation. Hopefully this fixes
2025-09-21 04:37:59 +00:00
Mason Daugherty
7ddc798f95 fix(openai): pin upper bound to prevent Pydantic 2.7.0 issues (#33038)
https://github.com/openai/openai-python/issues/2644
2025-09-21 00:27:03 -04:00
Mason Daugherty
7dcf6a515e fix: update method calls from dict to model_dump in Chain (#33035) 2025-09-20 23:47:44 -04:00
Mason Daugherty
043a7560a5 test: use .get() for safe ls_params access (#33034) 2025-09-20 23:46:37 -04:00
Mason Daugherty
5b418d3f26 feat(infra): add PR labeler configurations and workflows (#33031) 2025-09-20 22:33:08 -04:00
Mason Daugherty
6b4054c795 chore(infra): update pre-commit hooks to include linting (#33029) 2025-09-21 02:26:19 +00:00
Mason Daugherty
30fde5af38 chore(infra): remove couchbase formatting hook from pre-commit (#33030)
Should've been done when it was removed from the monorepo
2025-09-20 22:09:57 -04:00
Mason Daugherty
781db9d892 chore: update pyproject.toml files, remove codespell (#33028)
- Removes Codespell from deps, docs, and `Makefile`s
- Python version requirements in all `pyproject.toml` files now use the
`~=` (compatible release) specifier
- All dependency groups and main dependencies now use explicit lower and
upper bounds, reducing potential for breaking changes
2025-09-20 22:09:33 -04:00
Sydney Runkle
f2b0afd0b7 release(langchain): 1.0.0a6 (#33024)
w/ improvements to HITL, state schema merging, dynamic system prompt
2025-09-19 18:47:41 +00:00
Sydney Runkle
c3654202a3 fix(langchain): use state schema as input schema to middleware nodes (#33023)
We want state schema as the input schema to middleware nodes because the
conditional edges after these nodes need access to the full state.

Also, we just generally want all state passed to middleware nodes, so we
should be specifying this explicitly. If we don't, the state annotations
used by users in their node signatures are used (so they might be
missing fields).
2025-09-19 18:43:33 +00:00
Sydney Runkle
4d118777bc feat(langchain): dynamic system prompt middleware (#33006)
# Changes

## Adds support for `DynamicSystemPromptMiddleware`

```py
from langchain.agents.middleware import DynamicSystemPromptMiddleware
from langgraph.runtime import Runtime
from typing_extensions import TypedDict

class Context(TypedDict):
    user_name: str

def system_prompt(state: AgentState, runtime: Runtime[Context]) -> str:
    user_name = runtime.context.get("user_name", "n/a")
    return f"You are a helpful assistant. Always address the user by their name: {user_name}"

middleware = DynamicSystemPromptMiddleware(system_prompt)
```

## Adds support for `runtime` in middleware hooks

```py
class AgentMiddleware(Generic[StateT, ContextT]):
    def modify_model_request(
        self,
        request: ModelRequest,
        state: StateT,
        runtime: Runtime[ContextT],  # Optional runtime parameter
    ) -> ModelRequest:
        # upgrade model if runtime.context.subscription is `top-tier` or whatever
```

## Adds support for omitting state attributes from input / output
schemas

```py
from typing import Annotated, NotRequired
from langchain.agents.middleware.types import PrivateStateAttr, OmitFromInput, OmitFromOutput

class CustomState(AgentState):
    # Private field - not in input or output schemas
    internal_counter: NotRequired[Annotated[int, PrivateStateAttr]]
    
    # Input-only field - not in output schema
    user_input: NotRequired[Annotated[str, OmitFromOutput]]
    
    # Output-only field - not in input schema  
    computed_result: NotRequired[Annotated[str, OmitFromInput]]
```

## Additionally
* Removes filtering of state before passing into middleware hooks

Typing is not foolproof here, still need to figure out some of the
generics stuff w/ state and context schema extensions for middleware.

TODO:
* More docs for middleware, should hold off on this until other prios
like MCP and deepagents are met

---------

Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
2025-09-18 16:07:16 -04:00
339 changed files with 21182 additions and 14035 deletions

View File

@@ -1,4 +1,6 @@
# Adapted from https://github.com/tiangolo/fastapi/blob/master/.github/actions/people/action.yml
# TODO: fix this, migrate to new docs repo?
name: "Generate LangChain People"
description: "Generate the data for the LangChain People page"
author: "Jacob Lee <jacob@langchain.dev>"

View File

@@ -1,3 +1,5 @@
# Helper to set up Python and uv with caching
name: uv-install
description: Set up Python and uv with caching
@@ -8,15 +10,15 @@ inputs:
enable-cache:
description: Enable caching for uv dependencies
required: false
default: 'true'
default: "true"
cache-suffix:
description: Custom cache key suffix for cache invalidation
required: false
default: ''
default: ""
working-directory:
description: Working directory for cache glob scoping
required: false
default: '**'
default: "**"
env:
UV_VERSION: "0.5.25"

80
.github/pr-file-labeler.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
# Label PRs (config)
# Automatically applies labels based on changed files and branch patterns
# Core packages
core:
- changed-files:
- any-glob-to-any-file:
- "libs/core/**/*"
langchain:
- changed-files:
- any-glob-to-any-file:
- "libs/langchain/**/*"
- "libs/langchain_v1/**/*"
v1:
- changed-files:
- any-glob-to-any-file:
- "libs/langchain_v1/**/*"
cli:
- changed-files:
- any-glob-to-any-file:
- "libs/cli/**/*"
standard-tests:
- changed-files:
- any-glob-to-any-file:
- "libs/standard-tests/**/*"
# Partner integrations
integration:
- changed-files:
- any-glob-to-any-file:
- "libs/partners/**/*"
# Infrastructure and DevOps
infra:
- changed-files:
- any-glob-to-any-file:
- ".github/**/*"
- "Makefile"
- ".pre-commit-config.yaml"
- "scripts/**/*"
- "docker/**/*"
- "Dockerfile*"
github_actions:
- changed-files:
- any-glob-to-any-file:
- ".github/workflows/**/*"
- ".github/actions/**/*"
dependencies:
- changed-files:
- any-glob-to-any-file:
- "**/pyproject.toml"
- "uv.lock"
- "**/requirements*.txt"
- "**/poetry.lock"
# Documentation
documentation:
- changed-files:
- any-glob-to-any-file:
- "docs/**/*"
- "**/*.md"
- "**/*.rst"
- "**/README*"
# Security related changes
security:
- changed-files:
- any-glob-to-any-file:
- "**/*security*"
- "**/*auth*"
- "**/*credential*"
- "**/*secret*"
- "**/*token*"
- ".github/workflows/security*"

41
.github/pr-title-labeler.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
# 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

@@ -1,3 +1,18 @@
"""Analyze git diffs to determine which directories need to be tested.
Intelligently determines which LangChain packages and directories need to be tested,
linted, or built based on the changes. Handles dependency relationships between
packages, maps file changes to appropriate CI job configurations, and outputs JSON
configurations for GitHub Actions.
- Maps changed files to affected package directories (libs/core, libs/partners/*, etc.)
- Builds dependency graph to include dependent packages when core components change
- Generates test matrix configurations with appropriate Python versions
- Handles special cases for Pydantic version testing and performance benchmarks
Used as part of the check_diffs workflow.
"""
import glob
import json
import os
@@ -17,7 +32,7 @@ LANGCHAIN_DIRS = [
"libs/langchain_v1",
]
# when set to True, we are ignoring core dependents
# 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
@@ -49,9 +64,9 @@ def all_package_dirs() -> Set[str]:
def dependents_graph() -> dict:
"""
Construct a mapping of package -> dependents, such that we can
run tests on all dependents of a package when a change is made.
"""Construct a mapping of package -> dependents
Done such that we can run tests on all dependents of a package when a change is made.
"""
dependents = defaultdict(set)
@@ -123,9 +138,6 @@ def _get_configs_for_single_dir(job: str, dir_: str) -> List[Dict[str, str]]:
elif dir_ == "libs/core":
py_versions = ["3.9", "3.10", "3.11", "3.12", "3.13"]
# custom logic for specific directories
elif dir_ == "libs/partners/milvus":
# milvus doesn't allow 3.12 because they declare deps in funny way
py_versions = ["3.9", "3.11"]
elif dir_ in PY_312_MAX_PACKAGES:
py_versions = ["3.9", "3.12"]

View File

@@ -1,3 +1,5 @@
"""Check that no dependencies allow prereleases unless we're releasing a prerelease."""
import sys
import tomllib
@@ -6,15 +8,14 @@ if __name__ == "__main__":
# Get the TOML file path from the command line argument
toml_file = sys.argv[1]
# read toml file
with open(toml_file, "rb") as file:
toml_data = tomllib.load(file)
# see if we're releasing an rc
# See if we're releasing an rc or dev version
version = toml_data["project"]["version"]
releasing_rc = "rc" in version or "dev" in version
# if not, iterate through dependencies and make sure none allow prereleases
# If not, iterate through dependencies and make sure none allow prereleases
if not releasing_rc:
dependencies = toml_data["project"]["dependencies"]
for dep_version in dependencies:

View File

@@ -1,3 +1,5 @@
"""Get minimum versions of dependencies from a pyproject.toml file."""
import sys
from collections import defaultdict
from typing import Optional
@@ -5,7 +7,7 @@ from typing import Optional
if sys.version_info >= (3, 11):
import tomllib
else:
# for python 3.10 and below, which doesnt have stdlib tomllib
# For Python 3.10 and below, which doesnt have stdlib tomllib
import tomli as tomllib
import re
@@ -34,14 +36,13 @@ SKIP_IF_PULL_REQUEST = [
def get_pypi_versions(package_name: str) -> List[str]:
"""
Fetch all available versions for a package from PyPI.
"""Fetch all available versions for a package from PyPI.
Args:
package_name (str): Name of the package
package_name: Name of the package
Returns:
List[str]: List of all available versions
List of all available versions
Raises:
requests.exceptions.RequestException: If PyPI API request fails
@@ -54,24 +55,23 @@ def get_pypi_versions(package_name: str) -> List[str]:
def get_minimum_version(package_name: str, spec_string: str) -> Optional[str]:
"""
Find the minimum published version that satisfies the given constraints.
"""Find the minimum published version that satisfies the given constraints.
Args:
package_name (str): Name of the package
spec_string (str): Version specification string (e.g., ">=0.2.43,<0.4.0,!=0.3.0")
package_name: Name of the package
spec_string: Version specification string (e.g., ">=0.2.43,<0.4.0,!=0.3.0")
Returns:
Optional[str]: Minimum compatible version or None if no compatible version found
Minimum compatible version or None if no compatible version found
"""
# rewrite occurrences of ^0.0.z to 0.0.z (can be anywhere in constraint string)
# Rewrite occurrences of ^0.0.z to 0.0.z (can be anywhere in constraint string)
spec_string = re.sub(r"\^0\.0\.(\d+)", r"0.0.\1", spec_string)
# rewrite occurrences of ^0.y.z to >=0.y.z,<0.y+1 (can be anywhere in constraint string)
# Rewrite occurrences of ^0.y.z to >=0.y.z,<0.y+1 (can be anywhere in constraint string)
for y in range(1, 10):
spec_string = re.sub(
rf"\^0\.{y}\.(\d+)", rf">=0.{y}.\1,<0.{y + 1}", spec_string
)
# rewrite occurrences of ^x.y.z to >=x.y.z,<x+1.0.0 (can be anywhere in constraint string)
# Rewrite occurrences of ^x.y.z to >=x.y.z,<x+1.0.0 (can be anywhere in constraint string)
for x in range(1, 10):
spec_string = re.sub(
rf"\^{x}\.(\d+)\.(\d+)", rf">={x}.\1.\2,<{x + 1}", spec_string
@@ -154,22 +154,25 @@ def get_min_version_from_toml(
def check_python_version(version_string, constraint_string):
"""
Check if the given Python version matches the given constraints.
"""Check if the given Python version matches the given constraints.
:param version_string: A string representing the Python version (e.g. "3.8.5").
:param constraint_string: A string representing the package's Python version constraints (e.g. ">=3.6, <4.0").
:return: True if the version matches the constraints, False otherwise.
Args:
version_string: A string representing the Python version (e.g. "3.8.5").
constraint_string: A string representing the package's Python version
constraints (e.g. ">=3.6, <4.0").
Returns:
True if the version matches the constraints
"""
# rewrite occurrences of ^0.0.z to 0.0.z (can be anywhere in constraint string)
# Rewrite occurrences of ^0.0.z to 0.0.z (can be anywhere in constraint string)
constraint_string = re.sub(r"\^0\.0\.(\d+)", r"0.0.\1", constraint_string)
# rewrite occurrences of ^0.y.z to >=0.y.z,<0.y+1.0 (can be anywhere in constraint string)
# Rewrite occurrences of ^0.y.z to >=0.y.z,<0.y+1.0 (can be anywhere in constraint string)
for y in range(1, 10):
constraint_string = re.sub(
rf"\^0\.{y}\.(\d+)", rf">=0.{y}.\1,<0.{y + 1}.0", constraint_string
)
# rewrite occurrences of ^x.y.z to >=x.y.z,<x+1.0.0 (can be anywhere in constraint string)
# Rewrite occurrences of ^x.y.z to >=x.y.z,<x+1.0.0 (can be anywhere in constraint string)
for x in range(1, 10):
constraint_string = re.sub(
rf"\^{x}\.0\.(\d+)", rf">={x}.0.\1,<{x + 1}.0.0", constraint_string

View File

@@ -1,5 +1,8 @@
#!/usr/bin/env python
"""Script to sync libraries from various repositories into the main langchain repository."""
"""Sync libraries from various repositories into this monorepo.
Moves cloned partner packages into libs/partners structure.
"""
import os
import shutil
@@ -10,7 +13,7 @@ import yaml
def load_packages_yaml() -> Dict[str, Any]:
"""Load and parse the packages.yml file."""
"""Load and parse packages.yml."""
with open("langchain/libs/packages.yml", "r") as f:
return yaml.safe_load(f)
@@ -61,12 +64,15 @@ def move_libraries(packages: list) -> None:
def main():
"""Main function to orchestrate the library sync process."""
"""Orchestrate the library sync process."""
try:
# Load packages configuration
package_yaml = load_packages_yaml()
# Clean target directories
# Clean/empty target directories in preparation for moving new ones
#
# Only for packages in the langchain-ai org or explicitly included via
# include_in_api_ref, excluding 'langchain' itself and 'langchain-ai21'
clean_target_directories(
[
p
@@ -80,7 +86,9 @@ def main():
]
)
# Move libraries to their new locations
# Move cloned libraries to their new locations, only for packages in the
# langchain-ai org or explicitly included via include_in_api_ref,
# excluding 'langchain' itself and 'langchain-ai21'
move_libraries(
[
p
@@ -95,7 +103,7 @@ def main():
]
)
# Delete ones without a pyproject.toml
# Delete partner packages without a pyproject.toml
for partner in Path("langchain/libs/partners").iterdir():
if partner.is_dir() and not (partner / "pyproject.toml").exists():
print(f"Removing {partner} as it does not have a pyproject.toml")

View File

@@ -1,3 +1,11 @@
# Validates that a package's integration tests compile without syntax or import errors.
#
# (If an integration test fails to compile, it won't run.)
#
# Called as part of check_diffs.yml workflow
#
# Runs pytest with compile marker to check syntax/imports.
name: '🔗 Compile Integration Tests'
on:

View File

@@ -1,3 +1,10 @@
# Runs `make integration_tests` on the specified package.
#
# Manually triggered via workflow_dispatch for testing with real APIs.
#
# Installs integration test dependencies and executes full test suite.
name: '🚀 Integration Tests'
run-name: 'Test ${{ inputs.working-directory }} on Python ${{ inputs.python-version }}'
@@ -83,7 +90,7 @@ jobs:
run: |
make integration_tests
- name: Ensure the tests did not create any additional files
- name: 'Ensure testing did not create/modify files'
shell: bash
run: |
set -eu

View File

@@ -1,6 +1,11 @@
name: '🧹 Code Linting'
# Runs code quality checks using ruff, mypy, and other linting tools
# Checks both package code and test code for consistency
# Runs linting.
#
# Uses the package's Makefile to run the checks, specifically the
# `lint_package` and `lint_tests` targets.
#
# Called as part of check_diffs.yml workflow.
name: '🧹 Linting'
on:
workflow_call:
@@ -43,14 +48,6 @@ jobs:
working-directory: ${{ inputs.working-directory }}
- name: '📦 Install Lint & Typing Dependencies'
# Also installs dev/lint/test/typing dependencies, to ensure we have
# type hints for as many of our libraries as possible.
# This helps catch errors that require dependencies to be spotted, for example:
# https://github.com/langchain-ai/langchain/pull/10249/files#diff-935185cd488d015f026dcd9e19616ff62863e8cde8c0bee70318d3ccbca98341
#
# If you change this configuration, make sure to change the `cache-key`
# in the `poetry_setup` action above to stop using the old cache.
# It doesn't matter how you change it, any change will cause a cache-bust.
working-directory: ${{ inputs.working-directory }}
run: |
uv sync --group lint --group typing
@@ -60,20 +57,13 @@ jobs:
run: |
make lint_package
- name: '📦 Install Unit Test Dependencies'
# Also installs dev/lint/test/typing dependencies, to ensure we have
# type hints for as many of our libraries as possible.
# This helps catch errors that require dependencies to be spotted, for example:
# https://github.com/langchain-ai/langchain/pull/10249/files#diff-935185cd488d015f026dcd9e19616ff62863e8cde8c0bee70318d3ccbca98341
#
# If you change this configuration, make sure to change the `cache-key`
# in the `poetry_setup` action above to stop using the old cache.
# It doesn't matter how you change it, any change will cause a cache-bust.
- name: '📦 Install Test Dependencies (non-partners)'
# (For directories NOT starting with libs/partners/)
if: ${{ ! startsWith(inputs.working-directory, 'libs/partners/') }}
working-directory: ${{ inputs.working-directory }}
run: |
uv sync --inexact --group test
- name: '📦 Install Unit + Integration Test Dependencies'
- name: '📦 Install Test Dependencies'
if: ${{ startsWith(inputs.working-directory, 'libs/partners/') }}
working-directory: ${{ inputs.working-directory }}
run: |

View File

@@ -1,3 +1,9 @@
# Builds and publishes LangChain packages to PyPI.
#
# Manually triggered, though can be used as a reusable workflow (workflow_call).
#
# Handles version bumping, building, and publishing to PyPI with authentication.
name: '🚀 Package Release'
run-name: 'Release ${{ inputs.working-directory }} ${{ inputs.release-version }}'
on:
@@ -52,8 +58,8 @@ jobs:
# We want to keep this build stage *separate* from the release stage,
# so that there's no sharing of permissions between them.
# The release stage has trusted publishing and GitHub repo contents write access,
# and we want to keep the scope of that access limited just to the release job.
# (Release stage has trusted publishing and GitHub repo contents write access,
#
# Otherwise, a malicious `build` step (e.g. via a compromised dependency)
# could get access to our GitHub or PyPI credentials.
#
@@ -288,16 +294,19 @@ jobs:
run: |
VIRTUAL_ENV=.venv uv pip install dist/*.whl
- name: Run unit tests
run: make tests
working-directory: ${{ inputs.working-directory }}
- name: Check for prerelease versions
# Block release if any dependencies allow prerelease versions
# (unless this is itself a prerelease version)
working-directory: ${{ inputs.working-directory }}
run: |
uv run python $GITHUB_WORKSPACE/.github/scripts/check_prerelease_dependencies.py pyproject.toml
- name: Run unit tests
run: make tests
working-directory: ${{ inputs.working-directory }}
- name: Get minimum versions
# Find the minimum published versions that satisfies the given constraints
working-directory: ${{ inputs.working-directory }}
id: min-version
run: |
@@ -322,6 +331,7 @@ jobs:
working-directory: ${{ inputs.working-directory }}
- name: Run integration tests
# Uses the Makefile's `integration_tests` target for the specified package
if: ${{ startsWith(inputs.working-directory, 'libs/partners/') }}
env:
AI21_API_KEY: ${{ secrets.AI21_API_KEY }}
@@ -362,7 +372,10 @@ jobs:
working-directory: ${{ inputs.working-directory }}
# Test select published packages against new core
# Done when code changes are made to langchain-core
test-prior-published-packages-against-new-core:
# Installs the new core with old partners: Installs the new unreleased core
# alongside the previously published partner packages and runs integration tests
needs:
- build
- release-notes
@@ -390,6 +403,7 @@ jobs:
# We implement this conditional as Github Actions does not have good support
# for conditionally needing steps. https://github.com/actions/runner/issues/491
# TODO: this seems to be resolved upstream, so we can probably remove this workaround
- name: Check if libs/core
run: |
if [ "${{ startsWith(inputs.working-directory, 'libs/core') }}" != "true" ]; then
@@ -444,6 +458,7 @@ jobs:
make integration_tests
publish:
# Publishes the package to PyPI
needs:
- build
- release-notes
@@ -486,6 +501,7 @@ jobs:
attestations: false
mark-release:
# Marks the GitHub release with the new version tag
needs:
- build
- release-notes
@@ -495,7 +511,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
# This permission is needed by `ncipollo/release-action` to
# create the GitHub release.
# create the GitHub release/tag
contents: write
defaults:

View File

@@ -1,6 +1,7 @@
name: '🧪 Unit Testing'
# Runs unit tests with both current and minimum supported dependency versions
# to ensure compatibility across the supported range
# to ensure compatibility across the supported range.
name: '🧪 Unit Testing'
on:
workflow_call:

View File

@@ -1,3 +1,10 @@
# Validates that all import statements in `.ipynb` notebooks are correct and functional.
#
# Called as part of check_diffs.yml.
#
# Installs test dependencies and LangChain packages in editable mode and
# runs check_imports.py.
name: '📑 Documentation Import Testing'
on:

View File

@@ -1,3 +1,5 @@
# Facilitate unit testing against different Pydantic versions for a provided package.
name: '🐍 Pydantic Version Testing'
on:

View File

@@ -1,11 +1,19 @@
# Build the API reference documentation.
#
# Runs daily. Can also be triggered manually for immediate updates.
#
# Built HTML pushed to langchain-ai/langchain-api-docs-html.
#
# Looks for langchain-ai org repos in packages.yml and checks them out.
# Calls prep_api_docs_build.py.
name: '📚 API Docs'
run-name: 'Build & Deploy API Reference'
# Runs daily or can be triggered manually for immediate updates
on:
workflow_dispatch:
schedule:
- cron: '0 13 * * *' # Daily at 1PM UTC
- cron: '0 13 * * *' # Runs daily at 1PM UTC (9AM EDT/6AM PDT)
env:
PYTHON_VERSION: "3.11"
@@ -31,6 +39,8 @@ jobs:
uses: mikefarah/yq@master
with:
cmd: |
# Extract repos from packages.yml that are in the langchain-ai org
# (excluding 'langchain' itself)
yq '
.packages[]
| select(
@@ -77,24 +87,31 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: '📦 Install Initial Python Dependencies'
- name: '📦 Install Initial Python Dependencies using uv'
working-directory: langchain
run: |
python -m pip install -U uv
python -m uv pip install --upgrade --no-cache-dir pip setuptools pyyaml
- name: '📦 Organize Library Directories'
# Places cloned partner packages into libs/partners structure
run: python langchain/.github/scripts/prep_api_docs_build.py
- name: '🧹 Remove Old HTML Files'
- name: '🧹 Clear Prior Build'
run:
# Remove artifacts from prior docs build
rm -rf langchain-api-docs-html/api_reference_build/html
- name: '📦 Install Documentation Dependencies'
- name: '📦 Install Documentation Dependencies using uv'
working-directory: langchain
run: |
# Install all partner packages in editable mode with overrides
python -m uv pip install $(ls ./libs/partners | xargs -I {} echo "./libs/partners/{}") --overrides ./docs/vercel_overrides.txt
# Install core langchain and other main packages
python -m uv pip install libs/core libs/langchain libs/text-splitters libs/community libs/experimental libs/standard-tests
# Install Sphinx and related packages for building docs
python -m uv pip install -r docs/api_reference/requirements.txt
- name: '🔧 Configure Git Settings'
@@ -106,14 +123,29 @@ jobs:
- name: '📚 Build API Documentation'
working-directory: langchain
run: |
# Generate the API reference RST files
python docs/api_reference/create_api_rst.py
# Build the HTML documentation using Sphinx
# -T: show full traceback on exception
# -E: don't use cached environment (force rebuild, ignore cached doctrees)
# -b html: build HTML docs (vs PDS, etc.)
# -d: path for the cached environment (parsed document trees / doctrees)
# - Separate from output dir for faster incremental builds
# -c: path to conf.py
# -j auto: parallel build using all available CPU cores
python -m sphinx -T -E -b html -d ../langchain-api-docs-html/_build/doctrees -c docs/api_reference docs/api_reference ../langchain-api-docs-html/api_reference_build/html -j auto
# Post-process the generated HTML
python docs/api_reference/scripts/custom_formatter.py ../langchain-api-docs-html/api_reference_build/html
# Default index page is blank so we copy in the actual home page.
cp ../langchain-api-docs-html/api_reference_build/html/{reference,index}.html
# Removes Sphinx's intermediate build artifacts after the build is complete.
rm -rf ../langchain-api-docs-html/_build/
# https://github.com/marketplace/actions/add-commit
# Commit and push changes to langchain-api-docs-html repo
- uses: EndBug/add-and-commit@v9
with:
cwd: langchain-api-docs-html

View File

@@ -1,9 +1,11 @@
# Runs broken link checker in /docs on a daily schedule.
name: '🔗 Check Broken Links'
on:
workflow_dispatch:
schedule:
- cron: '0 13 * * *'
- cron: '0 13 * * *' # Runs daily at 1PM UTC (9AM EDT/6AM PDT)
permissions:
contents: read
@@ -15,7 +17,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: '🟢 Setup Node.js 18.x'
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 18.x
cache: "yarn"

View File

@@ -1,6 +1,8 @@
name: '🔍 Check `core` Version Equality'
# Ensures version numbers in pyproject.toml and version.py stay in sync
# Prevents releases with mismatched version numbers
# Ensures version numbers in pyproject.toml and version.py stay in sync.
#
# (Prevents releases with mismatched version numbers)
name: '🔍 Check Version Equality'
on:
pull_request:

View File

@@ -1,3 +1,18 @@
# Primary CI workflow.
#
# Only runs against packages that have changed files.
#
# Runs:
# - Linting (_lint.yml)
# - Unit Tests (_test.yml)
# - Pydantic compatibility tests (_test_pydantic.yml)
# - Documentation import tests (_test_doc_imports.yml)
# - Integration test compilation checks (_compile_integration_test.yml)
# - Extended test suites that require additional dependencies
# - Codspeed benchmarks (if not labeled 'codspeed-ignore')
#
# Reports status to GitHub checks and PR status.
name: '🔧 CI'
on:
@@ -11,8 +26,8 @@ on:
# cancel the earlier run in favor of the next run.
#
# There's no point in testing an outdated version of the code. GitHub only allows
# a limited number of job runners to be active at the same time, so it's better to cancel
# pointless jobs early so that more useful jobs can run sooner.
# a limited number of job runners to be active at the same time, so it's better to
# cancel pointless jobs early so that more useful jobs can run sooner.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -111,6 +126,7 @@ jobs:
# Verify integration tests compile without actually running them (faster feedback)
compile-integration-tests:
name: 'Compile Integration Tests'
needs: [ build ]
if: ${{ needs.build.outputs.compile-integration-tests != '[]' }}
strategy:

View File

@@ -1,3 +1,6 @@
# For integrations, we run check_templates.py to ensure that new docs use the correct
# templates based on their type. See the script for more details.
name: '📑 Integration Docs Lint'
on:

View File

@@ -1,10 +0,0 @@
import toml
pyproject_toml = toml.load("pyproject.toml")
# Extract the ignore words list (adjust the key as per your TOML structure)
ignore_words_list = (
pyproject_toml.get("tool", {}).get("codespell", {}).get("ignore-words-list")
)
print(f"::set-output name=ignore_words_list::{ignore_words_list}")

View File

@@ -1,9 +1,11 @@
# Updates the LangChain People data by fetching the latest info from the LangChain Git.
# TODO: broken/not used
name: '👥 LangChain People'
run-name: 'Update People Data'
# This workflow updates the LangChain People data by fetching the latest information from the LangChain Git
on:
schedule:
- cron: "0 14 1 * *"
- cron: "0 14 1 * *" # Runs at 14:00 UTC on the 1st of every month (10AM EDT/7AM PDT)
push:
branches: [jacob/people]
workflow_dispatch:

28
.github/workflows/pr_labeler_file.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
# Label PRs based on changed files.
#
# See `.github/pr-file-labeler.yml` to see rules for each label/directory.
name: "🏷️ Pull Request Labeler"
on:
# Safe since we're not checking out or running the PR's code
# Never check out the PR's head in a pull_request_target job
pull_request_target:
types: [opened, synchronize, reopened, edited]
jobs:
labeler:
name: 'label'
permissions:
contents: read
pull-requests: write
issues: write
runs-on: ubuntu-latest
steps:
- name: Label Pull Request
uses: actions/labeler@v6
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
configuration-path: .github/pr-file-labeler.yml
sync-labels: false

28
.github/workflows/pr_labeler_title.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
# Label PRs based on their titles.
#
# See `.github/pr-title-labeler.yml` to see rules for each label/title pattern.
name: "🏷️ PR Title Labeler"
on:
# Safe since we're not checking out or running the PR's code
# Never check out the PR's head in a pull_request_target job
pull_request_target:
types: [opened, synchronize, reopened, edited]
jobs:
pr-title-labeler:
name: 'label'
permissions:
contents: read
pull-requests: write
issues: write
runs-on: ubuntu-latest
steps:
- name: Label PR based on title
# Archived repo; latest commit (v0.1.0)
uses: grafana/pr-labeler-action@f19222d3ef883d2ca5f04420fdfe8148003763f0
with:
token: ${{ secrets.GITHUB_TOKEN }}
configuration-path: .github/pr-title-labeler.yml

View File

@@ -1,51 +1,43 @@
# -----------------------------------------------------------------------------
# PR Title Lint Workflow
# PR title linting.
#
# Purpose:
# Enforces Conventional Commits format for pull request titles to maintain a
# clear, consistent, and machine-readable change history across our repository.
# This helps with automated changelog generation and semantic versioning.
# FORMAT (Conventional Commits 1.0.0):
#
# Enforced Commit Message Format (Conventional Commits 1.0.0):
# <type>[optional scope]: <description>
# [optional body]
# [optional footer(s)]
#
# Examples:
# feat(core): add multitenant support
# fix(cli): resolve flag parsing error
# docs: update API usage examples
# docs(openai): update API usage examples
#
# Allowed Types:
# feat — a new feature (MINOR bump)
# fix — a bug fix (PATCH bump)
# docs — documentation only changes
# style — formatting, missing semi-colons, etc.; no code change
# refactor — code change that neither fixes a bug nor adds a feature
# perf — code change that improves performance
# test — adding missing tests or correcting existing tests
# build — changes that affect the build system or external dependencies
# ci — continuous integration/configuration changes
# chore — other changes that don't modify src or test files
# revert — reverts a previous commit
# release — prepare a new release
# * feat — a new feature (MINOR)
# * fix — a bug fix (PATCH)
# * docs — documentation only changes (either in /docs or code comments)
# * style — formatting, linting, etc.; no code change or typing refactors
# * refactor — code change that neither fixes a bug nor adds a feature
# * perf — code change that improves performance
# * test — adding tests or correcting existing
# * build — changes that affect the build system/external dependencies
# * ci — continuous integration/configuration changes
# * chore — other changes that don't modify source or test files
# * revert — reverts a previous commit
# * release — prepare a new release
#
# Allowed Scopes (optional):
# core, cli, langchain, langchain_v1, langchain_legacy, standard-tests,
# core, cli, langchain, langchain_v1, langchain_legacy, standard-tests,
# text-splitters, docs, anthropic, chroma, deepseek, exa, fireworks, groq,
# huggingface, mistralai, nomic, ollama, openai, perplexity, prompty, qdrant,
# xai, infra
#
# Rules & Tips for New Committers:
# 1. Subject (type) must start with a lowercase letter and, if possible, be
# followed by a scope wrapped in parenthesis `(scope)`
# 2. Breaking changes:
# Append "!" after type/scope (e.g., feat!: drop Node 12 support)
# Or include a footer "BREAKING CHANGE: <details>"
# 3. Example PR titles:
# feat(core): add multitenant support
# fix(cli): resolve flag parsing error
# docs: update API usage examples
# docs(openai): update API usage examples
# Rules:
# 1. The 'Type' must start with a lowercase letter.
# 2. Breaking changes: append "!" after type/scope (e.g., feat!: drop x support)
#
# Resources:
# • Conventional Commits spec: https://www.conventionalcommits.org/en/v1.0.0/
# -----------------------------------------------------------------------------
# Enforces Conventional Commits format for pull request titles to maintain a clear and
# machine-readable change history.
name: '🏷️ PR Title Lint'
@@ -57,9 +49,9 @@ on:
types: [opened, edited, synchronize]
jobs:
# Validates that PR title follows Conventional Commits specification
# Validates that PR title follows Conventional Commits 1.0.0 specification
lint-pr-title:
name: 'Validate PR Title Format'
name: 'validate format'
runs-on: ubuntu-latest
steps:
- name: '✅ Validate Conventional Commits Format'

View File

@@ -1,3 +1,5 @@
# Integration tests for documentation notebooks.
name: '📓 Validate Documentation Notebooks'
run-name: 'Test notebooks in ${{ inputs.working-directory }}'
on:

View File

@@ -1,8 +1,14 @@
# Routine integration tests against partner libraries with live API credentials.
#
# Uses `make integration_tests` for each library in the matrix.
#
# Runs daily. Can also be triggered manually for immediate updates.
name: '⏰ Scheduled Integration Tests'
run-name: "Run Integration Tests - ${{ inputs.working-directory-force || 'all libs' }} (Python ${{ inputs.python-version-force || '3.9, 3.11' }})"
on:
workflow_dispatch: # Allows maintainers to trigger the workflow manually in GitHub UI
workflow_dispatch:
inputs:
working-directory-force:
type: string
@@ -54,13 +60,13 @@ jobs:
echo $matrix
echo "matrix=$matrix" >> $GITHUB_OUTPUT
# Run integration tests against partner libraries with live API credentials
# Tests are run with both Poetry and UV depending on the library's setup
# Tests are run with Poetry or UV depending on the library's setup
build:
if: github.repository_owner == 'langchain-ai' || github.event_name != 'schedule'
name: '🐍 Python ${{ matrix.python-version }}: ${{ matrix.working-directory }}'
runs-on: ubuntu-latest
needs: [compute-matrix]
timeout-minutes: 20
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
@@ -161,7 +167,7 @@ jobs:
make integration_tests
- name: '🧹 Clean up External Libraries'
# Clean up external libraries to avoid affecting git status check
# Clean up external libraries to avoid affecting the following git status check
run: |
rm -rf \
langchain/libs/partners/google-genai \

9
.github/workflows/v1_changes.md vendored Normal file
View File

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

View File

@@ -2,110 +2,104 @@ repos:
- repo: local
hooks:
- id: core
name: format core
name: format and lint core
language: system
entry: make -C libs/core format
entry: make -C libs/core format lint
files: ^libs/core/
pass_filenames: false
- id: langchain
name: format langchain
name: format and lint langchain
language: system
entry: make -C libs/langchain format
entry: make -C libs/langchain format lint
files: ^libs/langchain/
pass_filenames: false
- id: standard-tests
name: format standard-tests
name: format and lint standard-tests
language: system
entry: make -C libs/standard-tests format
entry: make -C libs/standard-tests format lint
files: ^libs/standard-tests/
pass_filenames: false
- id: text-splitters
name: format text-splitters
name: format and lint text-splitters
language: system
entry: make -C libs/text-splitters format
entry: make -C libs/text-splitters format lint
files: ^libs/text-splitters/
pass_filenames: false
- id: anthropic
name: format partners/anthropic
name: format and lint partners/anthropic
language: system
entry: make -C libs/partners/anthropic format
entry: make -C libs/partners/anthropic format lint
files: ^libs/partners/anthropic/
pass_filenames: false
- id: chroma
name: format partners/chroma
name: format and lint partners/chroma
language: system
entry: make -C libs/partners/chroma format
entry: make -C libs/partners/chroma format lint
files: ^libs/partners/chroma/
pass_filenames: false
- id: couchbase
name: format partners/couchbase
language: system
entry: make -C libs/partners/couchbase format
files: ^libs/partners/couchbase/
pass_filenames: false
- id: exa
name: format partners/exa
name: format and lint partners/exa
language: system
entry: make -C libs/partners/exa format
entry: make -C libs/partners/exa format lint
files: ^libs/partners/exa/
pass_filenames: false
- id: fireworks
name: format partners/fireworks
name: format and lint partners/fireworks
language: system
entry: make -C libs/partners/fireworks format
entry: make -C libs/partners/fireworks format lint
files: ^libs/partners/fireworks/
pass_filenames: false
- id: groq
name: format partners/groq
name: format and lint partners/groq
language: system
entry: make -C libs/partners/groq format
entry: make -C libs/partners/groq format lint
files: ^libs/partners/groq/
pass_filenames: false
- id: huggingface
name: format partners/huggingface
name: format and lint partners/huggingface
language: system
entry: make -C libs/partners/huggingface format
entry: make -C libs/partners/huggingface format lint
files: ^libs/partners/huggingface/
pass_filenames: false
- id: mistralai
name: format partners/mistralai
name: format and lint partners/mistralai
language: system
entry: make -C libs/partners/mistralai format
entry: make -C libs/partners/mistralai format lint
files: ^libs/partners/mistralai/
pass_filenames: false
- id: nomic
name: format partners/nomic
name: format and lint partners/nomic
language: system
entry: make -C libs/partners/nomic format
entry: make -C libs/partners/nomic format lint
files: ^libs/partners/nomic/
pass_filenames: false
- id: ollama
name: format partners/ollama
name: format and lint partners/ollama
language: system
entry: make -C libs/partners/ollama format
entry: make -C libs/partners/ollama format lint
files: ^libs/partners/ollama/
pass_filenames: false
- id: openai
name: format partners/openai
name: format and lint partners/openai
language: system
entry: make -C libs/partners/openai format
entry: make -C libs/partners/openai format lint
files: ^libs/partners/openai/
pass_filenames: false
- id: prompty
name: format partners/prompty
name: format and lint partners/prompty
language: system
entry: make -C libs/partners/prompty format
entry: make -C libs/partners/prompty format lint
files: ^libs/partners/prompty/
pass_filenames: false
- id: qdrant
name: format partners/qdrant
name: format and lint partners/qdrant
language: system
entry: make -C libs/partners/qdrant format
entry: make -C libs/partners/qdrant format lint
files: ^libs/partners/qdrant/
pass_filenames: false
- id: root
name: format docs, cookbook
name: format and lint docs, cookbook
language: system
entry: make format
entry: make format lint
files: ^(docs|cookbook)/
pass_filenames: false

View File

@@ -1,25 +0,0 @@
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.11"
commands:
- mkdir -p $READTHEDOCS_OUTPUT
- cp -r api_reference_build/* $READTHEDOCS_OUTPUT
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/api_reference/conf.py
# If using Sphinx, optionally build your docs in additional formats such as PDF
formats:
- pdf
# Optionally declare the Python requirements required to build your docs
python:
install:
- requirements: docs/api_reference/requirements.txt

View File

@@ -78,5 +78,10 @@
"editor.insertSpaces": true
},
"python.terminal.activateEnvironment": false,
"python.defaultInterpreterPath": "./.venv/bin/python"
"python.defaultInterpreterPath": "./.venv/bin/python",
"github.copilot.chat.commitMessageGeneration.instructions": [
{
"file": ".github/workflows/pr_lint.yml"
}
]
}

325
AGENTS.md Normal file
View File

@@ -0,0 +1,325 @@
# Global Development Guidelines for LangChain Projects
## Core Development Principles
### 1. Maintain Stable Public Interfaces ⚠️ CRITICAL
**Always attempt to preserve function signatures, argument positions, and names for exported/public methods.**
**Bad - Breaking Change:**
```python
def get_user(id, verbose=False): # Changed from `user_id`
pass
```
**Good - Stable Interface:**
```python
def get_user(user_id: str, verbose: bool = False) -> User:
"""Retrieve user by ID with optional verbose output."""
pass
```
**Before making ANY changes to public APIs:**
- Check if the function/class is exported in `__init__.py`
- Look for existing usage patterns in tests and examples
- Use keyword-only arguments for new parameters: `*, new_param: str = "default"`
- Mark experimental features clearly with docstring warnings (using reStructuredText, like `.. warning::`)
🧠 *Ask yourself:* "Would this change break someone's code if they used it last week?"
### 2. Code Quality Standards
**All Python code MUST include type hints and return types.**
**Bad:**
```python
def p(u, d):
return [x for x in u if x not in d]
```
**Good:**
```python
def filter_unknown_users(users: list[str], known_users: set[str]) -> list[str]:
"""Filter out users that are not in the known users set.
Args:
users: List of user identifiers to filter.
known_users: Set of known/valid user identifiers.
Returns:
List of users that are not in the known_users set.
"""
return [user for user in users if user not in known_users]
```
**Style Requirements:**
- Use descriptive, **self-explanatory variable names**. Avoid overly short or cryptic identifiers.
- Attempt to break up complex functions (>20 lines) into smaller, focused functions where it makes sense
- Avoid unnecessary abstraction or premature optimization
- Follow existing patterns in the codebase you're modifying
### 3. Testing Requirements
**Every new feature or bugfix MUST be covered by unit tests.**
**Test Organization:**
- Unit tests: `tests/unit_tests/` (no network calls allowed)
- Integration tests: `tests/integration_tests/` (network calls permitted)
- Use `pytest` as the testing framework
**Test Quality Checklist:**
- [ ] Tests fail when your new logic is broken
- [ ] Happy path is covered
- [ ] Edge cases and error conditions are tested
- [ ] Use fixtures/mocks for external dependencies
- [ ] Tests are deterministic (no flaky tests)
Checklist questions:
- [ ] Does the test suite fail if your new logic is broken?
- [ ] Are all expected behaviors exercised (happy path, invalid input, etc)?
- [ ] Do tests use fixtures or mocks where needed?
```python
def test_filter_unknown_users():
"""Test filtering unknown users from a list."""
users = ["alice", "bob", "charlie"]
known_users = {"alice", "bob"}
result = filter_unknown_users(users, known_users)
assert result == ["charlie"]
assert len(result) == 1
```
### 4. Security and Risk Assessment
**Security Checklist:**
- No `eval()`, `exec()`, or `pickle` on user-controlled input
- Proper exception handling (no bare `except:`) and use a `msg` variable for error messages
- Remove unreachable/commented code before committing
- Race conditions or resource leaks (file handles, sockets, threads).
- Ensure proper resource cleanup (file handles, connections)
**Bad:**
```python
def load_config(path):
with open(path) as f:
return eval(f.read()) # ⚠️ Never eval config
```
**Good:**
```python
import json
def load_config(path: str) -> dict:
with open(path) as f:
return json.load(f)
```
### 5. Documentation Standards
**Use Google-style docstrings with Args 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
- Focus on "why" rather than "what" in descriptions
- Document all parameters, return values, and exceptions
- Keep descriptions concise but clear
- Use reStructuredText for docstrings to enable rich formatting
📌 *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
```bash
# Run unit tests (no network)
make test
# Don't run integration tests, as API keys must be set
# Run specific test file
uv run --group test pytest tests/unit_tests/test_specific.py
```
### Code Quality
```bash
# Lint code
make lint
# Format code
make format
# Type checking
uv run --group lint mypy .
```
### Dependency Management Patterns
**Local Development Dependencies:**
```toml
[tool.uv.sources]
langchain-core = { path = "../core", editable = true }
langchain-tests = { path = "../standard-tests", editable = true }
```
**For tools, use the `@tool` decorator from `langchain_core.tools`:**
```python
from langchain_core.tools import tool
@tool
def search_database(query: str) -> str:
"""Search the database for relevant information.
Args:
query: The search query string.
"""
# Implementation here
return results
```
## Commit Standards
**Use Conventional Commits format for PR titles:**
- `feat(core): add multi-tenant support`
- `fix(cli): resolve flag parsing error`
- `docs: update API usage examples`
- `docs(openai): update API usage examples`
## Framework-Specific Guidelines
- 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`
### Partner Integrations
- Follow the established patterns in existing partner libraries
- Implement standard interfaces (`BaseChatModel`, `BaseEmbeddings`, etc.)
- Include comprehensive integration tests
- Document API key requirements and authentication
---
## Quick Reference Checklist
Before submitting code changes:
- [ ] **Breaking Changes**: Verified no public API changes
- [ ] **Type Hints**: All functions have complete type annotations
- [ ] **Tests**: New functionality is fully tested
- [ ] **Security**: No dangerous patterns (eval, silent failures, etc.)
- [ ] **Documentation**: Google-style docstrings for public functions
- [ ] **Code Quality**: `make lint` and `make format` pass
- [ ] **Architecture**: Suggested improvements where applicable
- [ ] **Commit Message**: Follows Conventional Commits format

View File

@@ -1,4 +1,4 @@
.PHONY: all clean help docs_build docs_clean docs_linkcheck api_docs_build api_docs_clean api_docs_linkcheck spell_check spell_fix lint lint_package lint_tests format format_diff
.PHONY: all clean help docs_build docs_clean docs_linkcheck api_docs_build api_docs_clean api_docs_linkcheck lint lint_package lint_tests format format_diff
.EXPORT_ALL_VARIABLES:
UV_FROZEN = true
@@ -78,18 +78,6 @@ api_docs_linkcheck:
fi
@echo "✅ API link check complete"
## spell_check: Run codespell on the project.
spell_check:
@echo "✏️ Checking spelling across project..."
uv run --group codespell codespell --toml pyproject.toml
@echo "✅ Spell check complete"
## spell_fix: Run codespell on the project and fix the errors.
spell_fix:
@echo "✏️ Fixing spelling errors across project..."
uv run --group codespell codespell --toml pyproject.toml -w
@echo "✅ Spelling errors fixed"
######################
# LINTING AND FORMATTING
######################
@@ -100,7 +88,7 @@ lint lint_package lint_tests:
uv run --group lint ruff check docs cookbook
uv run --group lint ruff format docs cookbook cookbook --diff
git --no-pager grep 'from langchain import' docs cookbook | grep -vE 'from langchain import (hub)' && echo "Error: no importing langchain from root in docs, except for hub" && exit 1 || exit 0
git --no-pager grep 'api.python.langchain.com' -- docs/docs ':!docs/docs/additional_resources/arxiv_references.mdx' ':!docs/docs/integrations/document_loaders/sitemap.ipynb' || exit 0 && \
echo "Error: you should link python.langchain.com/api_reference, not api.python.langchain.com in the docs" && \
exit 1

View File

@@ -35,7 +35,7 @@ open source projects at [huntr](https://huntr.com/bounties/disclose/?target=http
Before reporting a vulnerability, please review:
1) In-Scope Targets and Out-of-Scope Targets below.
2) The [langchain-ai/langchain](https://python.langchain.com/docs/contributing/repo_structure) monorepo structure.
2) The [langchain-ai/langchain](https://docs.langchain.com/oss/python/contributing/code#supporting-packages) monorepo structure.
3) The [Best Practices](#best-practices) above to understand what we consider to be a security vulnerability vs. developer responsibility.
### In-Scope Targets

View File

@@ -1,3 +1,154 @@
# LangChain Documentation
For more information on contributing to our documentation, see the [Documentation Contributing Guide](https://python.langchain.com/docs/contributing/how_to/documentation)
For more information on contributing to our documentation, see the [Documentation Contributing Guide](https://python.langchain.com/docs/contributing/how_to/documentation).
## Structure
The primary documentation is located in the `docs/` directory. This directory contains
both the source files for the main documentation as well as the API reference doc
build process.
### API Reference
API reference documentation is located in `docs/api_reference/` and is generated from
the codebase using Sphinx.
The API reference have additional build steps that differ from the main documentation.
#### Deployment Process
Currently, the build process roughly follows these steps:
1. Using the `api_doc_build.yml` GitHub workflow, the API reference docs are
[built](#build-technical-details) and copied to the `langchain-api-docs-html`
repository. This workflow is triggered either (1) on a cron routine interval or (2)
triggered manually.
In short, the workflow extracts all `langchain-ai`-org-owned repos defined in
`langchain/libs/packages.yml`, clones them locally (in the workflow runner's file
system), and then builds the API reference RST files (using `create_api_rst.py`).
Following post-processing, the HTML files are pushed to the
`langchain-api-docs-html` repository.
2. After the HTML files are in the `langchain-api-docs-html` repository, they are **not**
automatically published to the [live docs site](https://python.langchain.com/api_reference/).
The docs site is served by Vercel. The Vercel deployment process copies the HTML
files from the `langchain-api-docs-html` repository and deploys them to the live
site. Deployments are triggered on each new commit pushed to `master`.
#### Build Technical Details
The build process creates a virtual monorepo by syncing multiple repositories, then generates comprehensive API documentation:
1. **Repository Sync Phase:**
- `.github/scripts/prep_api_docs_build.py` - Clones external partner repos and organizes them into the `libs/partners/` structure to create a virtual monorepo for documentation building
2. **RST Generation Phase:**
- `docs/api_reference/create_api_rst.py` - Main script that **generates RST files** from Python source code
- Scans `libs/` directories and extracts classes/functions from each module (using `inspect`)
- Creates `.rst` files using specialized templates for different object types
- Templates in `docs/api_reference/templates/` (`pydantic.rst`, `runnable_pydantic.rst`, etc.)
3. **HTML Build Phase:**
- Sphinx-based, uses `sphinx.ext.autodoc` (auto-extracts docstrings from the codebase)
- `docs/api_reference/conf.py` (sphinx config) configures `autodoc` and other extensions
- `sphinx-build` processes the generated `.rst` files into HTML using autodoc
- `docs/api_reference/scripts/custom_formatter.py` - Post-processes the generated HTML
- Copies `reference.html` to `index.html` to create the default landing page (artifact? might not need to do this - just put everyhing in index directly?)
4. **Deployment:**
- `.github/workflows/api_doc_build.yml` - Workflow responsible for orchestrating the entire build and deployment process
- Built HTML files are committed and pushed to the `langchain-api-docs-html` repository
#### Local Build
For local development and testing of API documentation, use the Makefile targets in the repository root:
```bash
# Full build
make api_docs_build
```
Like the CI process, this target:
- Installs the CLI package in editable mode
- Generates RST files for all packages using `create_api_rst.py`
- Builds HTML documentation with Sphinx
- Post-processes the HTML with `custom_formatter.py`
- Opens the built documentation (`reference.html`) in your browser
**Quick Preview:**
```bash
make api_docs_quick_preview API_PKG=openai
```
- Generates RST files for only the specified package (default: `text-splitters`)
- Builds and post-processes HTML documentation
- Opens the preview in your browser
Both targets automatically clean previous builds and handle the complete build pipeline locally, mirroring the CI process but for faster iteration during development.
#### Documentation Standards
**Docstring Format:**
The API reference uses **Google-style docstrings** with reStructuredText markup. Sphinx processes these through the `sphinx.ext.napoleon` extension to generate documentation.
**Required format:**
```python
def example_function(param1: str, param2: int = 5) -> bool:
"""Brief description of the function.
Longer description can go here. Use reStructuredText syntax for
rich formatting like **bold** and *italic*.
TODO: code: figure out what works?
Args:
param1: Description of the first parameter.
param2: Description of the second parameter with default value.
Returns:
Description of the return value.
Raises:
ValueError: When param1 is empty.
TypeError: When param2 is not an integer.
.. warning::
This function is experimental and may change.
"""
```
**Special Markers:**
- `:private:` in docstrings excludes members from documentation
- `.. warning::` adds warning admonitions
#### Site Styling and Assets
**Theme and Styling:**
- Uses [**PyData Sphinx Theme**](https://pydata-sphinx-theme.readthedocs.io/en/stable/index.html) (`pydata_sphinx_theme`)
- Custom CSS in `docs/api_reference/_static/css/custom.css` with LangChain-specific:
- Color palette
- Inter font family
- Custom navbar height and sidebar formatting
- Deprecated/beta feature styling
**Static Assets:**
- Logos: `_static/wordmark-api.svg` (light) and `_static/wordmark-api-dark.svg` (dark mode)
- Favicon: `_static/img/brand/favicon.png`
- Custom CSS: `_static/css/custom.css`
**Post-Processing:**
- `scripts/custom_formatter.py` cleans up generated HTML:
- Shortens TOC entries from `ClassName.method()` to `method()`
**Analytics and Integration:**
- GitHub integration (source links, edit buttons)
- Example backlinking through custom `ExampleLinksDirective`

View File

@@ -50,7 +50,7 @@ class GalleryGridDirective(SphinxDirective):
individual cards + ["image", "header", "content", "title"].
Danger:
This directive can only be used in the context of a Myst documentation page as
This directive can only be used in the context of a MyST documentation page as
the templates use Markdown flavored formatting.
"""

View File

@@ -1,7 +1,5 @@
"""Configuration file for the Sphinx documentation builder."""
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
@@ -20,16 +18,18 @@ from docutils.parsers.rst.directives.admonitions import BaseAdmonition
from docutils.statemachine import StringList
from sphinx.util.docutils import SphinxDirective
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
# Add paths to Python import system so Sphinx can import LangChain modules
# This allows autodoc to introspect and document the actual code
_DIR = Path(__file__).parent.absolute()
sys.path.insert(0, os.path.abspath("."))
sys.path.insert(0, os.path.abspath("../../libs/langchain"))
sys.path.insert(0, os.path.abspath(".")) # Current directory
sys.path.insert(0, os.path.abspath("../../libs/langchain")) # LangChain main package
# Load package metadata from pyproject.toml (for version info, etc.)
with (_DIR.parents[1] / "libs" / "langchain" / "pyproject.toml").open("r") as f:
data = toml.load(f)
# Load mapping of classes to example notebooks for backlinking
# This file is generated by scripts that scan our tutorial/example notebooks
with (_DIR / "guide_imports.json").open("r") as f:
imported_classes = json.load(f)
@@ -86,6 +86,7 @@ class Beta(BaseAdmonition):
def setup(app):
"""Register custom directives and hooks with Sphinx."""
app.add_directive("example_links", ExampleLinksDirective)
app.add_directive("beta", Beta)
app.connect("autodoc-skip-member", skip_private_members)
@@ -125,7 +126,7 @@ extensions = [
"sphinx.ext.viewcode",
"sphinxcontrib.autodoc_pydantic",
"IPython.sphinxext.ipython_console_highlighting",
"myst_parser",
"myst_parser", # For generated index.md and reference.md
"_extensions.gallery_directive",
"sphinx_design",
"sphinx_copybutton",
@@ -258,6 +259,7 @@ html_static_path = ["_static"]
html_css_files = ["css/custom.css"]
html_use_index = False
# Only used on the generated index.md and reference.md files
myst_enable_extensions = ["colon_fence"]
# generate autosummary even if no references
@@ -268,11 +270,11 @@ autosummary_ignore_module_all = False
html_copy_source = False
html_show_sourcelink = False
googleanalytics_id = "G-9B66JQQH2F"
# Set canonical URL from the Read the Docs Domain
html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "")
googleanalytics_id = "G-9B66JQQH2F"
# Tell Jinja2 templates the build is running on Read the Docs
if os.environ.get("READTHEDOCS", "") == "True":
html_context["READTHEDOCS"] = True

View File

@@ -1,4 +1,41 @@
"""Script for auto-generating api_reference.rst."""
"""Auto-generate API reference documentation (RST files) for LangChain packages.
* Automatically discovers all packages in `libs/` and `libs/partners/`
* For each package, recursively walks the filesystem to:
* Load Python modules using importlib
* Extract classes and functions using Python's inspect module
* Classify objects by type (Pydantic models, Runnables, TypedDicts, etc.)
* Filter out private members (names starting with '_') and deprecated items
* Creates structured RST files with:
* Module-level documentation pages with autosummary tables
* Different Sphinx templates based on object type (see templates/ directory)
* Proper cross-references and navigation structure
* Separation of current vs deprecated APIs
* Generates a directory tree like:
```
docs/api_reference/
├── index.md # Main landing page with package gallery
├── reference.md # Package overview and navigation
├── core/ # langchain-core documentation
│ ├── index.rst
│ ├── callbacks.rst
│ └── ...
├── langchain/ # langchain documentation
│ ├── index.rst
│ └── ...
└── partners/ # Integration packages
├── openai/
├── anthropic/
└── ...
```
## Key Features
* Respects privacy markers:
* Modules with `:private:` in docstring are excluded entirely
* Objects with `:private:` in docstring are filtered out
* Names starting with '_' are treated as private
"""
import importlib
import inspect
@@ -177,12 +214,13 @@ def _load_package_modules(
Traversal based on the file system makes it easy to determine which
of the modules/packages are part of the package vs. 3rd party or built-in.
Parameters:
package_directory (Union[str, Path]): Path to the package directory.
submodule (Optional[str]): Optional name of submodule to load.
Args:
package_directory: Path to the package directory.
submodule: Optional name of submodule to load.
Returns:
Dict[str, ModuleMembers]: A dictionary where keys are module names and values are ModuleMembers objects.
A dictionary where keys are module names and values are `ModuleMembers`
objects.
"""
package_path = (
Path(package_directory)
@@ -199,12 +237,13 @@ def _load_package_modules(
package_path = package_path / submodule
for file_path in package_path.rglob("*.py"):
# Skip private modules
if file_path.name.startswith("_"):
continue
# Skip integration_template and project_template directories (for libs/cli)
if "integration_template" in file_path.parts:
continue
if "project_template" in file_path.parts:
continue
@@ -215,8 +254,13 @@ def _load_package_modules(
continue
# Get the full namespace of the module
# Example: langchain_core/schema/output_parsers.py ->
# langchain_core.schema.output_parsers
namespace = str(relative_module_name).replace(".py", "").replace("/", ".")
# Keep only the top level namespace
# Example: langchain_core.schema.output_parsers ->
# langchain_core
top_namespace = namespace.split(".")[0]
try:
@@ -253,16 +297,16 @@ def _construct_doc(
members_by_namespace: Dict[str, ModuleMembers],
package_version: str,
) -> List[typing.Tuple[str, str]]:
"""Construct the contents of the reference.rst file for the given package.
"""Construct the contents of the `reference.rst` for the given package.
Args:
package_namespace: The package top level namespace
members_by_namespace: The members of the package, dict organized by top level
module contains a list of classes and functions
inside of the top level namespace.
members_by_namespace: The members of the package dict organized by top level.
Module contains a list of classes and functions inside of the top level
namespace.
Returns:
The contents of the reference.rst file.
The string contents of the reference.rst file.
"""
docs = []
index_doc = f"""\
@@ -465,10 +509,13 @@ def _construct_doc(
def _build_rst_file(package_name: str = "langchain") -> None:
"""Create a rst file for building of documentation.
"""Create a rst file for a given package.
Args:
package_name: Can be either "langchain" or "core"
package_name: Name of the package to create the rst file for.
Returns:
The rst file is created in the same directory as this script.
"""
package_dir = _package_dir(package_name)
package_members = _load_package_modules(package_dir)
@@ -500,7 +547,10 @@ def _package_namespace(package_name: str) -> str:
def _package_dir(package_name: str = "langchain") -> Path:
"""Return the path to the directory containing the documentation."""
"""Return the path to the directory containing the documentation.
Attempts to find the package in `libs/` first, then `libs/partners/`.
"""
if (ROOT_DIR / "libs" / package_name).exists():
return ROOT_DIR / "libs" / package_name / _package_namespace(package_name)
else:
@@ -514,7 +564,7 @@ def _package_dir(package_name: str = "langchain") -> Path:
def _get_package_version(package_dir: Path) -> str:
"""Return the version of the package."""
"""Return the version of the package by reading the `pyproject.toml`."""
try:
with open(package_dir.parent / "pyproject.toml", "r") as f:
pyproject = toml.load(f)
@@ -540,6 +590,15 @@ def _out_file_path(package_name: str) -> Path:
def _build_index(dirs: List[str]) -> None:
"""Build the index.md file for the API reference.
Args:
dirs: List of package directories to include in the index.
Returns:
The index.md file is created in the same directory as this script.
"""
custom_names = {
"aws": "AWS",
"ai21": "AI21",
@@ -647,9 +706,14 @@ See the full list of integrations in the Section Navigation.
{integration_tree}
```
"""
# Write the reference.md file
with open(HERE / "reference.md", "w") as f:
f.write(doc)
# Write a dummy index.md file that points to reference.md
# Sphinx requires an index file to exist in each doc directory
# TODO: investigate why we don't just put everything in index.md directly?
# if it works it works I guess
dummy_index = """\
# API reference
@@ -665,8 +729,11 @@ Reference<reference>
def main(dirs: Optional[list] = None) -> None:
"""Generate the api_reference.rst file for each package."""
print("Starting to build API reference files.")
"""Generate the `api_reference.rst` file for each package.
If dirs is None, generate for all packages in `libs/` and `libs/partners/`.
Otherwise generate only for the specified package(s).
"""
if not dirs:
dirs = [
p.parent.name
@@ -675,18 +742,17 @@ def main(dirs: Optional[list] = None) -> None:
if p.parent.parent.name in ("libs", "partners")
]
for dir_ in sorted(dirs):
# Skip any hidden directories
# Skip any hidden directories prefixed with a dot
# Some of these could be present by mistake in the code base
# e.g., .pytest_cache from running tests from the wrong location.
# (e.g., .pytest_cache from running tests from the wrong location)
if dir_.startswith("."):
print("Skipping dir:", dir_)
continue
else:
print("Building package:", dir_)
print("Building:", dir_)
_build_rst_file(package_name=dir_)
_build_index(sorted(dirs))
print("API reference files built.")
if __name__ == "__main__":

View File

@@ -1,12 +1,12 @@
autodoc_pydantic>=2,<3
sphinx>=8,<9
myst-parser>=3
sphinx-autobuild>=2024
pydata-sphinx-theme>=0.15
toml>=0.10.2
myst-nb>=1.1.1
pyyaml
sphinx-design
sphinx-copybutton
beautifulsoup4
sphinxcontrib-googleanalytics
pydata-sphinx-theme>=0.15
myst-parser>=3
myst-nb>=1.1.1
toml>=0.10.2
pyyaml
beautifulsoup4

View File

@@ -1,3 +1,10 @@
"""Post-process generated HTML files to clean up table-of-contents headers.
Runs after Sphinx generates the API reference HTML. It finds TOC entries like
"ClassName.method_name()" and shortens them to just "method_name()" for better
readability in the sidebar navigation.
"""
import sys
from glob import glob
from pathlib import Path

View File

@@ -189,40 +189,6 @@ This can be very helpful when you've made changes to only certain parts of the p
We recognize linting can be annoying - if you do not want to do it, please contact a project maintainer, and they can help you with it. We do not want this to be a blocker for good code getting contributed.
### Spellcheck
Spellchecking for this project is done via [codespell](https://github.com/codespell-project/codespell).
Note that `codespell` finds common typos, so it could have false-positive (correctly spelled but rarely used) and false-negatives (not finding misspelled) words.
To check spelling for this project:
```bash
# If you have `make` installed:
make spell_check
# If you don't have `make` (Windows alternative):
uv run --all-groups codespell --toml pyproject.toml
```
To fix spelling in place:
```bash
# If you have `make` installed:
make spell_fix
# If you don't have `make` (Windows alternative):
uv run --all-groups codespell --toml pyproject.toml -w
```
If codespell is incorrectly flagging a word, you can skip spellcheck for that word by adding it to the codespell config in the `pyproject.toml` file.
```python
[tool.codespell]
...
# Add here:
ignore-words-list = 'momento,collison,ned,foor,reworkd,parth,whats,aapply,mysogyny,unsecure'
```
### Pre-commit
We use [pre-commit](https://pre-commit.com/) to ensure commits are formatted/linted.

View File

@@ -72,7 +72,7 @@ See [supported integrations](/docs/integrations/chat/) for details on getting st
### Example selectors
[Example Selectors](/docs/concepts/example_selectors) are responsible for selecting the correct few shot examples to pass to the prompt.
[Example Selectors](/docs/concepts/example_selectors) are responsible for selecting the correct few-shot examples to pass to the prompt.
- [How to: use example selectors](/docs/how_to/example_selectors)
- [How to: select examples by length](/docs/how_to/example_selectors_length_based)
@@ -168,7 +168,7 @@ See [supported integrations](/docs/integrations/vectorstores/) for details on ge
Indexing is the process of keeping your vectorstore in-sync with the underlying data source.
- [How to: reindex data to keep your vectorstore in sync with the underlying data source](/docs/how_to/indexing)
- [How to: reindex data to keep your vectorstore in-sync with the underlying data source](/docs/how_to/indexing)
### Tools

View File

@@ -118,7 +118,7 @@
"metadata": {},
"outputs": [],
"source": [
"from stripe_agent_toolkit.crewai.toolkit import StripeAgentToolkit\n",
"from stripe_agent_toolkit.langchain.toolkit import StripeAgentToolkit\n",
"\n",
"stripe_agent_toolkit = StripeAgentToolkit(\n",
" secret_key=os.getenv(\"STRIPE_SECRET_KEY\"),\n",

View File

@@ -1,4 +1,20 @@
"""This script checks documentation for broken import statements."""
"""Check documentation for broken import statements.
Validates that all import statements in Jupyter notebooks within the documentation
directory are functional and can be successfully imported.
- Scans all `.ipynb` files in `docs/`
- Extracts import statements from code cells
- Tests each import to ensure it works
- Reports any broken imports that would fail for users
Usage:
python docs/scripts/check_imports.py
Exit codes:
0: All imports are valid
1: Found broken imports (ImportError raised)
"""
import importlib
import json

View File

@@ -1,3 +0,0 @@
# Contributing to langchain-cli
Update CLI versions with `poe bump` to ensure that version commands display correctly.

View File

@@ -18,7 +18,7 @@ def create_demo_server(
Args:
config_keys: Optional sequence of config keys to expose in the playground.
playground_type: The type of playground to use. Can be `'default'` or `'chat'`.
playground_type: The type of playground to use.
Returns:
The demo server.

View File

@@ -41,12 +41,6 @@ format format_diff:
[ "$(PYTHON_FILES)" = "" ] || uv run ruff format $(PYTHON_FILES)
[ "$(PYTHON_FILES)" = "" ] || uv run ruff check --fix $(PYTHON_FILES)
spell_check:
uv run codespell --toml pyproject.toml
spell_fix:
uv run codespell --toml pyproject.toml -w
check_imports: $(shell find __module_name__ -name '*.py')
uv run python ./scripts/check_imports.py $^

View File

@@ -46,7 +46,7 @@ class __ModuleName__Retriever(BaseRetriever):
retriever.invoke(query)
.. code-block:: none
.. code-block::
# TODO: Example output.
@@ -80,7 +80,7 @@ class __ModuleName__Retriever(BaseRetriever):
chain.invoke("...")
.. code-block:: none
.. code-block::
# TODO: Example output.

View File

@@ -42,7 +42,7 @@ class __ModuleName__Toolkit(BaseToolkit):
toolkit.get_tools()
.. code-block:: none
.. code-block::
# TODO: Example output.
@@ -62,7 +62,7 @@ class __ModuleName__Toolkit(BaseToolkit):
for event in events:
event["messages"][-1].pretty_print()
.. code-block:: none
.. code-block::
# TODO: Example output.

View File

@@ -9,7 +9,7 @@ description = "An integration package connecting __ModuleName__ and LangChain"
authors = []
readme = "README.md"
license = "MIT"
requires-python = ">=3.10"
requires-python = ">=3.10.0,<4.0.0"
dependencies = [
"langchain-core>=0.3.15",
]
@@ -29,7 +29,6 @@ dev-dependencies = [
"pytest-socket>=0.7.0",
"pytest-watcher>=0.3.4",
"langchain-tests>=0.3.5",
"codespell>=2.2.6",
"ruff>=0.5",
"mypy>=1.10",
]

View File

@@ -18,7 +18,5 @@ class Test__ModuleName__Retriever(RetrieversIntegrationTests):
@property
def retriever_query_example(self) -> str:
"""
Returns a str representing the "query" of an example retriever call.
"""
"""Returns a str representing the "query" of an example retriever call."""
return "example query"

View File

@@ -21,7 +21,7 @@ class TestParrotMultiplyToolIntegration(ToolsIntegrationTests):
"""
Returns a dictionary representing the "args" of an example tool call.
This should NOT be a ToolCall dict - i.e. it should not
have {"name", "id", "args"} keys.
This should NOT be a ToolCall dict - i.e. it should not have
`{"name", "id", "args"}` keys.
"""
return {"a": 2, "b": 3}

View File

@@ -11,7 +11,7 @@ class TestParrotMultiplyToolUnit(ToolsUnitTests):
@property
def tool_constructor_params(self) -> dict:
# if your tool constructor instead required initialization arguments like
# If your tool constructor instead required initialization arguments like
# `def __init__(self, some_arg: int):`, you would return those here
# as a dictionary, e.g.: `return {'some_arg': 42}`
return {}
@@ -21,7 +21,7 @@ class TestParrotMultiplyToolUnit(ToolsUnitTests):
"""
Returns a dictionary representing the "args" of an example tool call.
This should NOT be a ToolCall dict - i.e. it should not
have {"name", "id", "args"} keys.
This should NOT be a ToolCall dict - i.e. it should not have
`{"name", "id", "args"}` keys.
"""
return {"a": 2, "b": 3}

View File

@@ -159,8 +159,8 @@ def add(
"""Add the specified template to the current LangServe app.
e.g.:
langchain app add extraction-openai-functions
langchain app add git+ssh://git@github.com/efriis/simple-pirate.git
`langchain app add extraction-openai-functions`
`langchain app add git+ssh://git@github.com/efriis/simple-pirate.git`
"""
if branch is None:
branch = []

View File

@@ -116,17 +116,17 @@ def new(
typer.echo(f"Folder {destination_dir} exists.")
raise typer.Exit(code=1)
# copy over template from ../integration_template
# Copy over template from ../integration_template
shutil.copytree(project_template_dir, destination_dir, dirs_exist_ok=False)
# folder movement
# Folder movement
package_dir = destination_dir / replacements["__module_name__"]
shutil.move(destination_dir / "integration_template", package_dir)
# replacements in files
# Replacements in files
replace_glob(destination_dir, "**/*", cast("dict[str, str]", replacements))
# dependency install
# Dependency install
try:
# Use --no-progress to avoid tty issues in CI/test environments
env = os.environ.copy()
@@ -149,7 +149,7 @@ def new(
"`uv sync --dev` manually in the package directory.",
)
else:
# confirm src and dst are the same length
# Confirm src and dst are the same length
if not src:
typer.echo("Cannot provide --dst without --src.")
raise typer.Exit(code=1)
@@ -158,7 +158,7 @@ def new(
typer.echo("Number of --src and --dst arguments must match.")
raise typer.Exit(code=1)
if not dst:
# assume we're in a package dir, copy to equivalent path
# Assume we're in a package dir, copy to equivalent path
dst_paths = [destination_dir / p for p in src]
else:
dst_paths = [Path.cwd() / p for p in dst]
@@ -169,7 +169,7 @@ def new(
for p in dst_paths
]
# confirm no duplicate dst_paths
# Confirm no duplicate dst_paths
if len(dst_paths) != len(set(dst_paths)):
typer.echo(
"Duplicate destination paths provided or computed - please "
@@ -177,7 +177,7 @@ def new(
)
raise typer.Exit(code=1)
# confirm no files exist at dst_paths
# Confirm no files exist at dst_paths
for dst_path in dst_paths:
if dst_path.exists():
typer.echo(f"File {dst_path} exists.")

View File

@@ -75,7 +75,7 @@ def generate_raw_migrations(
def generate_top_level_imports(pkg: str) -> list[tuple[str, str]]:
"""Look at all the top level modules in langchain_community.
Attempt to import everything from each ``__init__`` file. For example,
Attempt to import everything from each `__init__` file. For example,
langchain_community/
chat_models/
@@ -83,16 +83,15 @@ def generate_top_level_imports(pkg: str) -> list[tuple[str, str]]:
llm/
__init__.py # <-- import everything from here
It'll collect all the imports, import the classes / functions it can find
there. It'll return a list of 2-tuples
Each tuple will contain the fully qualified path of the class / function to where
its logic is defined
(e.g., ``langchain_community.chat_models.xyz_implementation.ver2.XYZ``)
its logic is defined.
(e.g., `langchain_community.chat_models.xyz_implementation.ver2.XYZ`)
and the second tuple will contain the path
to importing it from the top level namespaces
(e.g., ``langchain_community.chat_models.XYZ``)
(e.g., `langchain_community.chat_models.XYZ`)
Args:
pkg: The package to scan.

View File

@@ -28,7 +28,6 @@ def get_migrations_for_partner_package(pkg_name: str) -> list[tuple[str, str]]:
Returns:
List of 2-tuples containing old and new import paths.
"""
package = importlib.import_module(pkg_name)
classes_ = find_subclasses_in_module(

View File

@@ -38,19 +38,19 @@ def parse_dependency_string(
branch: str | None,
api_path: str | None,
) -> DependencySource:
"""Parse a dependency string into a DependencySource.
"""Parse a dependency string into a `DependencySource`.
Args:
dep: the dependency string.
repo: optional repository.
branch: optional branch.
api_path: optional API path.
dep: The dependency string
repo: Optional repository
branch: Optional branch
api_path: Optional API path
Returns:
The parsed dependency source information.
The parsed dependency source information
Raises:
ValueError: if the dependency string is invalid.
ValueError: If the dependency string is invalid
"""
if dep is not None and dep.startswith("git+"):
if repo is not None or branch is not None:
@@ -147,8 +147,8 @@ def parse_dependencies(
"""Parse dependencies.
Args:
dependencies: the dependencies to parse
repo: the repositories to use
dependencies: The dependencies to parse
repo: The repositories to use
branch: the branches to use
api_path: the api paths to use
@@ -244,7 +244,7 @@ def copy_repo(
) -> None:
"""Copiy a repo, ignoring git folders.
Raises FileNotFound error if it can't find source
Raises `FileNotFound` if it can't find source
"""
def ignore_func(_: str, files: list[str]) -> list[str]:

View File

@@ -37,13 +37,12 @@ def get_package_root(cwd: Path | None = None) -> Path:
class LangServeExport(TypedDict):
"""Fields from pyproject.toml that are relevant to LangServe.
"""Fields from `pyproject.toml` that are relevant to LangServe.
Attributes:
module: The module to import from, tool.langserve.export_module
attr: The attribute to import from the module, tool.langserve.export_attr
package_name: The name of the package, tool.poetry.name
module: The module to import from, `tool.langserve.export_module`
attr: The attribute to import from the module, `tool.langserve.export_attr`
package_name: The name of the package, `tool.poetry.name`
"""
module: str

View File

@@ -19,7 +19,7 @@ def add_dependencies_to_pyproject_toml(
pyproject_toml: Path,
local_editable_dependencies: Iterable[tuple[str, Path]],
) -> None:
"""Add dependencies to pyproject.toml."""
"""Add dependencies to `pyproject.toml`."""
with pyproject_toml.open(encoding="utf-8") as f:
# tomlkit types aren't amazing - treat as Dict instead
pyproject: dict[str, Any] = load(f)
@@ -37,7 +37,7 @@ def remove_dependencies_from_pyproject_toml(
pyproject_toml: Path,
local_editable_dependencies: Iterable[str],
) -> None:
"""Remove dependencies from pyproject.toml."""
"""Remove dependencies from `pyproject.toml`."""
with pyproject_toml.open(encoding="utf-8") as f:
pyproject: dict[str, Any] = load(f)
# tomlkit types aren't amazing - treat as Dict instead

View File

@@ -5,14 +5,14 @@ build-backend = "pdm.backend"
[project]
authors = [{ name = "Erick Friis", email = "erick@langchain.dev" }]
license = { text = "MIT" }
requires-python = ">=3.10"
requires-python = ">=3.10.0,<4.0.0"
dependencies = [
"typer<1.0.0,>=0.17",
"gitpython<4,>=3",
"langserve[all]>=0.0.51",
"uvicorn<1.0,>=0.23",
"tomlkit>=0.12",
"gritql<1.0.0,>=0.2.0",
"typer>=0.17.0,<1.0.0",
"gitpython>=3.0.0,<4.0.0",
"langserve[all]>=0.0.51,<1.0.0",
"uvicorn>=0.23.0,<1.0.0",
"tomlkit>=0.12.0,<1.0.0",
"gritql>=0.2.0,<1.0.0",
]
name = "langchain-cli"
version = "0.0.37"
@@ -29,8 +29,8 @@ langchain = "langchain_cli.cli:app"
langchain-cli = "langchain_cli.cli:app"
[dependency-groups]
dev = ["pytest<9.0.0,>=7.4.2", "pytest-watcher<1.0.0,>=0.3.4"]
lint = ["ruff<0.13,>=0.12.2", "mypy<1.19,>=1.18.1"]
dev = ["pytest>=7.4.2,<9.0.0", "pytest-watcher>=0.3.4,<1.0.0"]
lint = ["ruff>=0.13.1,<0.14", "mypy>=1.18.1,<1.19"]
test = ["langchain-core", "langchain"]
typing = ["langchain"]
test_integration = []
@@ -70,11 +70,14 @@ flake8-annotations.allow-star-arg-any = true
flake8-annotations.mypy-init-return = true
flake8-type-checking.runtime-evaluated-base-classes = ["pydantic.BaseModel","langchain_core.load.serializable.Serializable","langchain_core.runnables.base.RunnableSerializable"]
pep8-naming.classmethod-decorators = [ "classmethod", "langchain_core.utils.pydantic.pre_init", "pydantic.field_validator", "pydantic.v1.root_validator",]
pydocstyle.convention = "google"
pyupgrade.keep-runtime-typing = true
[tool.ruff.lint.pydocstyle]
convention = "google"
ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters
[tool.ruff.lint.per-file-ignores]
"tests/**" = [ "D1", "DOC", "S", "SLF",]
"tests/**" = [ "D1", "S", "SLF",]
"scripts/**" = [ "INP", "S",]
[tool.pytest.ini_options]

View File

@@ -1 +0,0 @@
"""Scripts."""

View File

@@ -9,7 +9,7 @@ from langchain_cli.namespaces.migrate.generate.generic import (
@pytest.mark.xfail(reason="Unknown reason")
def test_create_json_agent_migration() -> None:
"""Test the migration of create_json_agent from langchain to langchain_community."""
"""Test migration of `create_json_agent` from langchain to `langchain_community`."""
with sup1(), sup2():
raw_migrations = generate_simplified_migrations(
from_package="langchain",
@@ -40,7 +40,7 @@ def test_create_json_agent_migration() -> None:
@pytest.mark.xfail(reason="Unknown reason")
def test_create_single_store_retriever_db() -> None:
"""Test migration from langchain to langchain_core."""
"""Test migration from `langchain` to `langchain_core`."""
with sup1(), sup2():
raw_migrations = generate_simplified_migrations(
from_package="langchain",

221
libs/cli/uv.lock generated
View File

@@ -1,6 +1,6 @@
version = 1
revision = 3
requires-python = ">=3.10"
requires-python = ">=3.10.0, <4.0.0"
[[package]]
name = "annotated-types"
@@ -13,7 +13,7 @@ wheels = [
[[package]]
name = "anyio"
version = "4.10.0"
version = "4.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
@@ -21,9 +21,9 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" },
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
]
[[package]]
@@ -110,14 +110,14 @@ wheels = [
[[package]]
name = "click"
version = "8.2.1"
version = "8.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
]
[[package]]
@@ -143,16 +143,16 @@ wheels = [
[[package]]
name = "fastapi"
version = "0.116.2"
version = "0.117.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/01/64/1296f46d6b9e3b23fb22e5d01af3f104ef411425531376212f1eefa2794d/fastapi-0.116.2.tar.gz", hash = "sha256:231a6af2fe21cfa2c32730170ad8514985fc250bec16c9b242d3b94c835ef529", size = 298595, upload-time = "2025-09-16T18:29:23.058Z" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/7e/d9788300deaf416178f61fb3c2ceb16b7d0dc9f82a08fdb87a5e64ee3cc7/fastapi-0.117.1.tar.gz", hash = "sha256:fb2d42082d22b185f904ca0ecad2e195b851030bd6c5e4c032d1c981240c631a", size = 307155, upload-time = "2025-09-20T20:16:56.663Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/e4/c543271a8018874b7f682bf6156863c416e1334b8ed3e51a69495c5d4360/fastapi-0.116.2-py3-none-any.whl", hash = "sha256:c3a7a8fb830b05f7e087d920e0d786ca1fc9892eb4e9a84b227be4c1bc7569db", size = 95670, upload-time = "2025-09-16T18:29:21.329Z" },
{ url = "https://files.pythonhosted.org/packages/6d/45/d9d3e8eeefbe93be1c50060a9d9a9f366dba66f288bb518a9566a23a8631/fastapi-0.117.1-py3-none-any.whl", hash = "sha256:33c51a0d21cab2b9722d4e56dbb9316f3687155be6b276191790d8da03507552", size = 95959, upload-time = "2025-09-20T20:16:53.661Z" },
]
[[package]]
@@ -353,16 +353,15 @@ requires-dist = [
{ name = "langchain-text-splitters", editable = "../text-splitters" },
{ name = "langchain-together", marker = "extra == 'together'" },
{ name = "langchain-xai", marker = "extra == 'xai'" },
{ name = "langsmith", specifier = ">=0.1.17" },
{ name = "langsmith", specifier = ">=0.1.17,<1.0.0" },
{ name = "pydantic", specifier = ">=2.7.4,<3.0.0" },
{ name = "pyyaml", specifier = ">=5.3" },
{ name = "requests", specifier = ">=2,<3" },
{ name = "sqlalchemy", specifier = ">=1.4,<3" },
{ name = "pyyaml", specifier = ">=5.3.0,<7.0.0" },
{ name = "requests", specifier = ">=2.0.0,<3.0.0" },
{ name = "sqlalchemy", specifier = ">=1.4.0,<3.0.0" },
]
provides-extras = ["community", "anthropic", "openai", "azure-ai", "cohere", "google-vertexai", "google-genai", "fireworks", "ollama", "together", "mistralai", "huggingface", "groq", "aws", "deepseek", "xai", "perplexity"]
[package.metadata.requires-dev]
codespell = [{ name = "codespell", specifier = ">=2.2.0,<3.0.0" }]
dev = [
{ name = "jupyter", specifier = ">=1.0.0,<2.0.0" },
{ name = "langchain-core", editable = "../core" },
@@ -373,10 +372,10 @@ dev = [
lint = [
{ name = "cffi", marker = "python_full_version < '3.10'", specifier = "<1.17.1" },
{ name = "cffi", marker = "python_full_version >= '3.10'" },
{ name = "ruff", specifier = ">=0.12.2,<0.13" },
{ name = "ruff", specifier = ">=0.13.1,<0.14.0" },
]
test = [
{ name = "blockbuster", specifier = ">=1.5.18,<1.6" },
{ name = "blockbuster", specifier = ">=1.5.18,<1.6.0" },
{ name = "cffi", marker = "python_full_version < '3.10'", specifier = "<1.17.1" },
{ name = "cffi", marker = "python_full_version >= '3.10'" },
{ name = "duckdb-engine", specifier = ">=0.9.2,<1.0.0" },
@@ -388,9 +387,9 @@ test = [
{ name = "lark", specifier = ">=1.1.5,<2.0.0" },
{ name = "numpy", marker = "python_full_version < '3.13'", specifier = ">=1.26.4" },
{ name = "numpy", marker = "python_full_version >= '3.13'", specifier = ">=2.1.0" },
{ name = "packaging", specifier = ">=24.2" },
{ name = "packaging", specifier = ">=24.2.0,<26.0.0" },
{ name = "pandas", specifier = ">=2.0.0,<3.0.0" },
{ name = "pytest", specifier = ">=8,<9" },
{ name = "pytest", specifier = ">=8.0.0,<9.0.0" },
{ name = "pytest-asyncio", specifier = ">=0.23.2,<1.0.0" },
{ name = "pytest-cov", specifier = ">=4.0.0,<5.0.0" },
{ name = "pytest-dotenv", specifier = ">=0.5.2,<1.0.0" },
@@ -401,7 +400,7 @@ test = [
{ name = "requests-mock", specifier = ">=1.11.0,<2.0.0" },
{ name = "responses", specifier = ">=0.22.0,<1.0.0" },
{ name = "syrupy", specifier = ">=4.0.2,<5.0.0" },
{ name = "toml", specifier = ">=0.10.2" },
{ name = "toml", specifier = ">=0.10.2,<1.0.0" },
]
test-integration = [
{ name = "cassio", specifier = ">=0.1.0,<1.0.0" },
@@ -409,14 +408,14 @@ test-integration = [
{ name = "langchain-text-splitters", editable = "../text-splitters" },
{ name = "langchainhub", specifier = ">=0.1.16,<1.0.0" },
{ name = "python-dotenv", specifier = ">=1.0.0,<2.0.0" },
{ name = "urllib3", marker = "python_full_version < '3.10'", specifier = "<2" },
{ name = "vcrpy", specifier = ">=7.0" },
{ name = "urllib3", marker = "python_full_version < '3.10'", specifier = "<2.0.0" },
{ name = "vcrpy", specifier = ">=7.0.0,<8.0.0" },
{ name = "wrapt", specifier = ">=1.15.0,<2.0.0" },
]
typing = [
{ name = "langchain-core", editable = "../core" },
{ name = "langchain-text-splitters", editable = "../text-splitters" },
{ name = "mypy", specifier = ">=1.15,<1.16" },
{ name = "mypy", specifier = ">=1.15.0,<1.16.0" },
{ name = "mypy-protobuf", specifier = ">=3.0.0,<4.0.0" },
{ name = "numpy", marker = "python_full_version < '3.13'", specifier = ">=1.26.4" },
{ name = "numpy", marker = "python_full_version >= '3.13'", specifier = ">=2.1.0" },
@@ -460,12 +459,12 @@ typing = [
[package.metadata]
requires-dist = [
{ name = "gitpython", specifier = ">=3,<4" },
{ name = "gitpython", specifier = ">=3.0.0,<4.0.0" },
{ name = "gritql", specifier = ">=0.2.0,<1.0.0" },
{ name = "langserve", extras = ["all"], specifier = ">=0.0.51" },
{ name = "tomlkit", specifier = ">=0.12" },
{ name = "typer", specifier = ">=0.17,<1.0.0" },
{ name = "uvicorn", specifier = ">=0.23,<1.0" },
{ name = "langserve", extras = ["all"], specifier = ">=0.0.51,<1.0.0" },
{ name = "tomlkit", specifier = ">=0.12.0,<1.0.0" },
{ name = "typer", specifier = ">=0.17.0,<1.0.0" },
{ name = "uvicorn", specifier = ">=0.23.0,<1.0.0" },
]
[package.metadata.requires-dev]
@@ -475,7 +474,7 @@ dev = [
]
lint = [
{ name = "mypy", specifier = ">=1.18.1,<1.19" },
{ name = "ruff", specifier = ">=0.12.2,<0.13" },
{ name = "ruff", specifier = ">=0.13.1,<0.14" },
]
test = [
{ name = "langchain", editable = "../langchain" },
@@ -500,30 +499,30 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "jsonpatch", specifier = ">=1.33,<2.0" },
{ name = "langsmith", specifier = ">=0.3.45" },
{ name = "packaging", specifier = ">=23.2" },
{ name = "pydantic", specifier = ">=2.7.4" },
{ name = "pyyaml", specifier = ">=5.3" },
{ name = "jsonpatch", specifier = ">=1.33.0,<2.0.0" },
{ name = "langsmith", specifier = ">=0.3.45,<1.0.0" },
{ name = "packaging", specifier = ">=23.2.0,<26.0.0" },
{ name = "pydantic", specifier = ">=2.7.4,<3.0.0" },
{ name = "pyyaml", specifier = ">=5.3.0,<7.0.0" },
{ name = "tenacity", specifier = ">=8.1.0,!=8.4.0,<10.0.0" },
{ name = "typing-extensions", specifier = ">=4.7" },
{ name = "typing-extensions", specifier = ">=4.7.0,<5.0.0" },
]
[package.metadata.requires-dev]
dev = [
{ name = "grandalf", specifier = ">=0.8,<1.0" },
{ name = "grandalf", specifier = ">=0.8.0,<1.0.0" },
{ name = "jupyter", specifier = ">=1.0.0,<2.0.0" },
{ name = "setuptools", specifier = ">=67.6.1,<68.0.0" },
]
lint = [{ name = "ruff", specifier = ">=0.12.2,<0.13" }]
lint = [{ name = "ruff", specifier = ">=0.13.1,<0.14.0" }]
test = [
{ name = "blockbuster", specifier = "~=1.5.18" },
{ name = "blockbuster", specifier = ">=1.5.18,<1.6.0" },
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
{ name = "grandalf", specifier = ">=0.8,<1.0" },
{ name = "grandalf", specifier = ">=0.8.0,<1.0.0" },
{ name = "langchain-tests", directory = "../standard-tests" },
{ name = "numpy", marker = "python_full_version < '3.13'", specifier = ">=1.26.4" },
{ name = "numpy", marker = "python_full_version >= '3.13'", specifier = ">=2.1.0" },
{ name = "pytest", specifier = ">=8,<9" },
{ name = "pytest", specifier = ">=8.0.0,<9.0.0" },
{ name = "pytest-asyncio", specifier = ">=0.21.1,<1.0.0" },
{ name = "pytest-benchmark" },
{ name = "pytest-codspeed" },
@@ -537,7 +536,7 @@ test = [
test-integration = []
typing = [
{ name = "langchain-text-splitters", directory = "../text-splitters" },
{ name = "mypy", specifier = ">=1.18.1,<1.19" },
{ name = "mypy", specifier = ">=1.18.1,<1.19.0" },
{ name = "types-pyyaml", specifier = ">=6.0.12.2,<7.0.0.0" },
{ name = "types-requests", specifier = ">=2.28.11.5,<3.0.0.0" },
]
@@ -560,12 +559,12 @@ dev = [
]
lint = [
{ name = "langchain-core", editable = "../core" },
{ name = "ruff", specifier = ">=0.12.8,<0.13" },
{ name = "ruff", specifier = ">=0.13.1,<0.14.0" },
]
test = [
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
{ name = "langchain-core", editable = "../core" },
{ name = "pytest", specifier = ">=8,<9" },
{ name = "pytest", specifier = ">=8.0.0,<9.0.0" },
{ name = "pytest-asyncio", specifier = ">=0.21.1,<1.0.0" },
{ name = "pytest-mock", specifier = ">=3.10.0,<4.0.0" },
{ name = "pytest-socket", specifier = ">=0.7.0,<1.0.0" },
@@ -575,7 +574,9 @@ test = [
test-integration = [
{ name = "en-core-web-sm", url = "https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl" },
{ name = "nltk", specifier = ">=3.9.1,<4.0.0" },
{ name = "sentence-transformers", specifier = ">=3.0.1" },
{ name = "scipy", marker = "python_full_version == '3.12.*'", specifier = ">=1.7.0,<2.0.0" },
{ name = "scipy", marker = "python_full_version >= '3.13'", specifier = ">=1.14.1,<2.0.0" },
{ name = "sentence-transformers", specifier = ">=3.0.1,<4.0.0" },
{ name = "spacy", specifier = ">=3.8.7,<4.0.0" },
{ name = "thinc", specifier = ">=8.3.6,<9.0.0" },
{ name = "tiktoken", specifier = ">=0.8.0,<1.0.0" },
@@ -584,14 +585,14 @@ test-integration = [
typing = [
{ name = "beautifulsoup4", specifier = ">=4.13.5,<5.0.0" },
{ name = "lxml-stubs", specifier = ">=0.5.1,<1.0.0" },
{ name = "mypy", specifier = ">=1.18.1,<1.19" },
{ name = "mypy", specifier = ">=1.18.1,<1.19.0" },
{ name = "tiktoken", specifier = ">=0.11.0,<1.0.0" },
{ name = "types-requests", specifier = ">=2.31.0.20240218,<3.0.0.0" },
]
[[package]]
name = "langserve"
version = "0.3.1"
version = "0.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -599,9 +600,9 @@ dependencies = [
{ name = "orjson" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8c/4f/465c21b7ab4ab18d9bf59f2804f7dc599a52462beb53ba84180f7d2bf581/langserve-0.3.1.tar.gz", hash = "sha256:17bf7f7d7c182623298748c2eab176c4c0e1872e20f88be89ba89a962c94988d", size = 1141070, upload-time = "2024-12-27T02:40:21.417Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/fb/86e1f5049fb3593743f0fb049c4991f4984020cda00b830ae31f2c47b46b/langserve-0.3.2.tar.gz", hash = "sha256:134b78b1d897c6bcd1fb8a6258e30cf0fb318294505e4ea59c2bea72fa152129", size = 1141270, upload-time = "2025-09-17T20:01:22.183Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/89/e4/6a26851d96c445d783d188c330cb871b56f03b18824ad8fadf6452d18a88/langserve-0.3.1-py3-none-any.whl", hash = "sha256:c860435ebbcc2c051c3e34c349c9d6020921a8af9216ad17700b58dbd01be9a2", size = 1173074, upload-time = "2024-12-27T02:40:18.328Z" },
{ url = "https://files.pythonhosted.org/packages/bd/f0/193c34bf61e1dee8bd637dbeddcc644c46d14e8b03068792ca60b1909bc1/langserve-0.3.2-py3-none-any.whl", hash = "sha256:d9c4cd19d12f6362b82ceecb10357b339b3640a858b9bc30643d5f8a0a036bce", size = 1173213, upload-time = "2025-09-17T20:01:20.603Z" },
]
[package.optional-dependencies]
@@ -612,7 +613,7 @@ all = [
[[package]]
name = "langsmith"
version = "0.4.28"
version = "0.4.31"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -623,9 +624,9 @@ dependencies = [
{ name = "requests-toolbelt" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fd/70/a3e9824f7c4823f3ed3f89f024fa25887bb82b64ab4f565dffd02b1f27f9/langsmith-0.4.28.tar.gz", hash = "sha256:8734e6d3e16ce0085b5f7235633b0e14bc8e0c160b1c1d8ce2588f83a936e171", size = 956001, upload-time = "2025-09-15T16:59:46.095Z" }
sdist = { url = "https://files.pythonhosted.org/packages/55/f5/edbdf89a162ee025348b3b2080fb3b88f4a1040a5a186f32d34aca913994/langsmith-0.4.31.tar.gz", hash = "sha256:5fb3729e22bd9a225391936cb9d1080322e6c375bb776514af06b56d6c46ed3e", size = 959698, upload-time = "2025-09-25T04:18:19.55Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/b1/cf3a4d37b7b2e9dd1f35a3d89246b0d5851aa1caff9cbf73872a106ef7f7/langsmith-0.4.28-py3-none-any.whl", hash = "sha256:0440968566d56d38d889afa202e1ff56a238e1493aea87ceb5c3c28d41d01144", size = 384724, upload-time = "2025-09-15T16:59:44.118Z" },
{ url = "https://files.pythonhosted.org/packages/3e/8e/e7a43d907a147e1f87eebdd6737483f9feba52a5d4b20f69d0bd6f2fa22f/langsmith-0.4.31-py3-none-any.whl", hash = "sha256:64f340bdead21defe5f4a6ca330c11073e35444989169f669508edf45a19025f", size = 386347, upload-time = "2025-09-25T04:18:16.69Z" },
]
[[package]]
@@ -651,7 +652,7 @@ wheels = [
[[package]]
name = "mypy"
version = "1.18.1"
version = "1.18.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
@@ -659,39 +660,39 @@ dependencies = [
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/14/a3/931e09fc02d7ba96da65266884da4e4a8806adcdb8a57faaacc6edf1d538/mypy-1.18.1.tar.gz", hash = "sha256:9e988c64ad3ac5987f43f5154f884747faf62141b7f842e87465b45299eea5a9", size = 3448447, upload-time = "2025-09-11T23:00:47.067Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/06/29ea5a34c23938ae93bc0040eb2900eb3f0f2ef4448cc59af37ab3ddae73/mypy-1.18.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2761b6ae22a2b7d8e8607fb9b81ae90bc2e95ec033fd18fa35e807af6c657763", size = 12811535, upload-time = "2025-09-11T22:58:55.399Z" },
{ url = "https://files.pythonhosted.org/packages/a8/40/04c38cb04fa9f1dc224b3e9634021a92c47b1569f1c87dfe6e63168883bb/mypy-1.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b10e3ea7f2eec23b4929a3fabf84505da21034a4f4b9613cda81217e92b74f3", size = 11897559, upload-time = "2025-09-11T22:59:48.041Z" },
{ url = "https://files.pythonhosted.org/packages/46/bf/4c535bd45ea86cebbc1a3b6a781d442f53a4883f322ebd2d442db6444d0b/mypy-1.18.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:261fbfced030228bc0f724d5d92f9ae69f46373bdfd0e04a533852677a11dbea", size = 12507430, upload-time = "2025-09-11T22:59:30.415Z" },
{ url = "https://files.pythonhosted.org/packages/e2/e1/cbefb16f2be078d09e28e0b9844e981afb41f6ffc85beb68b86c6976e641/mypy-1.18.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4dc6b34a1c6875e6286e27d836a35c0d04e8316beac4482d42cfea7ed2527df8", size = 13243717, upload-time = "2025-09-11T22:59:11.297Z" },
{ url = "https://files.pythonhosted.org/packages/65/e8/3e963da63176f16ca9caea7fa48f1bc8766de317cd961528c0391565fd47/mypy-1.18.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1cabb353194d2942522546501c0ff75c4043bf3b63069cb43274491b44b773c9", size = 13492052, upload-time = "2025-09-11T23:00:09.29Z" },
{ url = "https://files.pythonhosted.org/packages/4b/09/d5d70c252a3b5b7530662d145437bd1de15f39fa0b48a27ee4e57d254aa1/mypy-1.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:738b171690c8e47c93569635ee8ec633d2cdb06062f510b853b5f233020569a9", size = 9765846, upload-time = "2025-09-11T22:58:26.198Z" },
{ url = "https://files.pythonhosted.org/packages/32/28/47709d5d9e7068b26c0d5189c8137c8783e81065ad1102b505214a08b548/mypy-1.18.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6c903857b3e28fc5489e54042684a9509039ea0aedb2a619469438b544ae1961", size = 12734635, upload-time = "2025-09-11T23:00:24.983Z" },
{ url = "https://files.pythonhosted.org/packages/7c/12/ee5c243e52497d0e59316854041cf3b3130131b92266d0764aca4dec3c00/mypy-1.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a0c8392c19934c2b6c65566d3a6abdc6b51d5da7f5d04e43f0eb627d6eeee65", size = 11817287, upload-time = "2025-09-11T22:59:07.38Z" },
{ url = "https://files.pythonhosted.org/packages/48/bd/2aeb950151005fe708ab59725afed7c4aeeb96daf844f86a05d4b8ac34f8/mypy-1.18.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f85eb7efa2ec73ef63fc23b8af89c2fe5bf2a4ad985ed2d3ff28c1bb3c317c92", size = 12430464, upload-time = "2025-09-11T22:58:48.084Z" },
{ url = "https://files.pythonhosted.org/packages/71/e8/7a20407aafb488acb5734ad7fb5e8c2ef78d292ca2674335350fa8ebef67/mypy-1.18.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82ace21edf7ba8af31c3308a61dc72df30500f4dbb26f99ac36b4b80809d7e94", size = 13164555, upload-time = "2025-09-11T23:00:13.803Z" },
{ url = "https://files.pythonhosted.org/packages/e8/c9/5f39065252e033b60f397096f538fb57c1d9fd70a7a490f314df20dd9d64/mypy-1.18.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a2dfd53dfe632f1ef5d161150a4b1f2d0786746ae02950eb3ac108964ee2975a", size = 13359222, upload-time = "2025-09-11T23:00:33.469Z" },
{ url = "https://files.pythonhosted.org/packages/85/b6/d54111ef3c1e55992cd2ec9b8b6ce9c72a407423e93132cae209f7e7ba60/mypy-1.18.1-cp311-cp311-win_amd64.whl", hash = "sha256:320f0ad4205eefcb0e1a72428dde0ad10be73da9f92e793c36228e8ebf7298c0", size = 9760441, upload-time = "2025-09-11T23:00:44.826Z" },
{ url = "https://files.pythonhosted.org/packages/e7/14/1c3f54d606cb88a55d1567153ef3a8bc7b74702f2ff5eb64d0994f9e49cb/mypy-1.18.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:502cde8896be8e638588b90fdcb4c5d5b8c1b004dfc63fd5604a973547367bb9", size = 12911082, upload-time = "2025-09-11T23:00:41.465Z" },
{ url = "https://files.pythonhosted.org/packages/90/83/235606c8b6d50a8eba99773add907ce1d41c068edb523f81eb0d01603a83/mypy-1.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7509549b5e41be279afc1228242d0e397f1af2919a8f2877ad542b199dc4083e", size = 11919107, upload-time = "2025-09-11T22:58:40.903Z" },
{ url = "https://files.pythonhosted.org/packages/ca/25/4e2ce00f8d15b99d0c68a2536ad63e9eac033f723439ef80290ec32c1ff5/mypy-1.18.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5956ecaabb3a245e3f34100172abca1507be687377fe20e24d6a7557e07080e2", size = 12472551, upload-time = "2025-09-11T22:58:37.272Z" },
{ url = "https://files.pythonhosted.org/packages/32/bb/92642a9350fc339dd9dcefcf6862d171b52294af107d521dce075f32f298/mypy-1.18.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8750ceb014a96c9890421c83f0db53b0f3b8633e2864c6f9bc0a8e93951ed18d", size = 13340554, upload-time = "2025-09-11T22:59:38.756Z" },
{ url = "https://files.pythonhosted.org/packages/cd/ee/38d01db91c198fb6350025d28f9719ecf3c8f2c55a0094bfbf3ef478cc9a/mypy-1.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fb89ea08ff41adf59476b235293679a6eb53a7b9400f6256272fb6029bec3ce5", size = 13530933, upload-time = "2025-09-11T22:59:20.228Z" },
{ url = "https://files.pythonhosted.org/packages/da/8d/6d991ae631f80d58edbf9d7066e3f2a96e479dca955d9a968cd6e90850a3/mypy-1.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:2657654d82fcd2a87e02a33e0d23001789a554059bbf34702d623dafe353eabf", size = 9828426, upload-time = "2025-09-11T23:00:21.007Z" },
{ url = "https://files.pythonhosted.org/packages/e4/ec/ef4a7260e1460a3071628a9277a7579e7da1b071bc134ebe909323f2fbc7/mypy-1.18.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d70d2b5baf9b9a20bc9c730015615ae3243ef47fb4a58ad7b31c3e0a59b5ef1f", size = 12918671, upload-time = "2025-09-11T22:58:29.814Z" },
{ url = "https://files.pythonhosted.org/packages/a1/82/0ea6c3953f16223f0b8eda40c1aeac6bd266d15f4902556ae6e91f6fca4c/mypy-1.18.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8367e33506300f07a43012fc546402f283c3f8bcff1dc338636affb710154ce", size = 11913023, upload-time = "2025-09-11T23:00:29.049Z" },
{ url = "https://files.pythonhosted.org/packages/ae/ef/5e2057e692c2690fc27b3ed0a4dbde4388330c32e2576a23f0302bc8358d/mypy-1.18.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:913f668ec50c3337b89df22f973c1c8f0b29ee9e290a8b7fe01cc1ef7446d42e", size = 12473355, upload-time = "2025-09-11T23:00:04.544Z" },
{ url = "https://files.pythonhosted.org/packages/98/43/b7e429fc4be10e390a167b0cd1810d41cb4e4add4ae50bab96faff695a3b/mypy-1.18.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a0e70b87eb27b33209fa4792b051c6947976f6ab829daa83819df5f58330c71", size = 13346944, upload-time = "2025-09-11T22:58:23.024Z" },
{ url = "https://files.pythonhosted.org/packages/89/4e/899dba0bfe36bbd5b7c52e597de4cf47b5053d337b6d201a30e3798e77a6/mypy-1.18.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c378d946e8a60be6b6ede48c878d145546fb42aad61df998c056ec151bf6c746", size = 13512574, upload-time = "2025-09-11T22:59:52.152Z" },
{ url = "https://files.pythonhosted.org/packages/f5/f8/7661021a5b0e501b76440454d786b0f01bb05d5c4b125fcbda02023d0250/mypy-1.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:2cd2c1e0f3a7465f22731987fff6fc427e3dcbb4ca5f7db5bbeaff2ff9a31f6d", size = 9837684, upload-time = "2025-09-11T22:58:44.454Z" },
{ url = "https://files.pythonhosted.org/packages/bf/87/7b173981466219eccc64c107cf8e5ab9eb39cc304b4c07df8e7881533e4f/mypy-1.18.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ba24603c58e34dd5b096dfad792d87b304fc6470cbb1c22fd64e7ebd17edcc61", size = 12900265, upload-time = "2025-09-11T22:59:03.4Z" },
{ url = "https://files.pythonhosted.org/packages/ae/cc/b10e65bae75b18a5ac8f81b1e8e5867677e418f0dd2c83b8e2de9ba96ebd/mypy-1.18.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ed36662fb92ae4cb3cacc682ec6656208f323bbc23d4b08d091eecfc0863d4b5", size = 11942890, upload-time = "2025-09-11T23:00:00.607Z" },
{ url = "https://files.pythonhosted.org/packages/39/d4/aeefa07c44d09f4c2102e525e2031bc066d12e5351f66b8a83719671004d/mypy-1.18.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:040ecc95e026f71a9ad7956fea2724466602b561e6a25c2e5584160d3833aaa8", size = 12472291, upload-time = "2025-09-11T22:59:43.425Z" },
{ url = "https://files.pythonhosted.org/packages/c6/07/711e78668ff8e365f8c19735594ea95938bff3639a4c46a905e3ed8ff2d6/mypy-1.18.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:937e3ed86cb731276706e46e03512547e43c391a13f363e08d0fee49a7c38a0d", size = 13318610, upload-time = "2025-09-11T23:00:17.604Z" },
{ url = "https://files.pythonhosted.org/packages/ca/85/df3b2d39339c31d360ce299b418c55e8194ef3205284739b64962f6074e7/mypy-1.18.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1f95cc4f01c0f1701ca3b0355792bccec13ecb2ec1c469e5b85a6ef398398b1d", size = 13513697, upload-time = "2025-09-11T22:58:59.534Z" },
{ url = "https://files.pythonhosted.org/packages/b1/df/462866163c99ea73bb28f0eb4d415c087e30de5d36ee0f5429d42e28689b/mypy-1.18.1-cp314-cp314-win_amd64.whl", hash = "sha256:e4f16c0019d48941220ac60b893615be2f63afedaba6a0801bdcd041b96991ce", size = 9985739, upload-time = "2025-09-11T22:58:51.644Z" },
{ url = "https://files.pythonhosted.org/packages/e0/1d/4b97d3089b48ef3d904c9ca69fab044475bd03245d878f5f0b3ea1daf7ce/mypy-1.18.1-py3-none-any.whl", hash = "sha256:b76a4de66a0ac01da1be14ecc8ae88ddea33b8380284a9e3eae39d57ebcbe26e", size = 2352212, upload-time = "2025-09-11T22:59:26.576Z" },
{ url = "https://files.pythonhosted.org/packages/03/6f/657961a0743cff32e6c0611b63ff1c1970a0b482ace35b069203bf705187/mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c", size = 12807973, upload-time = "2025-09-19T00:10:35.282Z" },
{ url = "https://files.pythonhosted.org/packages/10/e9/420822d4f661f13ca8900f5fa239b40ee3be8b62b32f3357df9a3045a08b/mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e", size = 11896527, upload-time = "2025-09-19T00:10:55.791Z" },
{ url = "https://files.pythonhosted.org/packages/aa/73/a05b2bbaa7005f4642fcfe40fb73f2b4fb6bb44229bd585b5878e9a87ef8/mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b", size = 12507004, upload-time = "2025-09-19T00:11:05.411Z" },
{ url = "https://files.pythonhosted.org/packages/4f/01/f6e4b9f0d031c11ccbd6f17da26564f3a0f3c4155af344006434b0a05a9d/mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66", size = 13245947, upload-time = "2025-09-19T00:10:46.923Z" },
{ url = "https://files.pythonhosted.org/packages/d7/97/19727e7499bfa1ae0773d06afd30ac66a58ed7437d940c70548634b24185/mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428", size = 13499217, upload-time = "2025-09-19T00:09:39.472Z" },
{ url = "https://files.pythonhosted.org/packages/9f/4f/90dc8c15c1441bf31cf0f9918bb077e452618708199e530f4cbd5cede6ff/mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed", size = 9766753, upload-time = "2025-09-19T00:10:49.161Z" },
{ url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" },
{ url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" },
{ url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" },
{ url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" },
{ url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" },
{ url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" },
{ url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" },
{ url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" },
{ url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" },
{ url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" },
{ url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" },
{ url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" },
{ url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" },
{ url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" },
{ url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" },
{ url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" },
{ url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" },
{ url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" },
{ url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" },
{ url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" },
{ url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" },
{ url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" },
{ url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" },
{ url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" },
{ url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" },
]
[[package]]
@@ -1035,28 +1036,28 @@ wheels = [
[[package]]
name = "ruff"
version = "0.12.12"
version = "0.13.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a8/f0/e0965dd709b8cabe6356811c0ee8c096806bb57d20b5019eb4e48a117410/ruff-0.12.12.tar.gz", hash = "sha256:b86cd3415dbe31b3b46a71c598f4c4b2f550346d1ccf6326b347cc0c8fd063d6", size = 5359915, upload-time = "2025-09-04T16:50:18.273Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ab/33/c8e89216845615d14d2d42ba2bee404e7206a8db782f33400754f3799f05/ruff-0.13.1.tar.gz", hash = "sha256:88074c3849087f153d4bb22e92243ad4c1b366d7055f98726bc19aa08dc12d51", size = 5397987, upload-time = "2025-09-18T19:52:44.33Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/09/79/8d3d687224d88367b51c7974cec1040c4b015772bfbeffac95face14c04a/ruff-0.12.12-py3-none-linux_armv6l.whl", hash = "sha256:de1c4b916d98ab289818e55ce481e2cacfaad7710b01d1f990c497edf217dafc", size = 12116602, upload-time = "2025-09-04T16:49:18.892Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c3/6e599657fe192462f94861a09aae935b869aea8a1da07f47d6eae471397c/ruff-0.12.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7acd6045e87fac75a0b0cdedacf9ab3e1ad9d929d149785903cff9bb69ad9727", size = 12868393, upload-time = "2025-09-04T16:49:23.043Z" },
{ url = "https://files.pythonhosted.org/packages/e8/d2/9e3e40d399abc95336b1843f52fc0daaceb672d0e3c9290a28ff1a96f79d/ruff-0.12.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:abf4073688d7d6da16611f2f126be86523a8ec4343d15d276c614bda8ec44edb", size = 12036967, upload-time = "2025-09-04T16:49:26.04Z" },
{ url = "https://files.pythonhosted.org/packages/e9/03/6816b2ed08836be272e87107d905f0908be5b4a40c14bfc91043e76631b8/ruff-0.12.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:968e77094b1d7a576992ac078557d1439df678a34c6fe02fd979f973af167577", size = 12276038, upload-time = "2025-09-04T16:49:29.056Z" },
{ url = "https://files.pythonhosted.org/packages/9f/d5/707b92a61310edf358a389477eabd8af68f375c0ef858194be97ca5b6069/ruff-0.12.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42a67d16e5b1ffc6d21c5f67851e0e769517fb57a8ebad1d0781b30888aa704e", size = 11901110, upload-time = "2025-09-04T16:49:32.07Z" },
{ url = "https://files.pythonhosted.org/packages/9d/3d/f8b1038f4b9822e26ec3d5b49cf2bc313e3c1564cceb4c1a42820bf74853/ruff-0.12.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b216ec0a0674e4b1214dcc998a5088e54eaf39417327b19ffefba1c4a1e4971e", size = 13668352, upload-time = "2025-09-04T16:49:35.148Z" },
{ url = "https://files.pythonhosted.org/packages/98/0e/91421368ae6c4f3765dd41a150f760c5f725516028a6be30e58255e3c668/ruff-0.12.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:59f909c0fdd8f1dcdbfed0b9569b8bf428cf144bec87d9de298dcd4723f5bee8", size = 14638365, upload-time = "2025-09-04T16:49:38.892Z" },
{ url = "https://files.pythonhosted.org/packages/74/5d/88f3f06a142f58ecc8ecb0c2fe0b82343e2a2b04dcd098809f717cf74b6c/ruff-0.12.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ac93d87047e765336f0c18eacad51dad0c1c33c9df7484c40f98e1d773876f5", size = 14060812, upload-time = "2025-09-04T16:49:42.732Z" },
{ url = "https://files.pythonhosted.org/packages/13/fc/8962e7ddd2e81863d5c92400820f650b86f97ff919c59836fbc4c1a6d84c/ruff-0.12.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01543c137fd3650d322922e8b14cc133b8ea734617c4891c5a9fccf4bfc9aa92", size = 13050208, upload-time = "2025-09-04T16:49:46.434Z" },
{ url = "https://files.pythonhosted.org/packages/53/06/8deb52d48a9a624fd37390555d9589e719eac568c020b27e96eed671f25f/ruff-0.12.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afc2fa864197634e549d87fb1e7b6feb01df0a80fd510d6489e1ce8c0b1cc45", size = 13311444, upload-time = "2025-09-04T16:49:49.931Z" },
{ url = "https://files.pythonhosted.org/packages/2a/81/de5a29af7eb8f341f8140867ffb93f82e4fde7256dadee79016ac87c2716/ruff-0.12.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0c0945246f5ad776cb8925e36af2438e66188d2b57d9cf2eed2c382c58b371e5", size = 13279474, upload-time = "2025-09-04T16:49:53.465Z" },
{ url = "https://files.pythonhosted.org/packages/7f/14/d9577fdeaf791737ada1b4f5c6b59c21c3326f3f683229096cccd7674e0c/ruff-0.12.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0fbafe8c58e37aae28b84a80ba1817f2ea552e9450156018a478bf1fa80f4e4", size = 12070204, upload-time = "2025-09-04T16:49:56.882Z" },
{ url = "https://files.pythonhosted.org/packages/77/04/a910078284b47fad54506dc0af13839c418ff704e341c176f64e1127e461/ruff-0.12.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b9c456fb2fc8e1282affa932c9e40f5ec31ec9cbb66751a316bd131273b57c23", size = 11880347, upload-time = "2025-09-04T16:49:59.729Z" },
{ url = "https://files.pythonhosted.org/packages/df/58/30185fcb0e89f05e7ea82e5817b47798f7fa7179863f9d9ba6fd4fe1b098/ruff-0.12.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f12856123b0ad0147d90b3961f5c90e7427f9acd4b40050705499c98983f489", size = 12891844, upload-time = "2025-09-04T16:50:02.591Z" },
{ url = "https://files.pythonhosted.org/packages/21/9c/28a8dacce4855e6703dcb8cdf6c1705d0b23dd01d60150786cd55aa93b16/ruff-0.12.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26a1b5a2bf7dd2c47e3b46d077cd9c0fc3b93e6c6cc9ed750bd312ae9dc302ee", size = 13360687, upload-time = "2025-09-04T16:50:05.8Z" },
{ url = "https://files.pythonhosted.org/packages/c8/fa/05b6428a008e60f79546c943e54068316f32ec8ab5c4f73e4563934fbdc7/ruff-0.12.12-py3-none-win32.whl", hash = "sha256:173be2bfc142af07a01e3a759aba6f7791aa47acf3604f610b1c36db888df7b1", size = 12052870, upload-time = "2025-09-04T16:50:09.121Z" },
{ url = "https://files.pythonhosted.org/packages/85/60/d1e335417804df452589271818749d061b22772b87efda88354cf35cdb7a/ruff-0.12.12-py3-none-win_amd64.whl", hash = "sha256:e99620bf01884e5f38611934c09dd194eb665b0109104acae3ba6102b600fd0d", size = 13178016, upload-time = "2025-09-04T16:50:12.559Z" },
{ url = "https://files.pythonhosted.org/packages/28/7e/61c42657f6e4614a4258f1c3b0c5b93adc4d1f8575f5229d1906b483099b/ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093", size = 12256762, upload-time = "2025-09-04T16:50:15.737Z" },
{ url = "https://files.pythonhosted.org/packages/f3/41/ca37e340938f45cfb8557a97a5c347e718ef34702546b174e5300dbb1f28/ruff-0.13.1-py3-none-linux_armv6l.whl", hash = "sha256:b2abff595cc3cbfa55e509d89439b5a09a6ee3c252d92020bd2de240836cf45b", size = 12304308, upload-time = "2025-09-18T19:51:56.253Z" },
{ url = "https://files.pythonhosted.org/packages/ff/84/ba378ef4129415066c3e1c80d84e539a0d52feb250685091f874804f28af/ruff-0.13.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4ee9f4249bf7f8bb3984c41bfaf6a658162cdb1b22e3103eabc7dd1dc5579334", size = 12937258, upload-time = "2025-09-18T19:52:00.184Z" },
{ url = "https://files.pythonhosted.org/packages/8d/b6/ec5e4559ae0ad955515c176910d6d7c93edcbc0ed1a3195a41179c58431d/ruff-0.13.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c5da4af5f6418c07d75e6f3224e08147441f5d1eac2e6ce10dcce5e616a3bae", size = 12214554, upload-time = "2025-09-18T19:52:02.753Z" },
{ url = "https://files.pythonhosted.org/packages/70/d6/cb3e3b4f03b9b0c4d4d8f06126d34b3394f6b4d764912fe80a1300696ef6/ruff-0.13.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80524f84a01355a59a93cef98d804e2137639823bcee2931f5028e71134a954e", size = 12448181, upload-time = "2025-09-18T19:52:05.279Z" },
{ url = "https://files.pythonhosted.org/packages/d2/ea/bf60cb46d7ade706a246cd3fb99e4cfe854efa3dfbe530d049c684da24ff/ruff-0.13.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff7f5ce8d7988767dd46a148192a14d0f48d1baea733f055d9064875c7d50389", size = 12104599, upload-time = "2025-09-18T19:52:07.497Z" },
{ url = "https://files.pythonhosted.org/packages/2d/3e/05f72f4c3d3a69e65d55a13e1dd1ade76c106d8546e7e54501d31f1dc54a/ruff-0.13.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c55d84715061f8b05469cdc9a446aa6c7294cd4bd55e86a89e572dba14374f8c", size = 13791178, upload-time = "2025-09-18T19:52:10.189Z" },
{ url = "https://files.pythonhosted.org/packages/81/e7/01b1fc403dd45d6cfe600725270ecc6a8f8a48a55bc6521ad820ed3ceaf8/ruff-0.13.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ac57fed932d90fa1624c946dc67a0a3388d65a7edc7d2d8e4ca7bddaa789b3b0", size = 14814474, upload-time = "2025-09-18T19:52:12.866Z" },
{ url = "https://files.pythonhosted.org/packages/fa/92/d9e183d4ed6185a8df2ce9faa3f22e80e95b5f88d9cc3d86a6d94331da3f/ruff-0.13.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c366a71d5b4f41f86a008694f7a0d75fe409ec298685ff72dc882f882d532e36", size = 14217531, upload-time = "2025-09-18T19:52:15.245Z" },
{ url = "https://files.pythonhosted.org/packages/3b/4a/6ddb1b11d60888be224d721e01bdd2d81faaf1720592858ab8bac3600466/ruff-0.13.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4ea9d1b5ad3e7a83ee8ebb1229c33e5fe771e833d6d3dcfca7b77d95b060d38", size = 13265267, upload-time = "2025-09-18T19:52:17.649Z" },
{ url = "https://files.pythonhosted.org/packages/81/98/3f1d18a8d9ea33ef2ad508f0417fcb182c99b23258ec5e53d15db8289809/ruff-0.13.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0f70202996055b555d3d74b626406476cc692f37b13bac8828acff058c9966a", size = 13243120, upload-time = "2025-09-18T19:52:20.332Z" },
{ url = "https://files.pythonhosted.org/packages/8d/86/b6ce62ce9c12765fa6c65078d1938d2490b2b1d9273d0de384952b43c490/ruff-0.13.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f8cff7a105dad631085d9505b491db33848007d6b487c3c1979dd8d9b2963783", size = 13443084, upload-time = "2025-09-18T19:52:23.032Z" },
{ url = "https://files.pythonhosted.org/packages/a1/6e/af7943466a41338d04503fb5a81b2fd07251bd272f546622e5b1599a7976/ruff-0.13.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9761e84255443316a258dd7dfbd9bfb59c756e52237ed42494917b2577697c6a", size = 12295105, upload-time = "2025-09-18T19:52:25.263Z" },
{ url = "https://files.pythonhosted.org/packages/3f/97/0249b9a24f0f3ebd12f007e81c87cec6d311de566885e9309fcbac5b24cc/ruff-0.13.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3d376a88c3102ef228b102211ef4a6d13df330cb0f5ca56fdac04ccec2a99700", size = 12072284, upload-time = "2025-09-18T19:52:27.478Z" },
{ url = "https://files.pythonhosted.org/packages/f6/85/0b64693b2c99d62ae65236ef74508ba39c3febd01466ef7f354885e5050c/ruff-0.13.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cbefd60082b517a82c6ec8836989775ac05f8991715d228b3c1d86ccc7df7dae", size = 12970314, upload-time = "2025-09-18T19:52:30.212Z" },
{ url = "https://files.pythonhosted.org/packages/96/fc/342e9f28179915d28b3747b7654f932ca472afbf7090fc0c4011e802f494/ruff-0.13.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd16b9a5a499fe73f3c2ef09a7885cb1d97058614d601809d37c422ed1525317", size = 13422360, upload-time = "2025-09-18T19:52:32.676Z" },
{ url = "https://files.pythonhosted.org/packages/37/54/6177a0dc10bce6f43e392a2192e6018755473283d0cf43cc7e6afc182aea/ruff-0.13.1-py3-none-win32.whl", hash = "sha256:55e9efa692d7cb18580279f1fbb525146adc401f40735edf0aaeabd93099f9a0", size = 12178448, upload-time = "2025-09-18T19:52:35.545Z" },
{ url = "https://files.pythonhosted.org/packages/64/51/c6a3a33d9938007b8bdc8ca852ecc8d810a407fb513ab08e34af12dc7c24/ruff-0.13.1-py3-none-win_amd64.whl", hash = "sha256:3a3fb595287ee556de947183489f636b9f76a72f0fa9c028bdcabf5bab2cc5e5", size = 13286458, upload-time = "2025-09-18T19:52:38.198Z" },
{ url = "https://files.pythonhosted.org/packages/fd/04/afc078a12cf68592345b1e2d6ecdff837d286bac023d7a22c54c7a698c5b/ruff-0.13.1-py3-none-win_arm64.whl", hash = "sha256:c0bae9ffd92d54e03c2bf266f466da0a65e145f298ee5b5846ed435f6a00518a", size = 12437893, upload-time = "2025-09-18T19:52:41.283Z" },
]
[[package]]
@@ -1218,7 +1219,7 @@ wheels = [
[[package]]
name = "typer"
version = "0.17.4"
version = "0.19.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
@@ -1226,9 +1227,9 @@ dependencies = [
{ name = "shellingham" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/92/e8/2a73ccf9874ec4c7638f172efc8972ceab13a0e3480b389d6ed822f7a822/typer-0.17.4.tar.gz", hash = "sha256:b77dc07d849312fd2bb5e7f20a7af8985c7ec360c45b051ed5412f64d8dc1580", size = 103734, upload-time = "2025-09-05T18:14:40.746Z" }
sdist = { url = "https://files.pythonhosted.org/packages/21/ca/950278884e2ca20547ff3eb109478c6baf6b8cf219318e6bc4f666fad8e8/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca", size = 104755, upload-time = "2025-09-23T09:47:48.256Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/93/72/6b3e70d32e89a5cbb6a4513726c1ae8762165b027af569289e19ec08edd8/typer-0.17.4-py3-none-any.whl", hash = "sha256:015534a6edaa450e7007eba705d5c18c3349dcea50a6ad79a5ed530967575824", size = 46643, upload-time = "2025-09-05T18:14:39.166Z" },
{ url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" },
]
[[package]]
@@ -1263,16 +1264,16 @@ wheels = [
[[package]]
name = "uvicorn"
version = "0.35.0"
version = "0.37.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" }
sdist = { url = "https://files.pythonhosted.org/packages/71/57/1616c8274c3442d802621abf5deb230771c7a0fec9414cb6763900eb3868/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13", size = 80367, upload-time = "2025-09-23T13:33:47.486Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" },
{ url = "https://files.pythonhosted.org/packages/85/cd/584a2ceb5532af99dd09e50919e3615ba99aa127e9850eafe5f31ddfdb9a/uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c", size = 67976, upload-time = "2025-09-23T13:33:45.842Z" },
]
[[package]]

View File

@@ -58,12 +58,6 @@ format format_diff:
[ "$(PYTHON_FILES)" = "" ] || uv run --all-groups ruff format $(PYTHON_FILES)
[ "$(PYTHON_FILES)" = "" ] || uv run --all-groups ruff check --fix $(PYTHON_FILES)
spell_check:
uv run --all-groups codespell --toml pyproject.toml
spell_fix:
uv run --all-groups codespell --toml pyproject.toml -w
benchmark:
uv run pytest tests/benchmarks --codspeed

View File

@@ -174,6 +174,7 @@ def beta(
def finalize(_wrapper: Callable[..., Any], new_doc: str) -> Any:
"""Finalize the property."""
return property(fget=_fget, fset=_fset, fdel=_fdel, doc=new_doc)
else:
_name = _name or obj.__qualname__
if not _obj_type:
@@ -226,17 +227,17 @@ def warn_beta(
) -> None:
"""Display a standardized beta annotation.
Arguments:
message : str, optional
Args:
message:
Override the default beta message. The
%(name)s, %(obj_type)s, %(addendum)s
format specifiers will be replaced by the
values of the respective arguments passed to this function.
name : str, optional
name:
The name of the annotated object.
obj_type : str, optional
obj_type:
The object type being annotated.
addendum : str, optional
addendum:
Additional text appended directly to the final message.
"""
if not message:

View File

@@ -431,35 +431,35 @@ def warn_deprecated(
) -> None:
"""Display a standardized deprecation.
Arguments:
since : str
Args:
since:
The release at which this API became deprecated.
message : str, optional
message:
Override the default deprecation message. The %(since)s,
%(name)s, %(alternative)s, %(obj_type)s, %(addendum)s,
and %(removal)s format specifiers will be replaced by the
values of the respective arguments passed to this function.
name : str, optional
name:
The name of the deprecated object.
alternative : str, optional
alternative:
An alternative API that the user may use in place of the
deprecated API. The deprecation warning will tell the user
about this alternative if provided.
alternative_import: str, optional
alternative_import:
An alternative import that the user may use instead.
pending : bool, optional
pending:
If True, uses a PendingDeprecationWarning instead of a
DeprecationWarning. Cannot be used together with removal.
obj_type : str, optional
obj_type:
The object type being deprecated.
addendum : str, optional
addendum:
Additional text appended directly to the final message.
removal : str, optional
removal:
The expected removal version. With the default (an empty
string), a removal version is automatically computed from
since. Set to other Falsy values to not schedule a removal
date. Cannot be used together with pending.
package: str, optional
package:
The package of the deprecated object.
"""
if not pending:

View File

@@ -92,7 +92,7 @@ def trace_as_chain_group(
metadata (dict[str, Any], optional): The metadata to apply to all runs.
Defaults to None.
.. note:
.. note::
Must have ``LANGCHAIN_TRACING_V2`` env var set to true to see the trace in
LangSmith.
@@ -179,7 +179,7 @@ async def atrace_as_chain_group(
Yields:
The async callback manager for the chain group.
.. note:
.. note::
Must have ``LANGCHAIN_TRACING_V2`` env var set to true to see the trace in
LangSmith.

View File

@@ -32,7 +32,7 @@ class UsageMetadataCallbackHandler(BaseCallbackHandler):
result_2 = llm_2.invoke("Hello", config={"callbacks": [callback]})
callback.usage_metadata
.. code-block:: none
.. code-block::
{'gpt-4o-mini-2024-07-18': {'input_tokens': 8,
'output_tokens': 10,
@@ -119,7 +119,7 @@ def get_usage_metadata_callback(
llm_2.invoke("Hello")
print(cb.usage_metadata)
.. code-block:: none
.. code-block::
{'gpt-4o-mini-2024-07-18': {'input_tokens': 8,
'output_tokens': 10,

View File

@@ -31,7 +31,7 @@ class LangSmithLoader(BaseLoader):
for doc in loader.lazy_load():
docs.append(doc)
.. code-block:: pycon
.. code-block:: python
# -> [Document("...", metadata={"inputs": {...}, "outputs": {...}, ...}), ...]

View File

@@ -296,7 +296,11 @@ def index(
For the time being, documents are indexed using their hashes, and users
are not able to specify the uid of the document.
Important:
.. versionchanged:: 0.3.25
Added ``scoped_full`` cleanup mode.
.. important::
* In full mode, the loader should be returning
the entire dataset, and not just a subset of the dataset.
Otherwise, the auto_cleanup will remove documents that it is not
@@ -309,7 +313,7 @@ def index(
chunks, and we index them using a batch size of 5, we'll have 3 batches
all with the same source id. In general, to avoid doing too much
redundant work select as big a batch size as possible.
* The `scoped_full` mode is suitable if determining an appropriate batch size
* The ``scoped_full`` mode is suitable if determining an appropriate batch size
is challenging or if your data loader cannot return the entire dataset at
once. This mode keeps track of source IDs in memory, which should be fine
for most use cases. If your dataset is large (10M+ docs), you will likely
@@ -378,10 +382,6 @@ def index(
TypeError: If ``vectorstore`` is not a VectorStore or a DocumentIndex.
AssertionError: If ``source_id`` is None when cleanup mode is incremental.
(should be unreachable code).
.. version_modified:: 0.3.25
* Added `scoped_full` cleanup mode.
"""
# Behavior is deprecated, but we keep it for backwards compatibility.
# # Warn only once per process.
@@ -636,26 +636,30 @@ async def aindex(
documents were deleted, which documents should be skipped.
For the time being, documents are indexed using their hashes, and users
are not able to specify the uid of the document.
are not able to specify the uid of the document.
Important:
* In full mode, the loader should be returning
the entire dataset, and not just a subset of the dataset.
Otherwise, the auto_cleanup will remove documents that it is not
supposed to.
* In incremental mode, if documents associated with a particular
source id appear across different batches, the indexing API
will do some redundant work. This will still result in the
correct end state of the index, but will unfortunately not be
100% efficient. For example, if a given document is split into 15
chunks, and we index them using a batch size of 5, we'll have 3 batches
all with the same source id. In general, to avoid doing too much
redundant work select as big a batch size as possible.
* The `scoped_full` mode is suitable if determining an appropriate batch size
is challenging or if your data loader cannot return the entire dataset at
once. This mode keeps track of source IDs in memory, which should be fine
for most use cases. If your dataset is large (10M+ docs), you will likely
need to parallelize the indexing process regardless.
.. versionchanged:: 0.3.25
Added ``scoped_full`` cleanup mode.
.. important::
* In full mode, the loader should be returning
the entire dataset, and not just a subset of the dataset.
Otherwise, the auto_cleanup will remove documents that it is not
supposed to.
* In incremental mode, if documents associated with a particular
source id appear across different batches, the indexing API
will do some redundant work. This will still result in the
correct end state of the index, but will unfortunately not be
100% efficient. For example, if a given document is split into 15
chunks, and we index them using a batch size of 5, we'll have 3 batches
all with the same source id. In general, to avoid doing too much
redundant work select as big a batch size as possible.
* The ``scoped_full`` mode is suitable if determining an appropriate batch size
is challenging or if your data loader cannot return the entire dataset at
once. This mode keeps track of source IDs in memory, which should be fine
for most use cases. If your dataset is large (10M+ docs), you will likely
need to parallelize the indexing process regardless.
Args:
docs_source: Data loader or iterable of documents to index.
@@ -720,10 +724,6 @@ async def aindex(
TypeError: If ``vector_store`` is not a VectorStore or DocumentIndex.
AssertionError: If ``source_id_key`` is None when cleanup mode is
incremental or ``scoped_full`` (should be unreachable).
.. version_modified:: 0.3.25
* Added `scoped_full` cleanup mode.
"""
# Behavior is deprecated, but we keep it for backwards compatibility.
# # Warn only once per process.

View File

@@ -471,7 +471,7 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
**kwargs: Any,
) -> Iterator[BaseMessageChunk]:
if not self._should_stream(async_api=False, **{**kwargs, "stream": True}):
# model doesn't implement streaming, so use default implementation
# Model doesn't implement streaming, so use default implementation
yield cast(
"BaseMessageChunk",
self.invoke(input, config=config, stop=stop, **kwargs),

View File

@@ -19,7 +19,7 @@ from langchain_core.runnables import RunnableConfig
class FakeMessagesListChatModel(BaseChatModel):
"""Fake ChatModel for testing purposes."""
"""Fake ``ChatModel`` for testing purposes."""
responses: list[BaseMessage]
"""List of responses to **cycle** through in order."""
@@ -212,10 +212,11 @@ class GenericFakeChatModel(BaseChatModel):
"""Generic fake chat model that can be used to test the chat model interface.
* Chat model should be usable in both sync and async tests
* Invokes on_llm_new_token to allow for testing of callback related code for new
* Invokes ``on_llm_new_token`` to allow for testing of callback related code for new
tokens.
* Includes logic to break messages into message chunk to facilitate testing of
streaming.
"""
messages: Iterator[Union[AIMessage, str]]
@@ -230,6 +231,7 @@ class GenericFakeChatModel(BaseChatModel):
.. warning::
Streaming is not implemented yet. We should try to implement it in the future by
delegating to invoke and then breaking the resulting output into message chunks.
"""
@override
@@ -351,6 +353,7 @@ class ParrotFakeChatModel(BaseChatModel):
"""Generic fake chat model that can be used to test the chat model interface.
* Chat model should be usable in both sync and async tests
"""
@override

View File

@@ -111,7 +111,7 @@ class Serializable(BaseModel, ABC):
# Remove default BaseModel init docstring.
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""""" # noqa: D419
"""""" # noqa: D419 # Intentional blank docstring
super().__init__(*args, **kwargs)
@classmethod

View File

@@ -45,7 +45,6 @@ class InputTokenDetails(TypedDict, total=False):
Does *not* need to sum to full input token count. Does *not* need to have all keys.
Example:
.. code-block:: python
{
@@ -72,6 +71,7 @@ class InputTokenDetails(TypedDict, total=False):
Since there was a cache hit, the tokens were read from the cache. More precisely,
the model state given these tokens was read from the cache.
"""
@@ -81,7 +81,6 @@ class OutputTokenDetails(TypedDict, total=False):
Does *not* need to sum to full output token count. Does *not* need to have all keys.
Example:
.. code-block:: python
{
@@ -100,6 +99,7 @@ class OutputTokenDetails(TypedDict, total=False):
Tokens generated by the model in a chain of thought process (i.e. by OpenAI's o1
models) that are not returned as part of model output.
"""
@@ -109,7 +109,6 @@ class UsageMetadata(TypedDict):
This is a standard representation of token usage that is consistent across models.
Example:
.. code-block:: python
{
@@ -148,6 +147,7 @@ class UsageMetadata(TypedDict):
"""Breakdown of output token counts.
Does *not* need to sum to full output token count. Does *not* need to have all keys.
"""
@@ -159,12 +159,14 @@ class AIMessage(BaseMessage):
This message represents the output of the model and consists of both
the raw output as returned by the model together standardized fields
(e.g., tool calls, usage metadata) added by the LangChain framework.
"""
example: bool = False
"""Use to denote that a message is part of an example conversation.
At the moment, this is ignored by most models. Usage is discouraged.
"""
tool_calls: list[ToolCall] = []
@@ -175,15 +177,18 @@ class AIMessage(BaseMessage):
"""If provided, usage metadata for a message, such as token counts.
This is a standard representation of token usage that is consistent across models.
"""
type: Literal["ai"] = "ai"
"""The type of the message (used for deserialization). Defaults to "ai"."""
"""The type of the message (used for deserialization). Defaults to ``'ai'``."""
def __init__(
self, content: Union[str, list[Union[str, dict]]], **kwargs: Any
self,
content: Union[str, list[Union[str, dict]]],
**kwargs: Any,
) -> None:
"""Pass in content as positional arg.
"""Initialize ``AIMessage``.
Args:
content: The content of the message.
@@ -254,6 +259,7 @@ class AIMessage(BaseMessage):
Returns:
A pretty representation of the message.
"""
base = super().pretty_repr(html=html)
lines = []
@@ -293,7 +299,10 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
# non-chunk variant.
type: Literal["AIMessageChunk"] = "AIMessageChunk" # type: ignore[assignment]
"""The type of the message (used for deserialization).
Defaults to "AIMessageChunk"."""
Defaults to ``AIMessageChunk``.
"""
tool_call_chunks: list[ToolCallChunk] = []
"""If provided, tool call chunks associated with the message."""
@@ -311,7 +320,10 @@ class AIMessageChunk(AIMessage, BaseMessageChunk):
"""Initialize tool calls from tool call chunks.
Returns:
This ``AIMessageChunk``.
The values with tool calls initialized.
Raises:
ValueError: If the tool call chunks are malformed.
"""
if not self.tool_call_chunks:
if self.tool_calls:
@@ -522,9 +534,9 @@ def add_usage(
def subtract_usage(
left: Optional[UsageMetadata], right: Optional[UsageMetadata]
) -> UsageMetadata:
"""Recursively subtract two UsageMetadata objects.
"""Recursively subtract two ``UsageMetadata`` objects.
Token counts cannot be negative so the actual operation is max(left - right, 0).
Token counts cannot be negative so the actual operation is ``max(left - right, 0)``.
Example:
.. code-block:: python

View File

@@ -20,7 +20,7 @@ if TYPE_CHECKING:
class BaseMessage(Serializable):
"""Base abstract message class.
Messages are the inputs and outputs of ChatModels.
Messages are the inputs and outputs of ``ChatModel``s.
"""
content: Union[str, list[Union[str, dict]]]
@@ -31,17 +31,18 @@ class BaseMessage(Serializable):
For example, for a message from an AI, this could include tool calls as
encoded by the model provider.
"""
response_metadata: dict = Field(default_factory=dict)
"""Response metadata. For example: response headers, logprobs, token counts, model
name."""
"""Examples: response headers, logprobs, token counts, model name."""
type: str
"""The type of the message. Must be a string that is unique to the message type.
The purpose of this field is to allow for easy identification of the message type
when deserializing messages.
"""
name: Optional[str] = None
@@ -51,20 +52,26 @@ class BaseMessage(Serializable):
Usage of this field is optional, and whether it's used or not is up to the
model implementation.
"""
id: Optional[str] = Field(default=None, coerce_numbers_to_str=True)
"""An optional unique identifier for the message. This should ideally be
provided by the provider/model which created the message."""
"""An optional unique identifier for the message.
This should ideally be provided by the provider/model which created the message.
"""
model_config = ConfigDict(
extra="allow",
)
def __init__(
self, content: Union[str, list[Union[str, dict]]], **kwargs: Any
self,
content: Union[str, list[Union[str, dict]]],
**kwargs: Any,
) -> None:
"""Pass in content as positional arg.
"""Initialize ``BaseMessage``.
Args:
content: The string contents of the message.
@@ -73,7 +80,7 @@ class BaseMessage(Serializable):
@classmethod
def is_lc_serializable(cls) -> bool:
"""BaseMessage is serializable.
"""``BaseMessage`` is serializable.
Returns:
True
@@ -90,10 +97,11 @@ class BaseMessage(Serializable):
return ["langchain", "schema", "messages"]
def text(self) -> str:
"""Get the text content of the message.
"""Get the text ``content`` of the message.
Returns:
The text content of the message.
"""
if isinstance(self.content, str):
return self.content
@@ -136,6 +144,7 @@ class BaseMessage(Serializable):
Returns:
A pretty representation of the message.
"""
title = get_msg_title_repr(self.type.title() + " Message", bold=html)
# TODO: handle non-string content.
@@ -155,11 +164,12 @@ def merge_content(
"""Merge multiple message contents.
Args:
first_content: The first content. Can be a string or a list.
contents: The other contents. Can be a string or a list.
first_content: The first ``content``. Can be a string or a list.
contents: The other ``content``s. Can be a string or a list.
Returns:
The merged content.
"""
merged = first_content
for content in contents:
@@ -207,9 +217,10 @@ class BaseMessageChunk(BaseMessage):
For example,
`AIMessageChunk(content="Hello") + AIMessageChunk(content=" World")`
``AIMessageChunk(content="Hello") + AIMessageChunk(content=" World")``
will give ``AIMessageChunk(content="Hello World")``
will give `AIMessageChunk(content="Hello World")`
"""
if isinstance(other, BaseMessageChunk):
# If both are (subclasses of) BaseMessageChunk,
@@ -257,8 +268,9 @@ def message_to_dict(message: BaseMessage) -> dict:
message: Message to convert.
Returns:
Message as a dict. The dict will have a "type" key with the message type
and a "data" key with the message data as a dict.
Message as a dict. The dict will have a ``type`` key with the message type
and a ``data`` key with the message data as a dict.
"""
return {"type": message.type, "data": message.model_dump()}
@@ -267,10 +279,11 @@ def messages_to_dict(messages: Sequence[BaseMessage]) -> list[dict]:
"""Convert a sequence of Messages to a list of dictionaries.
Args:
messages: Sequence of messages (as BaseMessages) to convert.
messages: Sequence of messages (as ``BaseMessage``s) to convert.
Returns:
List of messages as dicts.
"""
return [message_to_dict(m) for m in messages]
@@ -284,6 +297,7 @@ def get_msg_title_repr(title: str, *, bold: bool = False) -> str:
Returns:
The title representation.
"""
padded = " " + title + " "
sep_len = (80 - len(padded)) // 2

View File

@@ -30,7 +30,10 @@ class ChatMessageChunk(ChatMessage, BaseMessageChunk):
# non-chunk variant.
type: Literal["ChatMessageChunk"] = "ChatMessageChunk" # type: ignore[assignment]
"""The type of the message (used during serialization).
Defaults to "ChatMessageChunk"."""
Defaults to ``'ChatMessageChunk'``.
"""
@override
def __add__(self, other: Any) -> BaseMessageChunk: # type: ignore[override]

View File

@@ -15,19 +15,20 @@ from langchain_core.utils._merge import merge_dicts
class FunctionMessage(BaseMessage):
"""Message for passing the result of executing a tool back to a model.
FunctionMessage are an older version of the ToolMessage schema, and
do not contain the tool_call_id field.
``FunctionMessage`` are an older version of the ``ToolMessage`` schema, and
do not contain the ``tool_call_id`` field.
The tool_call_id field is used to associate the tool call request with the
The ``tool_call_id`` field is used to associate the tool call request with the
tool call response. This is useful in situations where a chat model is able
to request multiple tool calls in parallel.
"""
name: str
"""The name of the function that was executed."""
type: Literal["function"] = "function"
"""The type of the message (used for serialization). Defaults to "function"."""
"""The type of the message (used for serialization). Defaults to ``'function'``."""
class FunctionMessageChunk(FunctionMessage, BaseMessageChunk):
@@ -38,7 +39,10 @@ class FunctionMessageChunk(FunctionMessage, BaseMessageChunk):
# non-chunk variant.
type: Literal["FunctionMessageChunk"] = "FunctionMessageChunk" # type: ignore[assignment]
"""The type of the message (used for serialization).
Defaults to "FunctionMessageChunk"."""
Defaults to ``'FunctionMessageChunk'``.
"""
@override
def __add__(self, other: Any) -> BaseMessageChunk: # type: ignore[override]

View File

@@ -8,7 +8,7 @@ from langchain_core.messages.base import BaseMessage, BaseMessageChunk
class HumanMessage(BaseMessage):
"""Message from a human.
HumanMessages are messages that are passed in from a human to the model.
``HumanMessage``s are messages that are passed in from a human to the model.
Example:
@@ -32,15 +32,22 @@ class HumanMessage(BaseMessage):
At the moment, this is ignored by most models. Usage is discouraged.
Defaults to False.
"""
type: Literal["human"] = "human"
"""The type of the message (used for serialization). Defaults to "human"."""
"""The type of the message (used for serialization).
Defaults to ``'human'``.
"""
def __init__(
self, content: Union[str, list[Union[str, dict]]], **kwargs: Any
self,
content: Union[str, list[Union[str, dict]]],
**kwargs: Any,
) -> None:
"""Pass in content as positional arg.
"""Initialize ``HumanMessage``.
Args:
content: The string contents of the message.

View File

@@ -24,6 +24,7 @@ class RemoveMessage(BaseMessage):
Raises:
ValueError: If the 'content' field is passed in kwargs.
"""
if kwargs.pop("content", None):
msg = "RemoveMessage does not support 'content' field."

View File

@@ -28,7 +28,11 @@ class SystemMessage(BaseMessage):
"""
type: Literal["system"] = "system"
"""The type of the message (used for serialization). Defaults to "system"."""
"""The type of the message (used for serialization).
Defaults to ``'system'``.
"""
def __init__(
self, content: Union[str, list[Union[str, dict]]], **kwargs: Any
@@ -50,4 +54,7 @@ class SystemMessageChunk(SystemMessage, BaseMessageChunk):
# non-chunk variant.
type: Literal["SystemMessageChunk"] = "SystemMessageChunk" # type: ignore[assignment]
"""The type of the message (used for serialization).
Defaults to "SystemMessageChunk"."""
Defaults to ``'SystemMessageChunk'``.
"""

View File

@@ -14,19 +14,20 @@ from langchain_core.utils._merge import merge_dicts, merge_obj
class ToolOutputMixin:
"""Mixin for objects that tools can return directly.
If a custom BaseTool is invoked with a ToolCall and the output of custom code is
not an instance of ToolOutputMixin, the output will automatically be coerced to a
string and wrapped in a ToolMessage.
If a custom BaseTool is invoked with a ``ToolCall`` and the output of custom code is
not an instance of ``ToolOutputMixin``, the output will automatically be coerced to
a string and wrapped in a ``ToolMessage``.
"""
class ToolMessage(BaseMessage, ToolOutputMixin):
"""Message for passing the result of executing a tool back to a model.
ToolMessages contain the result of a tool invocation. Typically, the result
is encoded inside the `content` field.
``ToolMessage``s contain the result of a tool invocation. Typically, the result
is encoded inside the ``content`` field.
Example: A ToolMessage representing a result of 42 from a tool call with id
Example: A ``ToolMessage`` representing a result of ``42`` from a tool call with id
.. code-block:: python
@@ -35,7 +36,7 @@ class ToolMessage(BaseMessage, ToolOutputMixin):
ToolMessage(content="42", tool_call_id="call_Jja7J89XsjrOLA5r!MEOW!SL")
Example: A ToolMessage where only part of the tool output is sent to the model
Example: A ``ToolMessage`` where only part of the tool output is sent to the model
and the full output is passed in to artifact.
.. versionadded:: 0.2.17
@@ -57,7 +58,7 @@ class ToolMessage(BaseMessage, ToolOutputMixin):
tool_call_id="call_Jja7J89XsjrOLA5r!MEOW!SL",
)
The tool_call_id field is used to associate the tool call request with the
The ``tool_call_id`` field is used to associate the tool call request with the
tool call response. This is useful in situations where a chat model is able
to request multiple tool calls in parallel.
@@ -67,7 +68,11 @@ class ToolMessage(BaseMessage, ToolOutputMixin):
"""Tool call that this message is responding to."""
type: Literal["tool"] = "tool"
"""The type of the message (used for serialization). Defaults to "tool"."""
"""The type of the message (used for serialization).
Defaults to ``'tool'``.
"""
artifact: Any = None
"""Artifact of the Tool execution which is not meant to be sent to the model.
@@ -77,12 +82,14 @@ class ToolMessage(BaseMessage, ToolOutputMixin):
output is needed in other parts of the code.
.. versionadded:: 0.2.17
"""
status: Literal["success", "error"] = "success"
"""Status of the tool invocation.
.. versionadded:: 0.2.24
"""
additional_kwargs: dict = Field(default_factory=dict, repr=False)
@@ -97,6 +104,7 @@ class ToolMessage(BaseMessage, ToolOutputMixin):
Args:
values: The model arguments.
"""
content = values["content"]
if isinstance(content, tuple):
@@ -135,9 +143,11 @@ class ToolMessage(BaseMessage, ToolOutputMixin):
return values
def __init__(
self, content: Union[str, list[Union[str, dict]]], **kwargs: Any
self,
content: Union[str, list[Union[str, dict]]],
**kwargs: Any,
) -> None:
"""Create a ToolMessage.
"""Initialize ``ToolMessage``.
Args:
content: The string contents of the message.
@@ -187,8 +197,8 @@ class ToolCall(TypedDict):
{"name": "foo", "args": {"a": 1}, "id": "123"}
This represents a request to call the tool named "foo" with arguments {"a": 1}
and an identifier of "123".
This represents a request to call the tool named ``'foo'`` with arguments
``{"a": 1}`` and an identifier of ``'123'``.
"""
@@ -201,6 +211,7 @@ class ToolCall(TypedDict):
An identifier is needed to associate a tool call request with a tool
call result in events when multiple concurrent tool calls are made.
"""
type: NotRequired[Literal["tool_call"]]
@@ -227,9 +238,9 @@ def tool_call(
class ToolCallChunk(TypedDict):
"""A chunk of a tool call (e.g., as part of a stream).
When merging ToolCallChunks (e.g., via AIMessageChunk.__add__),
When merging ``ToolCallChunk``s (e.g., via ``AIMessageChunk.__add__``),
all string attributes are concatenated. Chunks are only merged if their
values of `index` are equal and not None.
values of ``index`` are equal and not None.
Example:
@@ -282,7 +293,7 @@ def tool_call_chunk(
class InvalidToolCall(TypedDict):
"""Allowance for errors made by LLM.
Here we add an `error` key to surface errors made during generation
Here we add an ``error`` key to surface errors made during generation
(e.g., invalid JSON arguments.)
"""

View File

@@ -5,6 +5,7 @@ Some examples of what you can do with these functions include:
* Convert messages to strings (serialization)
* Convert messages from dicts to Message objects (deserialization)
* Filter messages from a list of messages based on name, type or id etc.
"""
from __future__ import annotations
@@ -91,13 +92,14 @@ AnyMessage = Annotated[
def get_buffer_string(
messages: Sequence[BaseMessage], human_prefix: str = "Human", ai_prefix: str = "AI"
) -> str:
r"""Convert a sequence of Messages to strings and concatenate them into one string.
r"""Convert a sequence of messages to strings and concatenate them into one string.
Args:
messages: Messages to be converted to strings.
human_prefix: The prefix to prepend to contents of HumanMessages.
Default is "Human".
ai_prefix: The prefix to prepend to contents of AIMessages. Default is "AI".
human_prefix: The prefix to prepend to contents of ``HumanMessage``s.
Default is ``'Human'``.
ai_prefix: The prefix to prepend to contents of ``AIMessage``. Default is
``'AI'``.
Returns:
A single string concatenation of all input messages.
@@ -176,19 +178,20 @@ def _message_from_dict(message: dict) -> BaseMessage:
def messages_from_dict(messages: Sequence[dict]) -> list[BaseMessage]:
"""Convert a sequence of messages from dicts to Message objects.
"""Convert a sequence of messages from dicts to ``Message`` objects.
Args:
messages: Sequence of messages (as dicts) to convert.
Returns:
list of messages (BaseMessages).
"""
return [_message_from_dict(m) for m in messages]
def message_chunk_to_message(chunk: BaseMessage) -> BaseMessage:
"""Convert a message chunk to a message.
"""Convert a message chunk to a ``Message``.
Args:
chunk: Message chunk to convert.
@@ -221,10 +224,10 @@ def _create_message_from_message_type(
id: Optional[str] = None,
**additional_kwargs: Any,
) -> BaseMessage:
"""Create a message from a message type and content string.
"""Create a message from a ``Message`` type and content string.
Args:
message_type: (str) the type of the message (e.g., "human", "ai", etc.).
message_type: (str) the type of the message (e.g., ``'human'``, ``'ai'``, etc.).
content: (str) the content string.
name: (str) the name of the message. Default is None.
tool_call_id: (str) the tool call id. Default is None.
@@ -236,8 +239,9 @@ def _create_message_from_message_type(
a message of the appropriate type.
Raises:
ValueError: if the message type is not one of "human", "user", "ai",
"assistant", "function", "tool", "system", or "developer".
ValueError: if the message type is not one of ``'human'``, ``'user'``, ``'ai'``,
``'assistant'``, ``'function'``, ``'tool'``, ``'system'``, or
``'developer'``.
"""
kwargs: dict[str, Any] = {}
if name is not None:
@@ -303,15 +307,15 @@ def _create_message_from_message_type(
def _convert_to_message(message: MessageLikeRepresentation) -> BaseMessage:
"""Instantiate a message from a variety of message formats.
"""Instantiate a ``Message`` from a variety of message formats.
The message format can be one of the following:
- BaseMessagePromptTemplate
- BaseMessage
- 2-tuple of (role string, template); e.g., ("human", "{user_input}")
- ``BaseMessagePromptTemplate``
- ``BaseMessage``
- 2-tuple of (role string, template); e.g., (``'human'``, ``'{user_input}'``)
- dict: a message dict with role and content keys
- string: shorthand for ("human", template); e.g., "{user_input}"
- string: shorthand for (``'human'``, template); e.g., ``'{user_input}'``
Args:
message: a representation of a message in one of the supported formats.
@@ -322,6 +326,7 @@ def _convert_to_message(message: MessageLikeRepresentation) -> BaseMessage:
Raises:
NotImplementedError: if the message type is not supported.
ValueError: if the message dict does not contain the required keys.
"""
if isinstance(message, BaseMessage):
message_ = message
@@ -367,6 +372,7 @@ def convert_to_messages(
Returns:
list of messages (BaseMessages).
"""
# Import here to avoid circular imports
from langchain_core.prompt_values import PromptValue # noqa: PLC0415
@@ -417,36 +423,36 @@ def filter_messages(
exclude_ids: Optional[Sequence[str]] = None,
exclude_tool_calls: Optional[Sequence[str] | bool] = None,
) -> list[BaseMessage]:
"""Filter messages based on name, type or id.
"""Filter messages based on ``name``, ``type`` or ``id``.
Args:
messages: Sequence Message-like objects to filter.
include_names: Message names to include. Default is None.
exclude_names: Messages names to exclude. Default is None.
include_types: Message types to include. Can be specified as string names (e.g.
"system", "human", "ai", ...) or as BaseMessage classes (e.g.
SystemMessage, HumanMessage, AIMessage, ...). Default is None.
exclude_types: Message types to exclude. Can be specified as string names (e.g.
"system", "human", "ai", ...) or as BaseMessage classes (e.g.
SystemMessage, HumanMessage, AIMessage, ...). Default is None.
include_types: Message types to include. Can be specified as string names
(e.g. ``'system'``, ``'human'``, ``'ai'``, ...) or as ``BaseMessage``
classes (e.g. ``SystemMessage``, ``HumanMessage``, ``AIMessage``, ...).
Default is None.
exclude_types: Message types to exclude. Can be specified as string names
(e.g. ``'system'``, ``'human'``, ``'ai'``, ...) or as ``BaseMessage``
classes (e.g. ``SystemMessage``, ``HumanMessage``, ``AIMessage``, ...).
Default is None.
include_ids: Message IDs to include. Default is None.
exclude_ids: Message IDs to exclude. Default is None.
exclude_tool_calls: Tool call IDs to exclude. Default is None.
Can be one of the following:
- ``True``: Each ``AIMessages`` with tool calls and all ``ToolMessages``
will be excluded.
- ``True``: all ``AIMessage``s with tool calls and all
``ToolMessage``s will be excluded.
- a sequence of tool call IDs to exclude:
- ToolMessages with the corresponding tool call ID will be excluded.
- The ``tool_calls`` in the AIMessage will be updated to exclude matching
tool calls.
If all tool_calls are filtered from an AIMessage,
the whole message is excluded.
- ``ToolMessage``s with the corresponding tool call ID will be
excluded.
- The ``tool_calls`` in the AIMessage will be updated to exclude
matching tool calls. If all ``tool_calls`` are filtered from an
AIMessage, the whole message is excluded.
Returns:
A list of Messages that meets at least one of the incl_* conditions and none
of the excl_* conditions. If not incl_* conditions are specified then
A list of Messages that meets at least one of the ``incl_*`` conditions and none
of the ``excl_*`` conditions. If not ``incl_*`` conditions are specified then
anything that is not explicitly excluded will be included.
Raises:
@@ -558,13 +564,14 @@ def merge_message_runs(
) -> list[BaseMessage]:
r"""Merge consecutive Messages of the same type.
**NOTE**: ToolMessages are not merged, as each has a distinct tool call id that
can't be merged.
.. note::
ToolMessages are not merged, as each has a distinct tool call id that can't be
merged.
Args:
messages: Sequence Message-like objects to merge.
chunk_separator: Specify the string to be inserted between message chunks.
Default is "\n".
Default is ``'\n'``.
Returns:
list of BaseMessages with consecutive runs of message types merged into single
@@ -705,8 +712,8 @@ def trim_messages(
) -> list[BaseMessage]:
r"""Trim messages to be below a token count.
trim_messages can be used to reduce the size of a chat history to a specified token
count or specified message count.
``trim_messages`` can be used to reduce the size of a chat history to a specified
token count or specified message count.
In either case, if passing the trimmed chat history back into a chat model
directly, the resulting chat history should usually satisfy the following
@@ -714,13 +721,13 @@ def trim_messages(
1. The resulting chat history should be valid. Most chat models expect that chat
history starts with either (1) a ``HumanMessage`` or (2) a ``SystemMessage``
followed by a ``HumanMessage``. To achieve this, set ``start_on="human"``.
followed by a ``HumanMessage``. To achieve this, set ``start_on='human'``.
In addition, generally a ``ToolMessage`` can only appear after an ``AIMessage``
that involved a tool call.
Please see the following link for more information about messages:
https://python.langchain.com/docs/concepts/#messages
2. It includes recent messages and drops old messages in the chat history.
To achieve this set the ``strategy="last"``.
To achieve this set the ``strategy='last'``.
3. Usually, the new chat history should include the ``SystemMessage`` if it
was present in the original chat history since the ``SystemMessage`` includes
special instructions to the chat model. The ``SystemMessage`` is almost always
@@ -734,67 +741,67 @@ def trim_messages(
Args:
messages: Sequence of Message-like objects to trim.
max_tokens: Max token count of trimmed messages.
token_counter: Function or llm for counting tokens in a BaseMessage or a list of
BaseMessage. If a BaseLanguageModel is passed in then
BaseLanguageModel.get_num_tokens_from_messages() will be used.
Set to `len` to count the number of **messages** in the chat history.
token_counter: Function or llm for counting tokens in a ``BaseMessage`` or a
list of ``BaseMessage``. If a ``BaseLanguageModel`` is passed in then
``BaseLanguageModel.get_num_tokens_from_messages()`` will be used.
Set to ``len`` to count the number of **messages** in the chat history.
.. note::
Use `count_tokens_approximately` to get fast, approximate token counts.
This is recommended for using `trim_messages` on the hot path, where
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.
strategy: Strategy for trimming.
- "first": Keep the first <= n_count tokens of the messages.
- "last": Keep the last <= n_count tokens of the messages.
- ``'first'``: Keep the first ``<= n_count`` tokens of the messages.
- ``'last'``: Keep the last ``<= n_count`` tokens of the messages.
Default is ``'last'``.
allow_partial: Whether to split a message if only part of the message can be
included. If ``strategy="last"`` then the last partial contents of a message
are included. If ``strategy="first"`` then the first partial contents of a
included. If ``strategy='last'`` then the last partial contents of a message
are included. If ``strategy='first'`` then the first partial contents of a
message are included.
Default is False.
end_on: The message type to end on. If specified then every message after the
last occurrence of this type is ignored. If ``strategy=="last"`` then this
last occurrence of this type is ignored. If ``strategy='last'`` then this
is done before we attempt to get the last ``max_tokens``. If
``strategy=="first"`` then this is done after we get the first
``max_tokens``. Can be specified as string names (e.g. "system", "human",
"ai", ...) or as BaseMessage classes (e.g. SystemMessage, HumanMessage,
AIMessage, ...). Can be a single type or a list of types.
``strategy='first'`` then this is done after we get the first
``max_tokens``. Can be specified as string names (e.g. ``'system'``,
``'human'``, ``'ai'``, ...) or as ``BaseMessage`` classes (e.g.
``SystemMessage``, ``HumanMessage``, ``AIMessage``, ...). Can be a single
type or a list of types.
Default is None.
start_on: The message type to start on. Should only be specified if
``strategy="last"``. If specified then every message before
``strategy='last'``. If specified then every message before
the first occurrence of this type is ignored. This is done after we trim
the initial messages to the last ``max_tokens``. Does not
apply to a SystemMessage at index 0 if ``include_system=True``. Can be
specified as string names (e.g. "system", "human", "ai", ...) or as
BaseMessage classes (e.g. SystemMessage, HumanMessage, AIMessage, ...). Can
be a single type or a list of types.
apply to a ``SystemMessage`` at index 0 if ``include_system=True``. Can be
specified as string names (e.g. ``'system'``, ``'human'``, ``'ai'``, ...) or
as ``BaseMessage`` classes (e.g. ``SystemMessage``, ``HumanMessage``,
``AIMessage``, ...). Can be a single type or a list of types.
Default is None.
include_system: Whether to keep the SystemMessage if there is one at index 0.
Should only be specified if ``strategy="last"``.
Default is False.
text_splitter: Function or ``langchain_text_splitters.TextSplitter`` for
splitting the string contents of a message. Only used if
``allow_partial=True``. If ``strategy="last"`` then the last split tokens
from a partial message will be included. if ``strategy=="first"`` then the
``allow_partial=True``. If ``strategy='last'`` then the last split tokens
from a partial message will be included. if ``strategy='first'`` then the
first split tokens from a partial message will be included. Token splitter
assumes that separators are kept, so that split contents can be directly
concatenated to recreate the original text. Defaults to splitting on
newlines.
Returns:
list of trimmed BaseMessages.
list of trimmed ``BaseMessage``.
Raises:
ValueError: if two incompatible arguments are specified or an unrecognized
``strategy`` is specified.
Example:
Trim chat history based on token count, keeping the SystemMessage if
present, and ensuring that the chat history starts with a HumanMessage (
or a SystemMessage followed by a HumanMessage).
Trim chat history based on token count, keeping the ``SystemMessage`` if
present, and ensuring that the chat history starts with a ``HumanMessage`` (
or a ``SystemMessage`` followed by a ``HumanMessage``).
.. code-block:: python
@@ -849,9 +856,9 @@ def trim_messages(
HumanMessage(content="what do you call a speechless parrot"),
]
Trim chat history based on the message count, keeping the SystemMessage if
present, and ensuring that the chat history starts with a HumanMessage (
or a SystemMessage followed by a HumanMessage).
Trim chat history based on the message count, keeping the ``SystemMessage`` if
present, and ensuring that the chat history starts with a ``HumanMessage`` (
or a ``SystemMessage`` followed by a ``HumanMessage``).
trim_messages(
messages,
@@ -1040,17 +1047,16 @@ def convert_to_openai_messages(
messages: Message-like object or iterable of objects whose contents are
in OpenAI, Anthropic, Bedrock Converse, or VertexAI formats.
text_format: How to format string or text block contents:
- ``'string'``:
If a message has a string content, this is left as a string. If
a message has content blocks that are all of type 'text', these are
joined with a newline to make a single string. If a message has
content blocks and at least one isn't of type 'text', then
all blocks are left as dicts.
- ``'block'``:
If a message has a string content, this is turned into a list
with a single content block of type 'text'. If a message has content
blocks these are left as is.
- ``'string'``:
If a message has a string content, this is left as a string. If
a message has content blocks that are all of type ``'text'``, these
are joined with a newline to make a single string. If a message has
content blocks and at least one isn't of type ``'text'``, then
all blocks are left as dicts.
- ``'block'``:
If a message has a string content, this is turned into a list
with a single content block of type ``'text'``. If a message has
content blocks these are left as is.
Raises:
ValueError: if an unrecognized ``text_format`` is specified, or if a message
@@ -1379,7 +1385,7 @@ def convert_to_openai_messages(
},
}
)
elif block.get("type") == "thinking":
elif block.get("type") in ["thinking", "reasoning"]:
content.append(block)
else:
err = (

View File

@@ -15,14 +15,14 @@ from langchain_core.utils._merge import merge_dicts
class ChatGeneration(Generation):
"""A single chat generation output.
A subclass of Generation that represents the response from a chat model
A subclass of ``Generation`` that represents the response from a chat model
that generates chat messages.
The `message` attribute is a structured representation of the chat message.
Most of the time, the message will be of type `AIMessage`.
The ``message`` attribute is a structured representation of the chat message.
Most of the time, the message will be of type ``AIMessage``.
Users working with chat models will usually access information via either
`AIMessage` (returned from runnable interfaces) or `LLMResult` (available
``AIMessage`` (returned from runnable interfaces) or ``LLMResult`` (available
via callbacks).
"""
@@ -31,6 +31,7 @@ class ChatGeneration(Generation):
.. warning::
SHOULD NOT BE SET DIRECTLY!
"""
message: BaseMessage
"""The message output by the chat model."""
@@ -47,6 +48,9 @@ class ChatGeneration(Generation):
Returns:
The values of the object with the text attribute set.
Raises:
ValueError: If the message is not a string or a list.
"""
text = ""
if isinstance(self.message.content, str):
@@ -66,9 +70,9 @@ class ChatGeneration(Generation):
class ChatGenerationChunk(ChatGeneration):
"""ChatGeneration chunk.
"""``ChatGeneration`` chunk.
ChatGeneration chunks can be concatenated with other ChatGeneration chunks.
``ChatGeneration`` chunks can be concatenated with other ``ChatGeneration`` chunks.
"""
message: BaseMessageChunk

View File

@@ -113,8 +113,12 @@ class ImageURL(TypedDict, total=False):
"""Image URL."""
detail: Literal["auto", "low", "high"]
"""Specifies the detail level of the image. Defaults to "auto".
Can be "auto", "low", or "high"."""
"""Specifies the detail level of the image. Defaults to ``'auto'``.
Can be ``'auto'``, ``'low'``, or ``'high'``.
This follows OpenAI's Chat Completion API's image URL format.
"""
url: str
"""Either a URL of the image or the base64 encoded image data."""

View File

@@ -262,7 +262,7 @@ class BaseStringMessagePromptTemplate(BaseMessagePromptTemplate, ABC):
def from_template_file(
cls,
template_file: Union[str, Path],
input_variables: list[str],
input_variables: list[str], # noqa: ARG003 # Deprecated
**kwargs: Any,
) -> Self:
"""Create a class from a template file.
@@ -275,7 +275,7 @@ class BaseStringMessagePromptTemplate(BaseMessagePromptTemplate, ABC):
Returns:
A new instance of this class.
"""
prompt = PromptTemplate.from_file(template_file, input_variables)
prompt = PromptTemplate.from_file(template_file)
return cls(prompt=prompt, **kwargs)
@abstractmethod
@@ -813,7 +813,10 @@ class ChatPromptTemplate(BaseChatPromptTemplate):
)
prompt_value = template.invoke(
{"name": "Bob", "user_input": "What is your name?"}
{
"name": "Bob",
"user_input": "What is your name?",
}
)
# Output:
# ChatPromptValue(

View File

@@ -281,7 +281,10 @@ class FewShotChatMessagePromptTemplate(
]
example_prompt = ChatPromptTemplate.from_messages(
[("human", "What is {input}?"), ("ai", "{output}")]
[
("human", "What is {input}?"),
("ai", "{output}"),
]
)
few_shot_prompt = FewShotChatMessagePromptTemplate(

View File

@@ -1397,7 +1397,10 @@ class Runnable(ABC, Generic[Input, Output]):
.. code-block:: python
template = ChatPromptTemplate.from_messages(
[("system", "You are Cat Agent 007"), ("human", "{question}")]
[
("system", "You are Cat Agent 007"),
("human", "{question}"),
]
).with_config({"run_name": "my_template", "tags": ["my_template"]})

View File

@@ -269,7 +269,7 @@ def draw_ascii(vertices: Mapping[str, str], edges: Sequence[LangEdge]) -> str:
print(draw_ascii(vertices, edges))
.. code-block:: none
.. code-block::
+---+
| 1 |

View File

@@ -1324,8 +1324,7 @@ def get_all_basemodel_annotations(
"""
# cls has no subscript: cls = FooBar
if isinstance(cls, type):
# Gather pydantic field objects (v2: model_fields / v1: __fields__)
fields = getattr(cls, "model_fields", {}) or getattr(cls, "__fields__", {})
fields = get_fields(cls)
alias_map = {field.alias: name for name, field in fields.items() if field.alias}
annotations: dict[str, Union[type, TypeVar]] = {}
@@ -1402,7 +1401,7 @@ def _replace_type_vars(
if type_ in generic_map:
return generic_map[type_]
if default_to_bound:
return type_.__bound__ or Any
return type_.__bound__ if type_.__bound__ is not None else Any
return type_
if (origin := get_origin(type_)) and (args := get_args(type_)):
new_args = tuple(

View File

@@ -227,7 +227,7 @@ def tool(
\"\"\"
return bar
""" # noqa: D214, D410, D411
""" # noqa: D214, D410, D411 # We're intentionally showing bad formatting in examples
def _create_tool_factory(
tool_name: str,

View File

@@ -165,7 +165,7 @@ class Tee(Generic[T]):
A ``tee`` works lazily and can handle an infinite ``iterable``, provided
that all iterators advance.
.. code-block:: python3
.. code-block:: python
async def derivative(sensor_data):
previous, current = a.tee(sensor_data, n=2)

View File

@@ -667,14 +667,13 @@ def tool_example_to_messages(
The ``ToolMessage`` is required because some chat models are hyper-optimized for
agents rather than for an extraction use case.
Arguments:
input: string, the user input
tool_calls: list[BaseModel], a list of tool calls represented as Pydantic
BaseModels
tool_outputs: Optional[list[str]], a list of tool call outputs.
Args:
input: The user input
tool_calls: Tool calls represented as Pydantic BaseModels
tool_outputs: Tool call outputs.
Does not need to be provided. If not provided, a placeholder value
will be inserted. Defaults to None.
ai_response: Optional[str], if provided, content for a final ``AIMessage``.
ai_response: If provided, content for a final ``AIMessage``.
Returns:
A list of messages

View File

@@ -102,7 +102,7 @@ class Tee(Generic[T]):
A ``tee`` works lazily and can handle an infinite ``iterable``, provided
that all iterators advance.
.. code-block:: python3
.. code-block:: python
async def derivative(sensor_data):
previous, current = a.tee(sensor_data, n=2)

View File

@@ -94,7 +94,7 @@ class InMemoryVectorStore(VectorStore):
for doc in results:
print(f"* {doc.page_content} [{doc.metadata}]")
.. code-block:: none
.. code-block::
* thud [{'bar': 'baz'}]
@@ -111,7 +111,7 @@ class InMemoryVectorStore(VectorStore):
for doc in results:
print(f"* {doc.page_content} [{doc.metadata}]")
.. code-block:: none
.. code-block::
* thud [{'bar': 'baz'}]
@@ -123,7 +123,7 @@ class InMemoryVectorStore(VectorStore):
for doc, score in results:
print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]")
.. code-block:: none
.. code-block::
* [SIM=0.832268] foo [{'baz': 'bar'}]
@@ -144,7 +144,7 @@ class InMemoryVectorStore(VectorStore):
for doc, score in results:
print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]")
.. code-block:: none
.. code-block::
* [SIM=0.832268] foo [{'baz': 'bar'}]
@@ -157,7 +157,7 @@ class InMemoryVectorStore(VectorStore):
)
retriever.invoke("thud")
.. code-block:: none
.. code-block::
[Document(id='2', metadata={'bar': 'baz'}, page_content='thud')]

View File

@@ -5,15 +5,15 @@ build-backend = "pdm.backend"
[project]
authors = []
license = {text = "MIT"}
requires-python = ">=3.9"
requires-python = ">=3.9.0,<4.0.0"
dependencies = [
"langsmith>=0.3.45",
"tenacity!=8.4.0,<10.0.0,>=8.1.0",
"jsonpatch<2.0,>=1.33",
"PyYAML>=5.3",
"typing-extensions>=4.7",
"packaging>=23.2",
"pydantic>=2.7.4",
"langsmith>=0.3.45,<1.0.0",
"tenacity!=8.4.0,>=8.1.0,<10.0.0",
"jsonpatch>=1.33.0,<2.0.0",
"PyYAML>=5.3.0,<7.0.0",
"typing-extensions>=4.7.0,<5.0.0",
"packaging>=23.2.0,<26.0.0",
"pydantic>=2.7.4,<3.0.0",
]
name = "langchain-core"
version = "0.3.76"
@@ -26,30 +26,30 @@ readme = "README.md"
repository = "https://github.com/langchain-ai/langchain"
[dependency-groups]
lint = ["ruff<0.13,>=0.12.2"]
lint = ["ruff>=0.13.1,<0.14.0"]
typing = [
"mypy<1.19,>=1.18.1",
"types-pyyaml<7.0.0.0,>=6.0.12.2",
"types-requests<3.0.0.0,>=2.28.11.5",
"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-text-splitters",
]
dev = [
"jupyter<2.0.0,>=1.0.0",
"setuptools<68.0.0,>=67.6.1",
"grandalf<1.0,>=0.8",
"jupyter>=1.0.0,<2.0.0",
"setuptools>=67.6.1,<68.0.0",
"grandalf>=0.8.0,<1.0.0",
]
test = [
"pytest<9,>=8",
"freezegun<2.0.0,>=1.2.2",
"pytest-mock<4.0.0,>=3.10.0",
"syrupy<5.0.0,>=4.0.2",
"pytest-watcher<1.0.0,>=0.3.4",
"pytest-asyncio<1.0.0,>=0.21.1",
"grandalf<1.0,>=0.8",
"responses<1.0.0,>=0.25.0",
"pytest-socket<1.0.0,>=0.7.0",
"pytest>=8.0.0,<9.0.0",
"freezegun>=1.2.2,<2.0.0",
"pytest-mock>=3.10.0,<4.0.0",
"syrupy>=4.0.2,<5.0.0",
"pytest-watcher>=0.3.4,<1.0.0",
"pytest-asyncio>=0.21.1,<1.0.0",
"grandalf>=0.8.0,<1.0.0",
"responses>=0.25.0,<1.0.0",
"pytest-socket>=0.7.0,<1.0.0",
"pytest-xdist<4.0.0,>=3.6.1",
"blockbuster~=1.5.18",
"blockbuster>=1.5.18,<1.6.0",
"numpy>=1.26.4; python_version<'3.13'",
"numpy>=2.1.0; python_version>='3.13'",
"langchain-tests",
@@ -99,7 +99,6 @@ ignore = [
# TODO rules
"ANN401", # No Any types
"BLE", # Blind exceptions
"DOC", # Docstrings (preview)
"ERA", # No commented-out code
"PLR2004", # Comparison to magic number
]
@@ -113,10 +112,12 @@ flake8-annotations.mypy-init-return = true
flake8-builtins.ignorelist = ["id", "input", "type"]
flake8-type-checking.runtime-evaluated-base-classes = ["pydantic.BaseModel","langchain_core.load.serializable.Serializable","langchain_core.runnables.base.RunnableSerializable"]
pep8-naming.classmethod-decorators = [ "classmethod", "langchain_core.utils.pydantic.pre_init", "pydantic.field_validator", "pydantic.v1.root_validator",]
pydocstyle.convention = "google"
pydocstyle.ignore-var-parameters = true
pyupgrade.keep-runtime-typing = true
[tool.ruff.lint.pydocstyle]
convention = "google"
ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters
[tool.ruff.lint.per-file-ignores]
"langchain_core/utils/mustache.py" = [ "PLW0603",]
"tests/unit_tests/test_tools.py" = [ "ARG",]

View File

@@ -86,7 +86,7 @@ def test_indexing_same_content(
]
)
assert index(loader, record_manager, vector_store) == {
assert index(loader, record_manager, vector_store, key_encoder="sha256") == {
"num_added": 2,
"num_deleted": 0,
"num_skipped": 0,
@@ -97,7 +97,7 @@ def test_indexing_same_content(
for _ in range(2):
# Run the indexing again
assert index(loader, record_manager, vector_store) == {
assert index(loader, record_manager, vector_store, key_encoder="sha256") == {
"num_added": 0,
"num_deleted": 0,
"num_skipped": 2,
@@ -120,7 +120,12 @@ async def test_aindexing_same_content(
]
)
assert await aindex(loader, arecord_manager, vector_store) == {
assert await aindex(
loader,
arecord_manager,
vector_store,
key_encoder="sha256",
) == {
"num_added": 2,
"num_deleted": 0,
"num_skipped": 0,
@@ -131,7 +136,12 @@ async def test_aindexing_same_content(
for _ in range(2):
# Run the indexing again
assert await aindex(loader, arecord_manager, vector_store) == {
assert await aindex(
loader,
arecord_manager,
vector_store,
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
"num_skipped": 2,
@@ -159,7 +169,13 @@ def test_index_simple_delete_full(
"get_time",
return_value=datetime(2021, 1, 1, tzinfo=timezone.utc).timestamp(),
):
assert index(loader, record_manager, vector_store, cleanup="full") == {
assert index(
loader,
record_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 2,
"num_deleted": 0,
"num_skipped": 0,
@@ -171,7 +187,13 @@ def test_index_simple_delete_full(
"get_time",
return_value=datetime(2021, 1, 1, tzinfo=timezone.utc).timestamp(),
):
assert index(loader, record_manager, vector_store, cleanup="full") == {
assert index(
loader,
record_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
"num_skipped": 2,
@@ -194,7 +216,13 @@ def test_index_simple_delete_full(
"get_time",
return_value=datetime(2021, 1, 2, tzinfo=timezone.utc).timestamp(),
):
indexing_result = index(loader, record_manager, vector_store, cleanup="full")
indexing_result = index(
loader,
record_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
)
doc_texts = {
# Ignoring type since doc should be in the store and not a None
@@ -216,7 +244,13 @@ def test_index_simple_delete_full(
"get_time",
return_value=datetime(2021, 1, 2, tzinfo=timezone.utc).timestamp(),
):
assert index(loader, record_manager, vector_store, cleanup="full") == {
assert index(
loader,
record_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
"num_skipped": 2,
@@ -244,7 +278,13 @@ async def test_aindex_simple_delete_full(
"get_time",
return_value=datetime(2021, 1, 1, tzinfo=timezone.utc).timestamp(),
):
assert await aindex(loader, arecord_manager, vector_store, cleanup="full") == {
assert await aindex(
loader,
arecord_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 2,
"num_deleted": 0,
"num_skipped": 0,
@@ -256,7 +296,13 @@ async def test_aindex_simple_delete_full(
"get_time",
return_value=datetime(2021, 1, 1, tzinfo=timezone.utc).timestamp(),
):
assert await aindex(loader, arecord_manager, vector_store, cleanup="full") == {
assert await aindex(
loader,
arecord_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
"num_skipped": 2,
@@ -279,7 +325,13 @@ async def test_aindex_simple_delete_full(
"get_time",
return_value=datetime(2021, 1, 2, tzinfo=timezone.utc).timestamp(),
):
assert await aindex(loader, arecord_manager, vector_store, cleanup="full") == {
assert await aindex(
loader,
arecord_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 1,
"num_deleted": 1,
"num_skipped": 1,
@@ -299,7 +351,13 @@ async def test_aindex_simple_delete_full(
"get_time",
return_value=datetime(2021, 1, 2, tzinfo=timezone.utc).timestamp(),
):
assert await aindex(loader, arecord_manager, vector_store, cleanup="full") == {
assert await aindex(
loader,
arecord_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
"num_skipped": 2,
@@ -327,7 +385,13 @@ def test_index_delete_full_recovery_after_deletion_failure(
"get_time",
return_value=datetime(2021, 1, 1, tzinfo=timezone.utc).timestamp(),
):
assert index(loader, record_manager, vector_store, cleanup="full") == {
assert index(
loader,
record_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 2,
"num_deleted": 0,
"num_skipped": 0,
@@ -354,7 +418,13 @@ def test_index_delete_full_recovery_after_deletion_failure(
patch.object(vector_store, "delete", return_value=False),
pytest.raises(IndexingException),
):
indexing_result = index(loader, record_manager, vector_store, cleanup="full")
indexing_result = index(
loader,
record_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
)
# At this point, there should be 3 records in both the record manager
# and the vector store
@@ -374,7 +444,13 @@ def test_index_delete_full_recovery_after_deletion_failure(
"get_time",
return_value=datetime(2021, 1, 3, tzinfo=timezone.utc).timestamp(),
):
indexing_result = index(loader, record_manager, vector_store, cleanup="full")
indexing_result = index(
loader,
record_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
)
doc_texts = {
# Ignoring type since doc should be in the store and not a None
vector_store.get_by_ids([uid])[0].page_content
@@ -410,7 +486,13 @@ async def test_aindex_delete_full_recovery_after_deletion_failure(
"get_time",
return_value=datetime(2021, 1, 1, tzinfo=timezone.utc).timestamp(),
):
assert await aindex(loader, arecord_manager, vector_store, cleanup="full") == {
assert await aindex(
loader,
arecord_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 2,
"num_deleted": 0,
"num_skipped": 0,
@@ -438,7 +520,11 @@ async def test_aindex_delete_full_recovery_after_deletion_failure(
pytest.raises(IndexingException),
):
indexing_result = await aindex(
loader, arecord_manager, vector_store, cleanup="full"
loader,
arecord_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
)
# At this point, there should be 3 records in both the record manager
@@ -460,7 +546,11 @@ async def test_aindex_delete_full_recovery_after_deletion_failure(
return_value=datetime(2021, 1, 3, tzinfo=timezone.utc).timestamp(),
):
indexing_result = await aindex(
loader, arecord_manager, vector_store, cleanup="full"
loader,
arecord_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
)
doc_texts = {
# Ignoring type since doc should be in the store and not a None
@@ -504,7 +594,13 @@ def test_incremental_fails_with_bad_source_ids(
"incremental or scoped_full",
):
# Should raise an error because no source id function was specified
index(loader, record_manager, vector_store, cleanup="incremental")
index(
loader,
record_manager,
vector_store,
cleanup="incremental",
key_encoder="sha256",
)
with pytest.raises(
ValueError,
@@ -517,6 +613,7 @@ def test_incremental_fails_with_bad_source_ids(
vector_store,
cleanup="incremental",
source_id_key="source",
key_encoder="sha256",
)
@@ -552,6 +649,7 @@ async def test_aincremental_fails_with_bad_source_ids(
arecord_manager,
vector_store,
cleanup="incremental",
key_encoder="sha256",
)
with pytest.raises(
@@ -565,6 +663,7 @@ async def test_aincremental_fails_with_bad_source_ids(
vector_store,
cleanup="incremental",
source_id_key="source",
key_encoder="sha256",
)
@@ -604,6 +703,7 @@ def test_index_simple_delete_scoped_full(
vector_store,
cleanup="scoped_full",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 4,
"num_deleted": 0,
@@ -622,6 +722,7 @@ def test_index_simple_delete_scoped_full(
vector_store,
cleanup="scoped_full",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
@@ -653,6 +754,7 @@ def test_index_simple_delete_scoped_full(
vector_store,
cleanup="scoped_full",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 1,
"num_deleted": 2,
@@ -682,6 +784,7 @@ def test_index_simple_delete_scoped_full(
vector_store,
cleanup="scoped_full",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
@@ -726,6 +829,7 @@ async def test_aindex_simple_delete_scoped_full(
vector_store,
cleanup="scoped_full",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 4,
"num_deleted": 0,
@@ -744,6 +848,7 @@ async def test_aindex_simple_delete_scoped_full(
vector_store,
cleanup="scoped_full",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
@@ -775,6 +880,7 @@ async def test_aindex_simple_delete_scoped_full(
vector_store,
cleanup="scoped_full",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 1,
"num_deleted": 2,
@@ -804,6 +910,7 @@ async def test_aindex_simple_delete_scoped_full(
vector_store,
cleanup="scoped_full",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
@@ -839,7 +946,13 @@ def test_scoped_full_fails_with_bad_source_ids(
"is incremental or scoped_full",
):
# Should raise an error because no source id function was specified
index(loader, record_manager, vector_store, cleanup="scoped_full")
index(
loader,
record_manager,
vector_store,
cleanup="scoped_full",
key_encoder="sha256",
)
with pytest.raises(
ValueError,
@@ -852,6 +965,7 @@ def test_scoped_full_fails_with_bad_source_ids(
vector_store,
cleanup="scoped_full",
source_id_key="source",
key_encoder="sha256",
)
@@ -882,7 +996,13 @@ async def test_ascoped_full_fails_with_bad_source_ids(
"is incremental or scoped_full",
):
# Should raise an error because no source id function was specified
await aindex(loader, arecord_manager, vector_store, cleanup="scoped_full")
await aindex(
loader,
arecord_manager,
vector_store,
cleanup="scoped_full",
key_encoder="sha256",
)
with pytest.raises(
ValueError,
@@ -895,6 +1015,7 @@ async def test_ascoped_full_fails_with_bad_source_ids(
vector_store,
cleanup="scoped_full",
source_id_key="source",
key_encoder="sha256",
)
@@ -934,6 +1055,7 @@ def test_index_empty_doc_scoped_full(
vector_store,
cleanup="scoped_full",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 4,
"num_deleted": 0,
@@ -952,6 +1074,7 @@ def test_index_empty_doc_scoped_full(
vector_store,
cleanup="scoped_full",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
@@ -972,6 +1095,7 @@ def test_index_empty_doc_scoped_full(
vector_store,
cleanup="scoped_full",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
@@ -1016,6 +1140,7 @@ async def test_aindex_empty_doc_scoped_full(
vector_store,
cleanup="scoped_full",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 4,
"num_deleted": 0,
@@ -1034,6 +1159,7 @@ async def test_aindex_empty_doc_scoped_full(
vector_store,
cleanup="scoped_full",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
@@ -1054,6 +1180,7 @@ async def test_aindex_empty_doc_scoped_full(
vector_store,
cleanup="scoped_full",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
@@ -1090,6 +1217,7 @@ def test_no_delete(
vector_store,
cleanup=None,
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 2,
"num_deleted": 0,
@@ -1109,6 +1237,7 @@ def test_no_delete(
vector_store,
cleanup=None,
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
@@ -1141,6 +1270,7 @@ def test_no_delete(
vector_store,
cleanup=None,
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 1,
"num_deleted": 0,
@@ -1177,6 +1307,7 @@ async def test_ano_delete(
vector_store,
cleanup=None,
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 2,
"num_deleted": 0,
@@ -1196,6 +1327,7 @@ async def test_ano_delete(
vector_store,
cleanup=None,
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
@@ -1228,6 +1360,7 @@ async def test_ano_delete(
vector_store,
cleanup=None,
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 1,
"num_deleted": 0,
@@ -1264,6 +1397,7 @@ def test_incremental_delete(
vector_store,
cleanup="incremental",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 2,
"num_deleted": 0,
@@ -1290,6 +1424,7 @@ def test_incremental_delete(
vector_store,
cleanup="incremental",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
@@ -1327,6 +1462,7 @@ def test_incremental_delete(
vector_store,
cleanup="incremental",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 2,
"num_deleted": 1,
@@ -1374,6 +1510,7 @@ def test_incremental_delete_with_same_source(
vector_store,
cleanup="incremental",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 2,
"num_deleted": 0,
@@ -1409,6 +1546,7 @@ def test_incremental_delete_with_same_source(
vector_store,
cleanup="incremental",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 1,
@@ -1463,6 +1601,7 @@ def test_incremental_indexing_with_batch_size(
cleanup="incremental",
source_id_key="source",
batch_size=2,
key_encoder="sha256",
) == {
"num_added": 4,
"num_deleted": 0,
@@ -1489,6 +1628,7 @@ def test_incremental_indexing_with_batch_size(
cleanup="incremental",
source_id_key="source",
batch_size=2,
key_encoder="sha256",
) == {
"num_added": 2,
"num_deleted": 2,
@@ -1541,6 +1681,7 @@ def test_incremental_delete_with_batch_size(
cleanup="incremental",
source_id_key="source",
batch_size=3,
key_encoder="sha256",
) == {
"num_added": 4,
"num_deleted": 0,
@@ -1568,6 +1709,7 @@ def test_incremental_delete_with_batch_size(
cleanup="incremental",
source_id_key="source",
batch_size=3,
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
@@ -1606,6 +1748,7 @@ def test_incremental_delete_with_batch_size(
cleanup="incremental",
source_id_key="source",
batch_size=1,
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
@@ -1644,6 +1787,7 @@ def test_incremental_delete_with_batch_size(
cleanup="incremental",
source_id_key="source",
batch_size=1,
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
@@ -1681,6 +1825,7 @@ def test_incremental_delete_with_batch_size(
vector_store,
cleanup="incremental",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 2,
"num_deleted": 2,
@@ -1724,6 +1869,7 @@ async def test_aincremental_delete(
vector_store,
cleanup="incremental",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 2,
"num_deleted": 0,
@@ -1750,6 +1896,7 @@ async def test_aincremental_delete(
vector_store,
cleanup="incremental",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
@@ -1787,6 +1934,7 @@ async def test_aincremental_delete(
vector_store,
cleanup="incremental",
source_id_key="source",
key_encoder="sha256",
) == {
"num_added": 2,
"num_deleted": 1,
@@ -1812,7 +1960,13 @@ def test_indexing_with_no_docs(
"""Check edge case when loader returns no new docs."""
loader = ToyLoader(documents=[])
assert index(loader, record_manager, vector_store, cleanup="full") == {
assert index(
loader,
record_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
"num_skipped": 0,
@@ -1826,7 +1980,13 @@ async def test_aindexing_with_no_docs(
"""Check edge case when loader returns no new docs."""
loader = ToyLoader(documents=[])
assert await aindex(loader, arecord_manager, vector_store, cleanup="full") == {
assert await aindex(
loader,
arecord_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
"num_skipped": 0,
@@ -1850,7 +2010,13 @@ def test_deduplication(
]
# Should result in only a single document being added
assert index(docs, record_manager, vector_store, cleanup="full") == {
assert index(
docs,
record_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 1,
"num_deleted": 0,
"num_skipped": 1,
@@ -1874,7 +2040,13 @@ async def test_adeduplication(
]
# Should result in only a single document being added
assert await aindex(docs, arecord_manager, vector_store, cleanup="full") == {
assert await aindex(
docs,
arecord_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 1,
"num_deleted": 0,
"num_skipped": 1,
@@ -1917,6 +2089,7 @@ def test_within_batch_deduplication_counting(
vector_store,
batch_size=10, # All docs in one batch
cleanup="full",
key_encoder="sha256",
)
# Should have 3 unique documents added
@@ -1972,6 +2145,7 @@ async def test_awithin_batch_deduplication_counting(
vector_store,
batch_size=10, # All docs in one batch
cleanup="full",
key_encoder="sha256",
)
# Should have 3 unique documents added
@@ -2004,7 +2178,13 @@ def test_full_cleanup_with_different_batchsize(
for d in range(1000)
]
assert index(docs, record_manager, vector_store, cleanup="full") == {
assert index(
docs,
record_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 1000,
"num_deleted": 0,
"num_skipped": 0,
@@ -2020,7 +2200,12 @@ def test_full_cleanup_with_different_batchsize(
]
assert index(
docs, record_manager, vector_store, cleanup="full", cleanup_batch_size=17
docs,
record_manager,
vector_store,
cleanup="full",
cleanup_batch_size=17,
key_encoder="sha256",
) == {
"num_added": 1001,
"num_deleted": 1000,
@@ -2047,6 +2232,7 @@ def test_incremental_cleanup_with_different_batchsize(
vector_store,
source_id_key="source",
cleanup="incremental",
key_encoder="sha256",
) == {
"num_added": 1000,
"num_deleted": 0,
@@ -2069,6 +2255,7 @@ def test_incremental_cleanup_with_different_batchsize(
source_id_key="source",
cleanup="incremental",
cleanup_batch_size=17,
key_encoder="sha256",
) == {
"num_added": 1001,
"num_deleted": 1000,
@@ -2089,7 +2276,13 @@ async def test_afull_cleanup_with_different_batchsize(
for d in range(1000)
]
assert await aindex(docs, arecord_manager, vector_store, cleanup="full") == {
assert await aindex(
docs,
arecord_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 1000,
"num_deleted": 0,
"num_skipped": 0,
@@ -2105,7 +2298,12 @@ async def test_afull_cleanup_with_different_batchsize(
]
assert await aindex(
docs, arecord_manager, vector_store, cleanup="full", cleanup_batch_size=17
docs,
arecord_manager,
vector_store,
cleanup="full",
cleanup_batch_size=17,
key_encoder="sha256",
) == {
"num_added": 1001,
"num_deleted": 1000,
@@ -2132,6 +2330,7 @@ async def test_aincremental_cleanup_with_different_batchsize(
vector_store,
source_id_key="source",
cleanup="incremental",
key_encoder="sha256",
) == {
"num_added": 1000,
"num_deleted": 0,
@@ -2154,6 +2353,7 @@ async def test_aincremental_cleanup_with_different_batchsize(
cleanup="incremental",
source_id_key="source",
cleanup_batch_size=17,
key_encoder="sha256",
) == {
"num_added": 1001,
"num_deleted": 1000,
@@ -2185,7 +2385,13 @@ def test_deduplication_v2(
),
]
assert index(docs, record_manager, vector_store, cleanup="full") == {
assert index(
docs,
record_manager,
vector_store,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 3,
"num_deleted": 0,
"num_skipped": 1,
@@ -2246,14 +2452,26 @@ def test_indexing_force_update(
),
]
assert index(docs, record_manager, upserting_vector_store, cleanup="full") == {
assert index(
docs,
record_manager,
upserting_vector_store,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 2,
"num_deleted": 0,
"num_skipped": 1,
"num_updated": 0,
}
assert index(docs, record_manager, upserting_vector_store, cleanup="full") == {
assert index(
docs,
record_manager,
upserting_vector_store,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
"num_skipped": 3,
@@ -2261,7 +2479,12 @@ def test_indexing_force_update(
}
assert index(
docs, record_manager, upserting_vector_store, cleanup="full", force_update=True
docs,
record_manager,
upserting_vector_store,
cleanup="full",
force_update=True,
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
@@ -2290,7 +2513,11 @@ async def test_aindexing_force_update(
]
assert await aindex(
docs, arecord_manager, upserting_vector_store, cleanup="full"
docs,
arecord_manager,
upserting_vector_store,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 2,
"num_deleted": 0,
@@ -2299,7 +2526,11 @@ async def test_aindexing_force_update(
}
assert await aindex(
docs, arecord_manager, upserting_vector_store, cleanup="full"
docs,
arecord_manager,
upserting_vector_store,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
@@ -2313,6 +2544,7 @@ async def test_aindexing_force_update(
upserting_vector_store,
cleanup="full",
force_update=True,
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
@@ -2377,7 +2609,11 @@ async def test_aindexing_custom_batch_size(
)
vector_store.aadd_documents = mock_add_documents # type: ignore[method-assign]
await aindex(
docs, arecord_manager, vector_store, batch_size=batch_size, key_encoder="sha256"
docs,
arecord_manager,
vector_store,
batch_size=batch_size,
key_encoder="sha256",
)
args, kwargs = mock_add_documents.call_args
assert args == ([doc_with_id],)
@@ -2398,14 +2634,26 @@ def test_index_into_document_index(record_manager: InMemoryRecordManager) -> Non
),
]
assert index(docs, record_manager, document_index, cleanup="full") == {
assert index(
docs,
record_manager,
document_index,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 2,
"num_deleted": 0,
"num_skipped": 0,
"num_updated": 0,
}
assert index(docs, record_manager, document_index, cleanup="full") == {
assert index(
docs,
record_manager,
document_index,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
"num_skipped": 2,
@@ -2413,7 +2661,12 @@ def test_index_into_document_index(record_manager: InMemoryRecordManager) -> Non
}
assert index(
docs, record_manager, document_index, cleanup="full", force_update=True
docs,
record_manager,
document_index,
cleanup="full",
force_update=True,
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
@@ -2421,7 +2674,13 @@ def test_index_into_document_index(record_manager: InMemoryRecordManager) -> Non
"num_updated": 2,
}
assert index([], record_manager, document_index, cleanup="full") == {
assert index(
[],
record_manager,
document_index,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 2,
"num_skipped": 0,
@@ -2445,14 +2704,26 @@ async def test_aindex_into_document_index(
),
]
assert await aindex(docs, arecord_manager, document_index, cleanup="full") == {
assert await aindex(
docs,
arecord_manager,
document_index,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 2,
"num_deleted": 0,
"num_skipped": 0,
"num_updated": 0,
}
assert await aindex(docs, arecord_manager, document_index, cleanup="full") == {
assert await aindex(
docs,
arecord_manager,
document_index,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
"num_skipped": 2,
@@ -2460,7 +2731,12 @@ async def test_aindex_into_document_index(
}
assert await aindex(
docs, arecord_manager, document_index, cleanup="full", force_update=True
docs,
arecord_manager,
document_index,
cleanup="full",
force_update=True,
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 0,
@@ -2468,7 +2744,13 @@ async def test_aindex_into_document_index(
"num_updated": 2,
}
assert await aindex([], arecord_manager, document_index, cleanup="full") == {
assert await aindex(
[],
arecord_manager,
document_index,
cleanup="full",
key_encoder="sha256",
) == {
"num_added": 0,
"num_deleted": 2,
"num_skipped": 0,
@@ -2496,7 +2778,13 @@ def test_index_with_upsert_kwargs(
upsert_kwargs = {"vector_field": "embedding"}
index(docs, record_manager, upserting_vector_store, upsert_kwargs=upsert_kwargs)
index(
docs,
record_manager,
upserting_vector_store,
upsert_kwargs=upsert_kwargs,
key_encoder="sha256",
)
# Assert that add_documents was called with the correct arguments
mock_add_documents.assert_called_once()
@@ -2549,6 +2837,7 @@ def test_index_with_upsert_kwargs_for_document_indexer(
document_index,
cleanup="full",
upsert_kwargs=upsert_kwargs,
key_encoder="sha256",
) == {
"num_added": 2,
"num_deleted": 0,
@@ -2587,6 +2876,7 @@ async def test_aindex_with_upsert_kwargs_for_document_indexer(
document_index,
cleanup="full",
upsert_kwargs=upsert_kwargs,
key_encoder="sha256",
) == {
"num_added": 2,
"num_deleted": 0,
@@ -2624,6 +2914,7 @@ async def test_aindex_with_upsert_kwargs(
arecord_manager,
upserting_vector_store,
upsert_kwargs=upsert_kwargs,
key_encoder="sha256",
)
# Assert that aadd_documents was called with the correct arguments

View File

@@ -1484,3 +1484,64 @@ def test_get_buffer_string_with_empty_content() -> None:
expected = "Human: \nAI: \nSystem: "
actual = get_buffer_string(messages)
assert actual == expected
def test_convert_to_openai_messages_reasoning_content() -> None:
"""Test convert_to_openai_messages with reasoning content blocks."""
# Test reasoning block with empty summary
msg = AIMessage(content=[{"type": "reasoning", "summary": []}])
result = convert_to_openai_messages(msg, text_format="block")
expected = {"role": "assistant", "content": [{"type": "reasoning", "summary": []}]}
assert result == expected
# Test reasoning block with summary content
msg_with_summary = AIMessage(
content=[
{
"type": "reasoning",
"summary": [
{"type": "text", "text": "First thought"},
{"type": "text", "text": "Second thought"},
],
}
]
)
result_with_summary = convert_to_openai_messages(
msg_with_summary, text_format="block"
)
expected_with_summary = {
"role": "assistant",
"content": [
{
"type": "reasoning",
"summary": [
{"type": "text", "text": "First thought"},
{"type": "text", "text": "Second thought"},
],
}
],
}
assert result_with_summary == expected_with_summary
# Test mixed content with reasoning and text
mixed_msg = AIMessage(
content=[
{"type": "text", "text": "Regular response"},
{
"type": "reasoning",
"summary": [{"type": "text", "text": "My reasoning process"}],
},
]
)
mixed_result = convert_to_openai_messages(mixed_msg, text_format="block")
expected_mixed = {
"role": "assistant",
"content": [
{"type": "text", "text": "Regular response"},
{
"type": "reasoning",
"summary": [{"type": "text", "text": "My reasoning process"}],
},
],
}
assert mixed_result == expected_mixed

View File

@@ -382,10 +382,10 @@
'description': '''
Message for passing the result of executing a tool back to a model.
FunctionMessage are an older version of the ToolMessage schema, and
do not contain the tool_call_id field.
``FunctionMessage`` are an older version of the ``ToolMessage`` schema, and
do not contain the ``tool_call_id`` field.
The tool_call_id field is used to associate the tool call request with the
The ``tool_call_id`` field is used to associate the tool call request with the
tool call response. This is useful in situations where a chat model is able
to request multiple tool calls in parallel.
''',
@@ -517,7 +517,7 @@
'description': '''
Message from a human.
HumanMessages are messages that are passed in from a human to the model.
``HumanMessage``s are messages that are passed in from a human to the model.
Example:
@@ -688,7 +688,6 @@
Does *not* need to sum to full input token count. Does *not* need to have all keys.
Example:
.. code-block:: python
{
@@ -722,7 +721,7 @@
'description': '''
Allowance for errors made by LLM.
Here we add an `error` key to surface errors made during generation
Here we add an ``error`` key to surface errors made during generation
(e.g., invalid JSON arguments.)
''',
'properties': dict({
@@ -792,7 +791,6 @@
Does *not* need to sum to full output token count. Does *not* need to have all keys.
Example:
.. code-block:: python
{
@@ -984,8 +982,8 @@
{"name": "foo", "args": {"a": 1}, "id": "123"}
This represents a request to call the tool named "foo" with arguments {"a": 1}
and an identifier of "123".
This represents a request to call the tool named ``'foo'`` with arguments
``{"a": 1}`` and an identifier of ``'123'``.
''',
'properties': dict({
'args': dict({
@@ -1025,9 +1023,9 @@
'description': '''
A chunk of a tool call (e.g., as part of a stream).
When merging ToolCallChunks (e.g., via AIMessageChunk.__add__),
When merging ``ToolCallChunk``s (e.g., via ``AIMessageChunk.__add__``),
all string attributes are concatenated. Chunks are only merged if their
values of `index` are equal and not None.
values of ``index`` are equal and not None.
Example:
@@ -1106,10 +1104,10 @@
'description': '''
Message for passing the result of executing a tool back to a model.
ToolMessages contain the result of a tool invocation. Typically, the result
is encoded inside the `content` field.
``ToolMessage``s contain the result of a tool invocation. Typically, the result
is encoded inside the ``content`` field.
Example: A ToolMessage representing a result of 42 from a tool call with id
Example: A ``ToolMessage`` representing a result of ``42`` from a tool call with id
.. code-block:: python
@@ -1118,7 +1116,7 @@
ToolMessage(content="42", tool_call_id="call_Jja7J89XsjrOLA5r!MEOW!SL")
Example: A ToolMessage where only part of the tool output is sent to the model
Example: A ``ToolMessage`` where only part of the tool output is sent to the model
and the full output is passed in to artifact.
.. versionadded:: 0.2.17
@@ -1140,7 +1138,7 @@
tool_call_id="call_Jja7J89XsjrOLA5r!MEOW!SL",
)
The tool_call_id field is used to associate the tool call request with the
The ``tool_call_id`` field is used to associate the tool call request with the
tool call response. This is useful in situations where a chat model is able
to request multiple tool calls in parallel.
''',
@@ -1312,7 +1310,6 @@
This is a standard representation of token usage that is consistent across models.
Example:
.. code-block:: python
{
@@ -1803,10 +1800,10 @@
'description': '''
Message for passing the result of executing a tool back to a model.
FunctionMessage are an older version of the ToolMessage schema, and
do not contain the tool_call_id field.
``FunctionMessage`` are an older version of the ``ToolMessage`` schema, and
do not contain the ``tool_call_id`` field.
The tool_call_id field is used to associate the tool call request with the
The ``tool_call_id`` field is used to associate the tool call request with the
tool call response. This is useful in situations where a chat model is able
to request multiple tool calls in parallel.
''',
@@ -1938,7 +1935,7 @@
'description': '''
Message from a human.
HumanMessages are messages that are passed in from a human to the model.
``HumanMessage``s are messages that are passed in from a human to the model.
Example:
@@ -2109,7 +2106,6 @@
Does *not* need to sum to full input token count. Does *not* need to have all keys.
Example:
.. code-block:: python
{
@@ -2143,7 +2139,7 @@
'description': '''
Allowance for errors made by LLM.
Here we add an `error` key to surface errors made during generation
Here we add an ``error`` key to surface errors made during generation
(e.g., invalid JSON arguments.)
''',
'properties': dict({
@@ -2213,7 +2209,6 @@
Does *not* need to sum to full output token count. Does *not* need to have all keys.
Example:
.. code-block:: python
{
@@ -2405,8 +2400,8 @@
{"name": "foo", "args": {"a": 1}, "id": "123"}
This represents a request to call the tool named "foo" with arguments {"a": 1}
and an identifier of "123".
This represents a request to call the tool named ``'foo'`` with arguments
``{"a": 1}`` and an identifier of ``'123'``.
''',
'properties': dict({
'args': dict({
@@ -2446,9 +2441,9 @@
'description': '''
A chunk of a tool call (e.g., as part of a stream).
When merging ToolCallChunks (e.g., via AIMessageChunk.__add__),
When merging ``ToolCallChunk``s (e.g., via ``AIMessageChunk.__add__``),
all string attributes are concatenated. Chunks are only merged if their
values of `index` are equal and not None.
values of ``index`` are equal and not None.
Example:
@@ -2527,10 +2522,10 @@
'description': '''
Message for passing the result of executing a tool back to a model.
ToolMessages contain the result of a tool invocation. Typically, the result
is encoded inside the `content` field.
``ToolMessage``s contain the result of a tool invocation. Typically, the result
is encoded inside the ``content`` field.
Example: A ToolMessage representing a result of 42 from a tool call with id
Example: A ``ToolMessage`` representing a result of ``42`` from a tool call with id
.. code-block:: python
@@ -2539,7 +2534,7 @@
ToolMessage(content="42", tool_call_id="call_Jja7J89XsjrOLA5r!MEOW!SL")
Example: A ToolMessage where only part of the tool output is sent to the model
Example: A ``ToolMessage`` where only part of the tool output is sent to the model
and the full output is passed in to artifact.
.. versionadded:: 0.2.17
@@ -2561,7 +2556,7 @@
tool_call_id="call_Jja7J89XsjrOLA5r!MEOW!SL",
)
The tool_call_id field is used to associate the tool call request with the
The ``tool_call_id`` field is used to associate the tool call request with the
tool call response. This is useful in situations where a chat model is able
to request multiple tool calls in parallel.
''',
@@ -2733,7 +2728,6 @@
This is a standard representation of token usage that is consistent across models.
Example:
.. code-block:: python
{

View File

@@ -27,7 +27,6 @@ EXAMPLE_PROMPT = PromptTemplate(
@pytest.fixture
@pytest.mark.requires("jinja2")
def example_jinja2_prompt() -> tuple[PromptTemplate, list[dict[str, str]]]:
example_template = "{{ word }}: {{ antonym }}"

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