Compare commits

...

70 Commits

Author SHA1 Message Date
Mason Daugherty
91e825b92c Merge branch 'wip-v0.4' into cc/0.4/docs 2025-08-11 15:11:33 -04:00
Mason Daugherty
281488a5cf Merge branch 'master' into wip-v0.4 2025-08-11 15:10:42 -04:00
Mason Daugherty
ee4c2510eb feat: port various nit changes from wip-v0.4 (#32506)
Lots of work that wasn't directly related to core
improvements/messages/testing functionality
2025-08-11 15:09:08 -04:00
Mason Daugherty
8d2ba88ef0 Merge branch 'master' into wip-v0.4 2025-08-11 13:45:21 -04:00
mishraravibhushan
7db9e60601 docs(docs): fix grammar, capitalization, and style issues across documentation (#32503)
**Changes made:**
- Fix 'Async programming with langchain' → 'Async programming with
LangChain'
- Fix 'Langchain asynchronous APIs' → 'LangChain asynchronous APIs'
- Fix 'How to: init any model' → 'How to: initialize any model'
- Fix 'async programming with Langchain' → 'async programming with
LangChain'
- Fix 'How to propagate callbacks constructor' → 'How to propagate
callbacks to the constructor'
- Fix 'How to add a semantic layer over graph database' → 'How to add a
semantic layer over a graph database'
- Fix 'Build a Question/Answering system' → 'Build a Question-Answering
system'

**Why is this change needed?**
- Improves documentation clarity and readability
- Maintains consistent LangChain branding throughout the docs
- Fixes grammar issues that could confuse users
- Follows proper documentation standards

**Files changed:**
- `docs/docs/concepts/async.mdx`
- `docs/docs/concepts/tools.mdx`
- `docs/docs/how_to/index.mdx`
- `docs/docs/how_to/callbacks_constructor.ipynb`
- `docs/docs/how_to/graph_semantic.ipynb`
- `docs/docs/tutorials/sql_qa.ipynb`

**Issue:** N/A (documentation improvements)

**Dependencies:** None

**Twitter handle:** https://x.com/mishraravibhush

Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-08-11 13:32:28 -04:00
Mason Daugherty
e5d0a4e4d6 feat(standard-tests): formatting (#32504)
Not touching `pyproject.toml` or chat model related items as to not
interfere with work in wip0.4 branch
2025-08-11 13:30:30 -04:00
Mason Daugherty
457ce9c4b0 feat(text-splitters): ruff fixes and rules (#32502) 2025-08-11 13:28:22 -04:00
Mason Daugherty
27b6b53f20 feat(xai): ruff fixes and rules (#32501) 2025-08-11 13:03:07 -04:00
Christophe Bornet
f55186b38f fix(core): fix beta decorator for properties (#32497) 2025-08-11 12:43:53 -04:00
Mason Daugherty
374f414c91 feat(qdrant): ruff fixes and rules (#32500) 2025-08-11 12:43:41 -04:00
dependabot[bot]
9b3f3dc8d9 chore: bump actions/download-artifact from 4 to 5 (#32495)
Bumps
[actions/download-artifact](https://github.com/actions/download-artifact)
from 4 to 5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/download-artifact/releases">actions/download-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/407">actions/download-artifact#407</a></li>
<li>BREAKING fix: inconsistent path behavior for single artifact
downloads by ID by <a
href="https://github.com/GrantBirki"><code>@​GrantBirki</code></a> in <a
href="https://redirect.github.com/actions/download-artifact/pull/416">actions/download-artifact#416</a></li>
</ul>
<h2>v5.0.0</h2>
<h3>🚨 Breaking Change</h3>
<p>This release fixes an inconsistency in path behavior for single
artifact downloads by ID. <strong>If you're downloading single artifacts
by ID, the output path may change.</strong></p>
<h4>What Changed</h4>
<p>Previously, <strong>single artifact downloads</strong> behaved
differently depending on how you specified the artifact:</p>
<ul>
<li><strong>By name</strong>: <code>name: my-artifact</code> → extracted
to <code>path/</code> (direct)</li>
<li><strong>By ID</strong>: <code>artifact-ids: 12345</code> → extracted
to <code>path/my-artifact/</code> (nested)</li>
</ul>
<p>Now both methods are consistent:</p>
<ul>
<li><strong>By name</strong>: <code>name: my-artifact</code> → extracted
to <code>path/</code> (unchanged)</li>
<li><strong>By ID</strong>: <code>artifact-ids: 12345</code> → extracted
to <code>path/</code> (fixed - now direct)</li>
</ul>
<h4>Migration Guide</h4>
<h5> No Action Needed If:</h5>
<ul>
<li>You download artifacts by <strong>name</strong></li>
<li>You download <strong>multiple</strong> artifacts by ID</li>
<li>You already use <code>merge-multiple: true</code> as a
workaround</li>
</ul>
<h5>⚠️ Action Required If:</h5>
<p>You download <strong>single artifacts by ID</strong> and your
workflows expect the nested directory structure.</p>
<p><strong>Before v5 (nested structure):</strong></p>
<pre lang="yaml"><code>- uses: actions/download-artifact@v4
  with:
    artifact-ids: 12345
    path: dist
# Files were in: dist/my-artifact/
</code></pre>
<blockquote>
<p>Where <code>my-artifact</code> is the name of the artifact you
previously uploaded</p>
</blockquote>
<p><strong>To maintain old behavior (if needed):</strong></p>
<pre lang="yaml"><code>&lt;/tr&gt;&lt;/table&gt; 
</code></pre>
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="634f93cb29"><code>634f93c</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/416">#416</a>
from actions/single-artifact-id-download-path</li>
<li><a
href="b19ff43027"><code>b19ff43</code></a>
refactor: resolve download path correctly in artifact download tests
(mainly ...</li>
<li><a
href="e262cbee4a"><code>e262cbe</code></a>
bundle dist</li>
<li><a
href="bff23f9308"><code>bff23f9</code></a>
update docs</li>
<li><a
href="fff8c148a8"><code>fff8c14</code></a>
fix download path logic when downloading a single artifact by id</li>
<li><a
href="448e3f862a"><code>448e3f8</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/download-artifact/issues/407">#407</a>
from actions/nebuk89-patch-1</li>
<li><a
href="47225c44b3"><code>47225c4</code></a>
Update README.md</li>
<li>See full diff in <a
href="https://github.com/actions/download-artifact/compare/v4...v5">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-11 12:41:58 -04:00
ccurme
45a067509f fix(core): fix tracing for PDFs in v1 messages (#32434) 2025-08-11 12:18:32 -04:00
lineuman
afc3b1824c docs(deepseek): Add DeepSeek model option (#32481) 2025-08-11 09:20:39 -04:00
ran8080
130b7e6170 docs(docs): add missing name to AIMessage in example (#32482)
**Description:**

In the `docs/docs/how_to/structured_output.ipynb` notebook, an
`AIMessage` within the tool-calling few-shot example was missing the
`name="example_assistant"` parameter. This was inconsistent with the
other `AIMessage` instances in the same list.

This change adds the missing `name` parameter to ensure all examples in
the section are consistent, improving the clarity and correctness of the
documentation.

**Issue:** N/A

**Dependencies:** N/A
2025-08-11 09:20:09 -04:00
Navanit Dubey
d40fa534c1 docs(docs): use model_json_schema() (#32485)
While trying the line People.schema got a warning. 
```The `schema` method is deprecated; use `model_json_schema` instead```

So made the changes and now working file.

Thank you for contributing to LangChain! Follow these steps to mark your pull request as ready for review. **If any of these steps are not completed, your PR will not be considered for review.**

- [ ] **PR title**: Follows the format: {TYPE}({SCOPE}): {DESCRIPTION}
  - Examples:
    - feat(core): add multi-tenant support
    - fix(cli): resolve flag parsing error
    - docs(openai): update API usage examples
  - Allowed `{TYPE}` values:
    - feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, release
  - Allowed `{SCOPE}` values (optional):
    - core, cli, langchain, standard-tests, docs, anthropic, chroma, deepseek, exa, fireworks, groq, huggingface, mistralai, nomic, ollama, openai, perplexity, prompty, qdrant, xai
  - Note: the `{DESCRIPTION}` must not start with an uppercase letter.
  - Once you've written the title, please delete this checklist item; do not include it in the PR.

- [ ] **PR message**: ***Delete this entire checklist*** and replace with
  - **Description:** a description of the change. Include a [closing keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) if applicable to a relevant issue.
  - **Issue:** the issue # it fixes, if applicable (e.g. Fixes #123)
  - **Dependencies:** any dependencies required for this change
  - **Twitter handle:** if your PR gets announced, and you'd like a mention, we'll gladly shout you out!

- [ ] **Add tests and docs**: If you're adding a new integration, you must include:
  1. A test for the integration, preferably unit tests that do not rely on network access,
  2. An example notebook showing its use. It lives in `docs/docs/integrations` directory.

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

Additional guidelines:

- Make sure optional dependencies are imported within a function.
- Please do not add dependencies to `pyproject.toml` files (even optional ones) unless they are **required** for unit tests.
- Most PRs should not touch more than one package.
- Changes should be backwards compatible.
2025-08-11 09:19:14 -04:00
mishraravibhushan
20bd296421 docs(docs): fix grammar in "How to deal with high-cardinality categoricals" guide title (#32488)
Description:
Corrected the guide title from "How deal with high cardinality
categoricals" to "How to deal with high-cardinality categoricals".
- Added missing "to" for grammatical correctness.
- Hyphenated "high-cardinality" for standard compound adjective usage.

Issue:
N/A

Dependencies:
None

Twitter handle:
https://x.com/mishraravibhush
2025-08-11 09:17:51 -04:00
Mason Daugherty
23c3fa65d4 feat(docs): enhance ResponseMetadata documentation with provider field usage notes (#32472) 2025-08-11 09:16:16 -04:00
Mason Daugherty
3c5cc349b6 docs: v0.4 top level refinements (#32474) 2025-08-11 09:15:59 -04:00
Mason Daugherty
5cfb7ce57b docs(ollama): update Ollama integration documentation for new chat model (#32475) 2025-08-11 09:13:54 -04:00
ccurme
9259eea846 fix(docs): use pepy for integration package download badges (#32491)
pypi stats has been down for some time.
2025-08-10 18:41:36 -04:00
ccurme
afcb097ef5 fix(docs): DigitalOcean Gradient: link to correct provider page and update page title (#32490) 2025-08-10 17:29:44 -04:00
Mason Daugherty
13d67cf37e fix(ollama): reasoning should come before text content (#32476) 2025-08-08 19:34:36 -04:00
Mason Daugherty
978119ef3c Merge branch 'wip-v0.4' into cc/0.4/docs 2025-08-08 17:28:54 -04:00
Chester Curme
dd68b762d9 headers -> details 2025-08-08 14:52:25 -04:00
Chester Curme
c784f63701 details -> columns 2025-08-08 14:50:51 -04:00
Chester Curme
aed20287af x 2025-08-08 14:29:20 -04:00
Chester Curme
5ada33b3e6 x 2025-08-08 14:06:44 -04:00
Mason Daugherty
7f989d3c3b feat(docs): clarify ToolMessage contentfield usage 2025-08-08 13:02:59 -04:00
Mason Daugherty
b7968c2b7d feat(docs): add link to artifact usage in ToolMessage 2025-08-08 12:51:15 -04:00
Chester Curme
a1c79711b3 update 2025-08-08 12:50:36 -04:00
ccurme
088095b663 release(openai): release 0.3.29 (#32463) 2025-08-08 11:04:33 -04:00
Chester Curme
1dc22c602e update 2025-08-08 11:00:03 -04:00
Chester Curme
18732e5b8b fix sidebar 2025-08-08 10:57:39 -04:00
Mason Daugherty
2f0c6421a1 Merge branch 'master' into wip-v0.4 2025-08-08 10:21:44 -04:00
Mason Daugherty
c31236264e chore: formatting across codebase (#32466) 2025-08-08 10:20:10 -04:00
Chester Curme
8f19ca30b0 update migration guides 2025-08-08 10:15:16 -04:00
Chester Curme
cfe13f673a Merge branch 'master' into wip-v0.4
# Conflicts:
#	libs/core/langchain_core/version.py
#	libs/core/pyproject.toml
#	libs/core/uv.lock
#	libs/partners/openai/tests/integration_tests/chat_models/test_responses_api.py
#	libs/partners/openai/uv.lock
2025-08-08 09:04:57 -04:00
ccurme
02001212b0 fix(openai): revert some changes (#32462)
Keep coverage on `output_version="v0"` (increasing coverage is being
managed in v0.4 branch).
2025-08-08 08:51:18 -04:00
Mason Daugherty
00244122bd feat(openai): minimal and verbosity (#32455) 2025-08-08 02:24:21 +00:00
Mason Daugherty
5599c59d4a chore: formatting across codebase (#32456)
To prevent polluting future PRs
2025-08-07 22:09:26 -04:00
ccurme
6727d6e8c8 release(core): 0.3.74 (#32454) 2025-08-07 16:39:01 -04:00
Michael Matloka
5036bd7adb fix(openai): don't crash get_num_tokens_from_messages on gpt-5 (#32451) 2025-08-07 16:33:19 -04:00
ccurme
ec2b34a02d feat(openai): custom tools (#32449) 2025-08-07 16:30:01 -04:00
Mason Daugherty
11d68a0b9e bump locks 2025-08-07 15:51:36 -04:00
Mason Daugherty
566774a893 Merge branch 'wip-v0.4' of github.com:langchain-ai/langchain into wip-v0.4 2025-08-07 15:50:40 -04:00
Mason Daugherty
255a6d668a feat: allow bypassing CI using PR label 2025-08-07 15:50:15 -04:00
Mason Daugherty
cbf4c0e565 Merge branch 'master' into wip-v0.4 2025-08-07 15:33:12 -04:00
Mason Daugherty
145d38f7dd test(openai): add tests for prompt_cache_key parameter and update docs (#32363)
Introduce tests to validate the behavior and inclusion of the
`prompt_cache_key` parameter in request payloads for the `ChatOpenAI`
model.
2025-08-07 15:29:47 -04:00
ccurme
68c70da33e fix(openai): add in output_text (#32450)
This property was deleted in `openai==1.99.2`.
2025-08-07 15:23:56 -04:00
Eugene Yurtsev
754528d23f feat(langchain): add stuff and map reduce chains (#32333)
* Add stuff and map reduce chains
* We'll need to rename and add unit tests to the chains prior to
official release
2025-08-07 15:20:05 -04:00
Mason Daugherty
dc66737f03 fix: docs and formatting (#32448) 2025-08-07 15:17:25 -04:00
Christophe Bornet
499dc35cfb chore(core): bump mypy version to 1.17 (#32390)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-08-07 13:26:29 -04:00
Mason Daugherty
42c1159991 feat: add TextAccessor, deprecate .text() as method (#32441)
Adds backward compat for `.text()` on messages while keeping `.text`
access

_The kicker:_

Any previous use of `.text()` will now need a `# type: ignore[operator]`
to silence type checkers. However, it will still behave as expected at
runtime. Deprecating in v0.4.0, to be removed in v2.0.0.
2025-08-07 12:16:31 -04:00
CLOVA Studio 개발
ac706c77d4 docs(docs): update v0.1.1 chatModel document on langchain-naver. (#32445)
## **Description:** 
This PR was requested after the `langchain-naver` partner-managed
packages were released
[v0.1.1](https://pypi.org/project/langchain-naver/0.1.1/).
So we've updated some our documents with the additional changed
features.

## **Dependencies:** 
https://github.com/langchain-ai/langchain/pull/30956

---------

Co-authored-by: 김필환[AI Studio Dev1] <pilhwan.kim@navercorp.com>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
2025-08-07 15:45:50 +00:00
Tianyu Chen
8493887b6f docs: update Docker image name for jaguardb setup (#32438)
**Description**
Updated the quick setup instructions for JaguarDB in the documentation.
Replaced the outdated Docker image `jaguardb/jaguardb_with_http` with
the current recommended image `jaguardb/jaguardb` for pulling and
running the server.
2025-08-07 11:23:29 -04:00
Christophe Bornet
a647073b26 feat(standard-tests): add a property to set the name of the parameter for the number of results to return (#32443)
Not all retrievers use `k` as param name to set the number of results to
return. Even in LangChain itself. Eg:
bc4251b9e0/libs/core/langchain_core/indexing/in_memory.py (L31)

So it's helpful to be able to change it for a given retriever.
The change also adds hints to disable the tests if the retriever doesn't
support setting the param in the constructor or in the invoke method
(for instance, the `InMemoryDocumentIndex` in the link supports in the
constructor but not in the invoke method).

This change is backward compatible.

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-07 11:22:24 -04:00
Mason Daugherty
cc6139860c fix: docs typing issues 2025-08-06 23:50:33 -04:00
Mason Daugherty
ae8f58ac6f fix(settings): update Python terminal settings and default interpreter path 2025-08-06 23:37:40 -04:00
Mason Daugherty
346731544b Merge branch 'master' into wip-v0.4 2025-08-06 18:24:10 -04:00
Mason Daugherty
c1b86cc929 feat: minor core work, v1 standard tests & (most of) v1 ollama (#32315)
Resolves #32215

---------

Co-authored-by: Chester Curme <chester.curme@gmail.com>
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
Co-authored-by: Nuno Campos <nuno@langchain.dev>
2025-08-06 18:22:02 -04:00
Mason Daugherty
376f70be96 sync wip with master (#32436)
Co-authored-by: Kanav Bansal <13186335+bansalkanav@users.noreply.github.com>
Co-authored-by: Pranav Bhartiya <124018094+pranauww@users.noreply.github.com>
Co-authored-by: Nelson Sproul <nelson.sproul@gmail.com>
Co-authored-by: John Bledsoe <jmbledsoe@gmail.com>
2025-08-06 17:57:05 -04:00
Chester Curme
a369b3aed5 update sidebar label 2025-08-06 16:43:18 -04:00
Chester Curme
5eec2207c0 update docusaurus config 2025-08-06 16:27:25 -04:00
Chester Curme
9b468a10a5 update vercel.json 2025-08-06 16:11:17 -04:00
Chester Curme
b7494d6566 x 2025-08-06 15:53:06 -04:00
ccurme
ac2de920b1 chore: increment versions for 0.4 branch (#32419) 2025-08-05 15:39:37 -04:00
ccurme
e02eed5489 feat: standard outputs (#32287)
Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Nuno Campos <nuno@langchain.dev>
2025-08-05 15:17:32 -04:00
Chester Curme
5414527236 Merge branch 'master' into wip-v0.4 2025-08-04 11:55:14 -04:00
Chester Curme
881c6534a6 Merge branch 'master' into wip-v0.4
# Conflicts:
#	.github/workflows/_integration_test.yml
#	.github/workflows/_release.yml
#	.github/workflows/api_doc_build.yml
#	.github/workflows/people.yml
#	.github/workflows/run_notebooks.yml
#	.github/workflows/scheduled_test.yml
#	SECURITY.md
#	docs/docs/integrations/vectorstores/pgvectorstore.ipynb
#	libs/langchain_v1/langchain/chat_models/base.py
#	libs/langchain_v1/tests/integration_tests/chat_models/test_base.py
#	libs/langchain_v1/tests/unit_tests/chat_models/test_chat_models.py
2025-07-30 13:16:17 -04:00
Mason Daugherty
5e9eb19a83 chore: update branch with changes from master (#32277)
Co-authored-by: Maxime Grenu <69890511+cluster2600@users.noreply.github.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: jmaillefaud <jonathan.maillefaud@evooq.ch>
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
Co-authored-by: tanwirahmad <tanwirahmad@users.noreply.github.com>
Co-authored-by: Christophe Bornet <cbornet@hotmail.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: niceg <79145285+growmuye@users.noreply.github.com>
Co-authored-by: Chaitanya varma <varmac301@gmail.com>
Co-authored-by: dishaprakash <57954147+dishaprakash@users.noreply.github.com>
Co-authored-by: Chester Curme <chester.curme@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Kanav Bansal <13186335+bansalkanav@users.noreply.github.com>
Co-authored-by: Aleksandr Filippov <71711753+alex-feel@users.noreply.github.com>
Co-authored-by: Alex Feel <afilippov@spotware.com>
2025-07-28 10:39:41 -04:00
304 changed files with 33273 additions and 7499 deletions

View File

@@ -15,12 +15,12 @@ You may use the button above, or follow these steps to open this repo in a Codes
1. Click **Create codespace on master**.
For more info, check out the [GitHub documentation](https://docs.github.com/en/free-pro-team@latest/github/developing-online-with-codespaces/creating-a-codespace#creating-a-codespace).
## VS Code Dev Containers
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/langchain-ai/langchain)
> [!NOTE]
> [!NOTE]
> If you click the link above you will open the main repo (`langchain-ai/langchain`) and *not* your local cloned repo. This is fine if you only want to run and test the library, but if you want to contribute you can use the link below and replace with your username and cloned repo name:
```txt

View File

@@ -4,7 +4,7 @@ services:
build:
dockerfile: libs/langchain/dev.Dockerfile
context: ..
networks:
- langchain-network

View File

@@ -129,4 +129,4 @@ For answers to common questions about this code of conduct, see the FAQ at
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -5,7 +5,7 @@ body:
- type: markdown
attributes:
value: |
Thank you for taking the time to file a bug report.
Thank you for taking the time to file a bug report.
Use this to report BUGS in LangChain. For usage questions, feature requests and general design questions, please use the [LangChain Forum](https://forum.langchain.com/).
@@ -50,7 +50,7 @@ body:
If a maintainer can copy it, run it, and see it right away, there's a much higher chance that you'll be able to get help.
**Important!**
**Important!**
* Avoid screenshots when possible, as they are hard to read and (more importantly) don't allow others to copy-and-paste your code.
* Reduce your code to the minimum required to reproduce the issue if possible. This makes it much easier for others to help you.
@@ -58,14 +58,14 @@ body:
* INCLUDE the language label (e.g. `python`) after the first three backticks to enable syntax highlighting. (e.g., ```python rather than ```).
placeholder: |
The following code:
The following code:
```python
from langchain_core.runnables import RunnableLambda
def bad_code(inputs) -> int:
raise NotImplementedError('For demo purpose')
chain = RunnableLambda(bad_code)
chain.invoke('Hello!')
```

View File

@@ -14,7 +14,7 @@ body:
Do **NOT** use this to ask usage questions or reporting issues with your code.
If you have usage questions or need help solving some problem,
If you have usage questions or need help solving some problem,
please use the [LangChain Forum](https://forum.langchain.com/).
If you're in the wrong place, here are some helpful links to find a better

View File

@@ -8,7 +8,7 @@ body:
If you are not a LangChain maintainer or were not asked directly by a maintainer to create an issue, then please start the conversation on the [LangChain Forum](https://forum.langchain.com/) instead.
You are a LangChain maintainer if you maintain any of the packages inside of the LangChain repository
You are a LangChain maintainer if you maintain any of the packages inside of the LangChain repository
or are a regular contributor to LangChain with previous merged pull requests.
- type: checkboxes
id: privileged

View File

@@ -4,4 +4,4 @@ RUN pip install httpx PyGithub "pydantic==2.0.2" pydantic-settings "pyyaml>=5.3.
COPY ./app /app
CMD ["python", "/app/main.py"]
CMD ["python", "/app/main.py"]

View File

@@ -4,8 +4,8 @@ description: "Generate the data for the LangChain People page"
author: "Jacob Lee <jacob@langchain.dev>"
inputs:
token:
description: 'User token, to read the GitHub API. Can be passed in using {{ secrets.LANGCHAIN_PEOPLE_GITHUB_TOKEN }}'
description: "User token, to read the GitHub API. Can be passed in using {{ secrets.LANGCHAIN_PEOPLE_GITHUB_TOKEN }}"
required: true
runs:
using: 'docker'
image: 'Dockerfile'
using: "docker"
image: "Dockerfile"

View File

@@ -3,14 +3,12 @@ import json
import os
import sys
from collections import defaultdict
from typing import Dict, List, Set
from pathlib import Path
from typing import Dict, List, Set
import tomllib
from packaging.requirements import Requirement
from get_min_versions import get_min_version_from_toml
from packaging.requirements import Requirement
LANGCHAIN_DIRS = [
"libs/core",
@@ -38,7 +36,7 @@ IGNORED_PARTNERS = [
]
PY_312_MAX_PACKAGES = [
"libs/partners/chroma", # https://github.com/chroma-core/chroma/issues/4382
"libs/partners/chroma", # https://github.com/chroma-core/chroma/issues/4382
]
@@ -85,9 +83,9 @@ def dependents_graph() -> dict:
for depline in extended_deps:
if depline.startswith("-e "):
# editable dependency
assert depline.startswith(
"-e ../partners/"
), "Extended test deps should only editable install partner packages"
assert depline.startswith("-e ../partners/"), (
"Extended test deps should only editable install partner packages"
)
partner = depline.split("partners/")[1]
dep = f"langchain-{partner}"
else:
@@ -271,7 +269,7 @@ if __name__ == "__main__":
dirs_to_run["extended-test"].add(dir_)
elif file.startswith("libs/standard-tests"):
# TODO: update to include all packages that rely on standard-tests (all partner packages)
# note: won't run on external repo partners
# Note: won't run on external repo partners
dirs_to_run["lint"].add("libs/standard-tests")
dirs_to_run["test"].add("libs/standard-tests")
dirs_to_run["lint"].add("libs/cli")
@@ -285,7 +283,7 @@ if __name__ == "__main__":
elif file.startswith("libs/cli"):
dirs_to_run["lint"].add("libs/cli")
dirs_to_run["test"].add("libs/cli")
elif file.startswith("libs/partners"):
partner_dir = file.split("/")[2]
if os.path.isdir(f"libs/partners/{partner_dir}") and [
@@ -303,7 +301,10 @@ if __name__ == "__main__":
f"Unknown lib: {file}. check_diff.py likely needs "
"an update for this new library!"
)
elif file.startswith("docs/") or file in ["pyproject.toml", "uv.lock"]: # docs or root uv files
elif file.startswith("docs/") or file in [
"pyproject.toml",
"uv.lock",
]: # docs or root uv files
docs_edited = True
dirs_to_run["lint"].add(".")

View File

@@ -1,4 +1,5 @@
import sys
import tomllib
if __name__ == "__main__":

View File

@@ -1,5 +1,5 @@
from collections import defaultdict
import sys
from collections import defaultdict
from typing import Optional
if sys.version_info >= (3, 11):
@@ -8,17 +8,13 @@ else:
# for python 3.10 and below, which doesnt have stdlib tomllib
import tomli as tomllib
from packaging.requirements import Requirement
from packaging.specifiers import SpecifierSet
from packaging.version import Version
import requests
from packaging.version import parse
import re
from typing import List
import re
import requests
from packaging.requirements import Requirement
from packaging.specifiers import SpecifierSet
from packaging.version import Version, parse
MIN_VERSION_LIBS = [
"langchain-core",
@@ -72,11 +68,13 @@ def get_minimum_version(package_name: str, spec_string: str) -> Optional[str]:
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)
for y in range(1, 10):
spec_string = re.sub(rf"\^0\.{y}\.(\d+)", rf">=0.{y}.\1,<0.{y+1}", spec_string)
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)
for x in range(1, 10):
spec_string = re.sub(
rf"\^{x}\.(\d+)\.(\d+)", rf">={x}.\1.\2,<{x+1}", spec_string
rf"\^{x}\.(\d+)\.(\d+)", rf">={x}.\1.\2,<{x + 1}", spec_string
)
spec_set = SpecifierSet(spec_string)
@@ -169,12 +167,12 @@ def check_python_version(version_string, 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
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)
for x in range(1, 10):
constraint_string = re.sub(
rf"\^{x}\.0\.(\d+)", rf">={x}.0.\1,<{x+1}.0.0", constraint_string
rf"\^{x}\.0\.(\d+)", rf">={x}.0.\1,<{x + 1}.0.0", constraint_string
)
try:

View File

@@ -3,9 +3,10 @@
import os
import shutil
import yaml
from pathlib import Path
from typing import Dict, Any
from typing import Any, Dict
import yaml
def load_packages_yaml() -> Dict[str, Any]:
@@ -28,7 +29,6 @@ def get_target_dir(package_name: str) -> Path:
def clean_target_directories(packages: list) -> None:
"""Remove old directories that will be replaced."""
for package in packages:
target_dir = get_target_dir(package["name"])
if target_dir.exists():
print(f"Removing {target_dir}")
@@ -38,7 +38,6 @@ def clean_target_directories(packages: list) -> None:
def move_libraries(packages: list) -> None:
"""Move libraries from their source locations to the target directories."""
for package in packages:
repo_name = package["repo"].split("/")[1]
source_path = package["path"]
target_dir = get_target_dir(package["name"])
@@ -68,23 +67,33 @@ def main():
package_yaml = load_packages_yaml()
# Clean target directories
clean_target_directories([
p
for p in package_yaml["packages"]
if (p["repo"].startswith("langchain-ai/") or p.get("include_in_api_ref"))
and p["repo"] != "langchain-ai/langchain"
and p["name"] != "langchain-ai21" # Skip AI21 due to dependency conflicts
])
clean_target_directories(
[
p
for p in package_yaml["packages"]
if (
p["repo"].startswith("langchain-ai/") or p.get("include_in_api_ref")
)
and p["repo"] != "langchain-ai/langchain"
and p["name"]
!= "langchain-ai21" # Skip AI21 due to dependency conflicts
]
)
# Move libraries to their new locations
move_libraries([
p
for p in package_yaml["packages"]
if not p.get("disabled", False)
and (p["repo"].startswith("langchain-ai/") or p.get("include_in_api_ref"))
and p["repo"] != "langchain-ai/langchain"
and p["name"] != "langchain-ai21" # Skip AI21 due to dependency conflicts
])
move_libraries(
[
p
for p in package_yaml["packages"]
if not p.get("disabled", False)
and (
p["repo"].startswith("langchain-ai/") or p.get("include_in_api_ref")
)
and p["repo"] != "langchain-ai/langchain"
and p["name"]
!= "langchain-ai21" # Skip AI21 due to dependency conflicts
]
)
# Delete ones without a pyproject.toml
for partner in Path("langchain/libs/partners").iterdir():

View File

@@ -81,56 +81,93 @@ import time
__version__ = "2022.12+dev"
# Update symlinks only if the platform supports not following them
UPDATE_SYMLINKS = bool(os.utime in getattr(os, 'supports_follow_symlinks', []))
UPDATE_SYMLINKS = bool(os.utime in getattr(os, "supports_follow_symlinks", []))
# Call os.path.normpath() only if not in a POSIX platform (Windows)
NORMALIZE_PATHS = (os.path.sep != '/')
NORMALIZE_PATHS = os.path.sep != "/"
# How many files to process in each batch when re-trying merge commits
STEPMISSING = 100
# (Extra) keywords for the os.utime() call performed by touch()
UTIME_KWS = {} if not UPDATE_SYMLINKS else {'follow_symlinks': False}
UTIME_KWS = {} if not UPDATE_SYMLINKS else {"follow_symlinks": False}
# Command-line interface ######################################################
def parse_args():
parser = argparse.ArgumentParser(
description=__doc__.split('\n---')[0])
parser = argparse.ArgumentParser(description=__doc__.split("\n---")[0])
group = parser.add_mutually_exclusive_group()
group.add_argument('--quiet', '-q', dest='loglevel',
action="store_const", const=logging.WARNING, default=logging.INFO,
help="Suppress informative messages and summary statistics.")
group.add_argument('--verbose', '-v', action="count", help="""
group.add_argument(
"--quiet",
"-q",
dest="loglevel",
action="store_const",
const=logging.WARNING,
default=logging.INFO,
help="Suppress informative messages and summary statistics.",
)
group.add_argument(
"--verbose",
"-v",
action="count",
help="""
Print additional information for each processed file.
Specify twice to further increase verbosity.
""")
""",
)
parser.add_argument('--cwd', '-C', metavar="DIRECTORY", help="""
parser.add_argument(
"--cwd",
"-C",
metavar="DIRECTORY",
help="""
Run as if %(prog)s was started in directory %(metavar)s.
This affects how --work-tree, --git-dir and PATHSPEC arguments are handled.
See 'man 1 git' or 'git --help' for more information.
""")
""",
)
parser.add_argument('--git-dir', dest='gitdir', metavar="GITDIR", help="""
parser.add_argument(
"--git-dir",
dest="gitdir",
metavar="GITDIR",
help="""
Path to the git repository, by default auto-discovered by searching
the current directory and its parents for a .git/ subdirectory.
""")
""",
)
parser.add_argument('--work-tree', dest='workdir', metavar="WORKTREE", help="""
parser.add_argument(
"--work-tree",
dest="workdir",
metavar="WORKTREE",
help="""
Path to the work tree root, by default the parent of GITDIR if it's
automatically discovered, or the current directory if GITDIR is set.
""")
""",
)
parser.add_argument('--force', '-f', default=False, action="store_true", help="""
parser.add_argument(
"--force",
"-f",
default=False,
action="store_true",
help="""
Force updating files with uncommitted modifications.
Untracked files and uncommitted deletions, renames and additions are
always ignored.
""")
""",
)
parser.add_argument('--merge', '-m', default=False, action="store_true", help="""
parser.add_argument(
"--merge",
"-m",
default=False,
action="store_true",
help="""
Include merge commits.
Leads to more recent times and more files per commit, thus with the same
time, which may or may not be what you want.
@@ -138,71 +175,130 @@ def parse_args():
are found sooner, which can improve performance, sometimes substantially.
But as merge commits are usually huge, processing them may also take longer.
By default, merge commits are only used for files missing from regular commits.
""")
""",
)
parser.add_argument('--first-parent', default=False, action="store_true", help="""
parser.add_argument(
"--first-parent",
default=False,
action="store_true",
help="""
Consider only the first parent, the "main branch", when evaluating merge commits.
Only effective when merge commits are processed, either when --merge is
used or when finding missing files after the first regular log search.
See --skip-missing.
""")
""",
)
parser.add_argument('--skip-missing', '-s', dest="missing", default=True,
action="store_false", help="""
parser.add_argument(
"--skip-missing",
"-s",
dest="missing",
default=True,
action="store_false",
help="""
Do not try to find missing files.
If merge commits were not evaluated with --merge and some files were
not found in regular commits, by default %(prog)s searches for these
files again in the merge commits.
This option disables this retry, so files found only in merge commits
will not have their timestamp updated.
""")
""",
)
parser.add_argument('--no-directories', '-D', dest='dirs', default=True,
action="store_false", help="""
parser.add_argument(
"--no-directories",
"-D",
dest="dirs",
default=True,
action="store_false",
help="""
Do not update directory timestamps.
By default, use the time of its most recently created, renamed or deleted file.
Note that just modifying a file will NOT update its directory time.
""")
""",
)
parser.add_argument('--test', '-t', default=False, action="store_true",
help="Test run: do not actually update any file timestamp.")
parser.add_argument(
"--test",
"-t",
default=False,
action="store_true",
help="Test run: do not actually update any file timestamp.",
)
parser.add_argument('--commit-time', '-c', dest='commit_time', default=False,
action='store_true', help="Use commit time instead of author time.")
parser.add_argument(
"--commit-time",
"-c",
dest="commit_time",
default=False,
action="store_true",
help="Use commit time instead of author time.",
)
parser.add_argument('--oldest-time', '-o', dest='reverse_order', default=False,
action='store_true', help="""
parser.add_argument(
"--oldest-time",
"-o",
dest="reverse_order",
default=False,
action="store_true",
help="""
Update times based on the oldest, instead of the most recent commit of a file.
This reverses the order in which the git log is processed to emulate a
file "creation" date. Note this will be inaccurate for files deleted and
re-created at later dates.
""")
""",
)
parser.add_argument('--skip-older-than', metavar='SECONDS', type=int, help="""
parser.add_argument(
"--skip-older-than",
metavar="SECONDS",
type=int,
help="""
Ignore files that are currently older than %(metavar)s.
Useful in workflows that assume such files already have a correct timestamp,
as it may improve performance by processing fewer files.
""")
""",
)
parser.add_argument('--skip-older-than-commit', '-N', default=False,
action='store_true', help="""
parser.add_argument(
"--skip-older-than-commit",
"-N",
default=False,
action="store_true",
help="""
Ignore files older than the timestamp it would be updated to.
Such files may be considered "original", likely in the author's repository.
""")
""",
)
parser.add_argument('--unique-times', default=False, action="store_true", help="""
parser.add_argument(
"--unique-times",
default=False,
action="store_true",
help="""
Set the microseconds to a unique value per commit.
Allows telling apart changes that would otherwise have identical timestamps,
as git's time accuracy is in seconds.
""")
""",
)
parser.add_argument('pathspec', nargs='*', metavar='PATHSPEC', help="""
parser.add_argument(
"pathspec",
nargs="*",
metavar="PATHSPEC",
help="""
Only modify paths matching %(metavar)s, relative to current directory.
By default, update all but untracked files and submodules.
""")
""",
)
parser.add_argument('--version', '-V', action='version',
version='%(prog)s version {version}'.format(version=get_version()))
parser.add_argument(
"--version",
"-V",
action="version",
version="%(prog)s version {version}".format(version=get_version()),
)
args_ = parser.parse_args()
if args_.verbose:
@@ -212,17 +308,18 @@ def parse_args():
def get_version(version=__version__):
if not version.endswith('+dev'):
if not version.endswith("+dev"):
return version
try:
cwd = os.path.dirname(os.path.realpath(__file__))
return Git(cwd=cwd, errors=False).describe().lstrip('v')
return Git(cwd=cwd, errors=False).describe().lstrip("v")
except Git.Error:
return '-'.join((version, "unknown"))
return "-".join((version, "unknown"))
# Helper functions ############################################################
def setup_logging():
"""Add TRACE logging level and corresponding method, return the root logger"""
logging.TRACE = TRACE = logging.DEBUG // 2
@@ -255,11 +352,13 @@ def normalize(path):
if path and path[0] == '"':
# Python 2: path = path[1:-1].decode("string-escape")
# Python 3: https://stackoverflow.com/a/46650050/624066
path = (path[1:-1] # Remove enclosing double quotes
.encode('latin1') # Convert to bytes, required by 'unicode-escape'
.decode('unicode-escape') # Perform the actual octal-escaping decode
.encode('latin1') # 1:1 mapping to bytes, UTF-8 encoded
.decode('utf8', 'surrogateescape')) # Decode from UTF-8
path = (
path[1:-1] # Remove enclosing double quotes
.encode("latin1") # Convert to bytes, required by 'unicode-escape'
.decode("unicode-escape") # Perform the actual octal-escaping decode
.encode("latin1") # 1:1 mapping to bytes, UTF-8 encoded
.decode("utf8", "surrogateescape")
) # Decode from UTF-8
if NORMALIZE_PATHS:
# Make sure the slash matches the OS; for Windows we need a backslash
path = os.path.normpath(path)
@@ -282,12 +381,12 @@ def touch_ns(path, mtime_ns):
def isodate(secs: int):
# time.localtime() accepts floats, but discards fractional part
return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(secs))
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(secs))
def isodate_ns(ns: int):
# for integers fromtimestamp() is equivalent and ~16% slower than isodate()
return datetime.datetime.fromtimestamp(ns / 1000000000).isoformat(sep=' ')
return datetime.datetime.fromtimestamp(ns / 1000000000).isoformat(sep=" ")
def get_mtime_ns(secs: int, idx: int):
@@ -305,35 +404,49 @@ def get_mtime_path(path):
# Git class and parse_log(), the heart of the script ##########################
class Git:
def __init__(self, workdir=None, gitdir=None, cwd=None, errors=True):
self.gitcmd = ['git']
self.gitcmd = ["git"]
self.errors = errors
self._proc = None
if workdir: self.gitcmd.extend(('--work-tree', workdir))
if gitdir: self.gitcmd.extend(('--git-dir', gitdir))
if cwd: self.gitcmd.extend(('-C', cwd))
if workdir:
self.gitcmd.extend(("--work-tree", workdir))
if gitdir:
self.gitcmd.extend(("--git-dir", gitdir))
if cwd:
self.gitcmd.extend(("-C", cwd))
self.workdir, self.gitdir = self._get_repo_dirs()
def ls_files(self, paths: list = None):
return (normalize(_) for _ in self._run('ls-files --full-name', paths))
return (normalize(_) for _ in self._run("ls-files --full-name", paths))
def ls_dirty(self, force=False):
return (normalize(_[3:].split(' -> ', 1)[-1])
for _ in self._run('status --porcelain')
if _[:2] != '??' and (not force or (_[0] in ('R', 'A')
or _[1] == 'D')))
return (
normalize(_[3:].split(" -> ", 1)[-1])
for _ in self._run("status --porcelain")
if _[:2] != "??" and (not force or (_[0] in ("R", "A") or _[1] == "D"))
)
def log(self, merge=False, first_parent=False, commit_time=False,
reverse_order=False, paths: list = None):
cmd = 'whatchanged --pretty={}'.format('%ct' if commit_time else '%at')
if merge: cmd += ' -m'
if first_parent: cmd += ' --first-parent'
if reverse_order: cmd += ' --reverse'
def log(
self,
merge=False,
first_parent=False,
commit_time=False,
reverse_order=False,
paths: list = None,
):
cmd = "whatchanged --pretty={}".format("%ct" if commit_time else "%at")
if merge:
cmd += " -m"
if first_parent:
cmd += " --first-parent"
if reverse_order:
cmd += " --reverse"
return self._run(cmd, paths)
def describe(self):
return self._run('describe --tags', check=True)[0]
return self._run("describe --tags", check=True)[0]
def terminate(self):
if self._proc is None:
@@ -345,18 +458,22 @@ class Git:
pass
def _get_repo_dirs(self):
return (os.path.normpath(_) for _ in
self._run('rev-parse --show-toplevel --absolute-git-dir', check=True))
return (
os.path.normpath(_)
for _ in self._run(
"rev-parse --show-toplevel --absolute-git-dir", check=True
)
)
def _run(self, cmdstr: str, paths: list = None, output=True, check=False):
cmdlist = self.gitcmd + shlex.split(cmdstr)
if paths:
cmdlist.append('--')
cmdlist.append("--")
cmdlist.extend(paths)
popen_args = dict(universal_newlines=True, encoding='utf8')
popen_args = dict(universal_newlines=True, encoding="utf8")
if not self.errors:
popen_args['stderr'] = subprocess.DEVNULL
log.trace("Executing: %s", ' '.join(cmdlist))
popen_args["stderr"] = subprocess.DEVNULL
log.trace("Executing: %s", " ".join(cmdlist))
if not output:
return subprocess.call(cmdlist, **popen_args)
if check:
@@ -379,30 +496,26 @@ def parse_log(filelist, dirlist, stats, git, merge=False, filterlist=None):
mtime = 0
datestr = isodate(0)
for line in git.log(
merge,
args.first_parent,
args.commit_time,
args.reverse_order,
filterlist
merge, args.first_parent, args.commit_time, args.reverse_order, filterlist
):
stats['loglines'] += 1
stats["loglines"] += 1
# Blank line between Date and list of files
if not line:
continue
# Date line
if line[0] != ':': # Faster than `not line.startswith(':')`
stats['commits'] += 1
if line[0] != ":": # Faster than `not line.startswith(':')`
stats["commits"] += 1
mtime = int(line)
if args.unique_times:
mtime = get_mtime_ns(mtime, stats['commits'])
mtime = get_mtime_ns(mtime, stats["commits"])
if args.debug:
datestr = isodate(mtime)
continue
# File line: three tokens if it describes a renaming, otherwise two
tokens = line.split('\t')
tokens = line.split("\t")
# Possible statuses:
# M: Modified (content changed)
@@ -411,7 +524,7 @@ def parse_log(filelist, dirlist, stats, git, merge=False, filterlist=None):
# T: Type changed: to/from regular file, symlinks, submodules
# R099: Renamed (moved), with % of unchanged content. 100 = pure rename
# Not possible in log: C=Copied, U=Unmerged, X=Unknown, B=pairing Broken
status = tokens[0].split(' ')[-1]
status = tokens[0].split(" ")[-1]
file = tokens[-1]
# Handles non-ASCII chars and OS path separator
@@ -419,56 +532,76 @@ def parse_log(filelist, dirlist, stats, git, merge=False, filterlist=None):
def do_file():
if args.skip_older_than_commit and get_mtime_path(file) <= mtime:
stats['skip'] += 1
stats["skip"] += 1
return
if args.debug:
log.debug("%d\t%d\t%d\t%s\t%s",
stats['loglines'], stats['commits'], stats['files'],
datestr, file)
log.debug(
"%d\t%d\t%d\t%s\t%s",
stats["loglines"],
stats["commits"],
stats["files"],
datestr,
file,
)
try:
touch(os.path.join(git.workdir, file), mtime)
stats['touches'] += 1
stats["touches"] += 1
except Exception as e:
log.error("ERROR: %s: %s", e, file)
stats['errors'] += 1
stats["errors"] += 1
def do_dir():
if args.debug:
log.debug("%d\t%d\t-\t%s\t%s",
stats['loglines'], stats['commits'],
datestr, "{}/".format(dirname or '.'))
log.debug(
"%d\t%d\t-\t%s\t%s",
stats["loglines"],
stats["commits"],
datestr,
"{}/".format(dirname or "."),
)
try:
touch(os.path.join(git.workdir, dirname), mtime)
stats['dirtouches'] += 1
stats["dirtouches"] += 1
except Exception as e:
log.error("ERROR: %s: %s", e, dirname)
stats['direrrors'] += 1
stats["direrrors"] += 1
if file in filelist:
stats['files'] -= 1
stats["files"] -= 1
filelist.remove(file)
do_file()
if args.dirs and status in ('A', 'D'):
if args.dirs and status in ("A", "D"):
dirname = os.path.dirname(file)
if dirname in dirlist:
dirlist.remove(dirname)
do_dir()
# All files done?
if not stats['files']:
if not stats["files"]:
git.terminate()
return
# Main Logic ##################################################################
def main():
start = time.time() # yes, Wall time. CPU time is not realistic for users.
stats = {_: 0 for _ in ('loglines', 'commits', 'touches', 'skip', 'errors',
'dirtouches', 'direrrors')}
stats = {
_: 0
for _ in (
"loglines",
"commits",
"touches",
"skip",
"errors",
"dirtouches",
"direrrors",
)
}
logging.basicConfig(level=args.loglevel, format='%(message)s')
logging.basicConfig(level=args.loglevel, format="%(message)s")
log.trace("Arguments: %s", args)
# First things first: Where and Who are we?
@@ -499,13 +632,16 @@ def main():
# Symlink (to file, to dir or broken - git handles the same way)
if not UPDATE_SYMLINKS and os.path.islink(fullpath):
log.warning("WARNING: Skipping symlink, no OS support for updates: %s",
path)
log.warning(
"WARNING: Skipping symlink, no OS support for updates: %s", path
)
continue
# skip files which are older than given threshold
if (args.skip_older_than
and start - get_mtime_path(fullpath) > args.skip_older_than):
if (
args.skip_older_than
and start - get_mtime_path(fullpath) > args.skip_older_than
):
continue
# Always add files relative to worktree root
@@ -519,15 +655,17 @@ def main():
else:
dirty = set(git.ls_dirty())
if dirty:
log.warning("WARNING: Modified files in the working directory were ignored."
"\nTo include such files, commit your changes or use --force.")
log.warning(
"WARNING: Modified files in the working directory were ignored."
"\nTo include such files, commit your changes or use --force."
)
filelist -= dirty
# Build dir list to be processed
dirlist = set(os.path.dirname(_) for _ in filelist) if args.dirs else set()
stats['totalfiles'] = stats['files'] = len(filelist)
log.info("{0:,} files to be processed in work dir".format(stats['totalfiles']))
stats["totalfiles"] = stats["files"] = len(filelist)
log.info("{0:,} files to be processed in work dir".format(stats["totalfiles"]))
if not filelist:
# Nothing to do. Exit silently and without errors, just like git does
@@ -544,10 +682,18 @@ def main():
if args.missing and not args.merge:
filterlist = list(filelist)
missing = len(filterlist)
log.info("{0:,} files not found in log, trying merge commits".format(missing))
log.info(
"{0:,} files not found in log, trying merge commits".format(missing)
)
for i in range(0, missing, STEPMISSING):
parse_log(filelist, dirlist, stats, git,
merge=True, filterlist=filterlist[i:i + STEPMISSING])
parse_log(
filelist,
dirlist,
stats,
git,
merge=True,
filterlist=filterlist[i : i + STEPMISSING],
)
# Still missing some?
for file in filelist:
@@ -556,29 +702,33 @@ def main():
# Final statistics
# Suggestion: use git-log --before=mtime to brag about skipped log entries
def log_info(msg, *a, width=13):
ifmt = '{:%d,}' % (width,) # not using 'n' for consistency with ffmt
ffmt = '{:%d,.2f}' % (width,)
ifmt = "{:%d,}" % (width,) # not using 'n' for consistency with ffmt
ffmt = "{:%d,.2f}" % (width,)
# %-formatting lacks a thousand separator, must pre-render with .format()
log.info(msg.replace('%d', ifmt).replace('%f', ffmt).format(*a))
log.info(msg.replace("%d", ifmt).replace("%f", ffmt).format(*a))
log_info(
"Statistics:\n"
"%f seconds\n"
"%d log lines processed\n"
"%d commits evaluated",
time.time() - start, stats['loglines'], stats['commits'])
"Statistics:\n%f seconds\n%d log lines processed\n%d commits evaluated",
time.time() - start,
stats["loglines"],
stats["commits"],
)
if args.dirs:
if stats['direrrors']: log_info("%d directory update errors", stats['direrrors'])
log_info("%d directories updated", stats['dirtouches'])
if stats["direrrors"]:
log_info("%d directory update errors", stats["direrrors"])
log_info("%d directories updated", stats["dirtouches"])
if stats['touches'] != stats['totalfiles']:
log_info("%d files", stats['totalfiles'])
if stats['skip']: log_info("%d files skipped", stats['skip'])
if stats['files']: log_info("%d files missing", stats['files'])
if stats['errors']: log_info("%d file update errors", stats['errors'])
if stats["touches"] != stats["totalfiles"]:
log_info("%d files", stats["totalfiles"])
if stats["skip"]:
log_info("%d files skipped", stats["skip"])
if stats["files"]:
log_info("%d files missing", stats["files"])
if stats["errors"]:
log_info("%d file update errors", stats["errors"])
log_info("%d files updated", stats['touches'])
log_info("%d files updated", stats["touches"])
if args.test:
log.info("TEST RUN - No files modified!")

View File

@@ -220,7 +220,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
with:
name: dist
path: ${{ inputs.working-directory }}/dist/
@@ -379,7 +379,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
if: startsWith(inputs.working-directory, 'libs/core')
with:
name: dist
@@ -447,7 +447,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
with:
name: dist
path: ${{ inputs.working-directory }}/dist/
@@ -486,7 +486,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
with:
name: dist
path: ${{ inputs.working-directory }}/dist/

View File

@@ -79,4 +79,4 @@ jobs:
# grep will exit non-zero if the target message isn't found,
# and `set -e` above will cause the step to fail.
echo "$STATUS" | grep 'nothing to commit, working tree clean'

View File

@@ -64,4 +64,4 @@ jobs:
# grep will exit non-zero if the target message isn't found,
# and `set -e` above will cause the step to fail.
echo "$STATUS" | grep 'nothing to commit, working tree clean'
echo "$STATUS" | grep 'nothing to commit, working tree clean'

View File

@@ -85,7 +85,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v5
with:
name: test-dist
path: ${{ inputs.working-directory }}/dist/

View File

@@ -52,7 +52,6 @@ jobs:
run: |
# Get unique repositories
REPOS=$(echo "$REPOS_UNSORTED" | sort -u)
# Checkout each unique repository
for repo in $REPOS; do
# Validate repository format (allow any org with proper format)
@@ -68,7 +67,6 @@ jobs:
echo "Error: Invalid repository name: $REPO_NAME"
exit 1
fi
echo "Checking out $repo to $REPO_NAME"
git clone --depth 1 https://github.com/$repo.git $REPO_NAME
done

View File

@@ -30,6 +30,7 @@ jobs:
build:
name: 'Detect Changes & Set Matrix'
runs-on: ubuntu-latest
if: ${{ !contains(github.event.pull_request.labels.*.name, 'ci-ignore') }}
steps:
- name: '📋 Checkout Code'
uses: actions/checkout@v4

View File

@@ -20,6 +20,7 @@ jobs:
codspeed:
name: 'Benchmark'
runs-on: ubuntu-latest
if: ${{ !contains(github.event.pull_request.labels.*.name, 'codspeed-ignore') }}
strategy:
matrix:
include:

View File

@@ -11,4 +11,4 @@
"MD046": {
"style": "fenced"
}
}
}

View File

@@ -21,7 +21,7 @@
"[python]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.organizeImports.ruff": "explicit",
"source.fixAll": "explicit"
},
"editor.defaultFormatter": "charliermarsh.ruff"
@@ -77,4 +77,6 @@
"editor.tabSize": 2,
"editor.insertSpaces": true
},
"python.terminal.activateEnvironment": false,
"python.defaultInterpreterPath": "./.venv/bin/python"
}

View File

@@ -63,4 +63,4 @@ Notebook | Description
[rag-locally-on-intel-cpu.ipynb](https://github.com/langchain-ai/langchain/tree/master/cookbook/rag-locally-on-intel-cpu.ipynb) | Perform Retrieval-Augmented-Generation (RAG) on locally downloaded open-source models using langchain and open source tools and execute it on Intel Xeon CPU. We showed an example of how to apply RAG on Llama 2 model and enable it to answer the queries related to Intel Q1 2024 earnings release.
[visual_RAG_vdms.ipynb](https://github.com/langchain-ai/langchain/tree/master/cookbook/visual_RAG_vdms.ipynb) | Performs Visual Retrieval-Augmented-Generation (RAG) using videos and scene descriptions generated by open source models.
[contextual_rag.ipynb](https://github.com/langchain-ai/langchain/tree/master/cookbook/contextual_rag.ipynb) | Performs contextual retrieval-augmented generation (RAG) prepending chunk-specific explanatory context to each chunk before embedding.
[rag-agents-locally-on-intel-cpu.ipynb](https://github.com/langchain-ai/langchain/tree/master/cookbook/local_rag_agents_intel_cpu.ipynb) | Build a RAG agent locally with open source models that routes questions through one of two paths to find answers. The agent generates answers based on documents retrieved from either the vector database or retrieved from web search. If the vector database lacks relevant information, the agent opts for web search. Open-source models for LLM and embeddings are used locally on an Intel Xeon CPU to execute this pipeline.
[rag-agents-locally-on-intel-cpu.ipynb](https://github.com/langchain-ai/langchain/tree/master/cookbook/local_rag_agents_intel_cpu.ipynb) | Build a RAG agent locally with open source models that routes questions through one of two paths to find answers. The agent generates answers based on documents retrieved from either the vector database or retrieved from web search. If the vector database lacks relevant information, the agent opts for web search. Open-source models for LLM and embeddings are used locally on an Intel Xeon CPU to execute this pipeline.

View File

@@ -97,7 +97,7 @@ def _load_module_members(module_path: str, namespace: str) -> ModuleMembers:
if type(type_) is typing_extensions._TypedDictMeta: # type: ignore
kind: ClassKind = "TypedDict"
elif type(type_) is typing._TypedDictMeta: # type: ignore
kind: ClassKind = "TypedDict"
kind = "TypedDict"
elif (
issubclass(type_, Runnable)
and issubclass(type_, BaseModel)
@@ -189,7 +189,7 @@ def _load_package_modules(
if isinstance(package_directory, str)
else package_directory
)
modules_by_namespace = {}
modules_by_namespace: Dict[str, ModuleMembers] = {}
# Get the high level package name
package_name = package_path.name
@@ -217,7 +217,11 @@ def _load_package_modules(
# Get the full namespace of the module
namespace = str(relative_module_name).replace(".py", "").replace("/", ".")
# Keep only the top level namespace
top_namespace = namespace.split(".")[0]
# (but make special exception for content_blocks and v1.messages)
if namespace == "messages.content_blocks" or namespace == "v1.messages":
top_namespace = namespace # Keep full namespace for content_blocks
else:
top_namespace = namespace.split(".")[0]
try:
# If submodule is present, we need to construct the paths in a slightly
@@ -283,7 +287,7 @@ def _construct_doc(
.. toctree::
:hidden:
:maxdepth: 2
"""
index_autosummary = """
"""
@@ -365,9 +369,9 @@ def _construct_doc(
module_doc += f"""\
:template: {template}
{class_["qualified_name"]}
"""
index_autosummary += f"""
{class_["qualified_name"]}
@@ -550,8 +554,8 @@ def _build_index(dirs: List[str]) -> None:
integrations = sorted(dir_ for dir_ in dirs if dir_ not in main_)
doc = """# LangChain Python API Reference
Welcome to the LangChain Python API reference. This is a reference for all
`langchain-x` packages.
Welcome to the LangChain Python API reference. This is a reference for all
`langchain-x` packages.
For user guides see [https://python.langchain.com](https://python.langchain.com).

View File

@@ -1,4 +1,4 @@
# Async programming with langchain
# Async programming with LangChain
:::info Prerequisites
* [Runnable interface](/docs/concepts/runnables)
@@ -12,7 +12,7 @@ You are expected to be familiar with asynchronous programming in Python before r
This guide specifically focuses on what you need to know to work with LangChain in an asynchronous context, assuming that you are already familiar with asynchronous programming.
:::
## Langchain asynchronous APIs
## LangChain asynchronous APIs
Many LangChain APIs are designed to be asynchronous, allowing you to build efficient and responsive applications.

View File

@@ -31,7 +31,7 @@ The key attributes that correspond to the tool's **schema**:
The key methods to execute the function associated with the **tool**:
- **invoke**: Invokes the tool with the given arguments.
- **ainvoke**: Invokes the tool with the given arguments, asynchronously. Used for [async programming with Langchain](/docs/concepts/async).
- **ainvoke**: Invokes the tool with the given arguments, asynchronously. Used for [async programming with LangChain](/docs/concepts/async).
## Create tools using the `@tool` decorator

View File

@@ -124,6 +124,47 @@ start "" htmlcov/index.html || open htmlcov/index.html
```
## Snapshot Testing
Some tests use [syrupy](https://github.com/tophat/syrupy) for snapshot testing, which captures the output of functions and compares them to stored snapshots. This is particularly useful for testing JSON schema generation and other structured outputs.
### Updating Snapshots
To update snapshots when the expected output has legitimately changed:
```bash
uv run --group test pytest path/to/test.py --snapshot-update
```
### Pydantic Version Compatibility Issues
Pydantic generates different JSON schemas across versions, which can cause snapshot test failures in CI when tests run with different Pydantic versions than what was used to generate the snapshots.
**Symptoms:**
- CI fails with snapshot mismatches showing differences like missing or extra fields.
- Tests pass locally but fail in CI with different Pydantic versions
**Solution:**
Locally update snapshots using the same Pydantic version that CI uses:
1. **Identify the failing Pydantic version** from CI logs (e.g., `2.7.0`, `2.8.0`, `2.9.0`)
2. **Update snapshots with that version:**
```bash
uv run --with "pydantic==2.9.0" --group test pytest tests/unit_tests/path/to/test.py::test_name --snapshot-update
```
3. **Verify compatibility across supported versions:**
```bash
# Test with the version you used to update
uv run --with "pydantic==2.9.0" --group test pytest tests/unit_tests/path/to/test.py::test_name
# Test with other supported versions
uv run --with "pydantic==2.8.0" --group test pytest tests/unit_tests/path/to/test.py::test_name
```
**Note:** Some tests use `@pytest.mark.skipif` decorators to only run with specific Pydantic version ranges (e.g., `PYDANTIC_VERSION_AT_LEAST_210`). Make sure to understand these constraints when updating snapshots.
## Coverage
Code coverage (i.e. the amount of code that is covered by unit tests) helps identify areas of the code that are potentially more or less brittle.

View File

@@ -122,13 +122,13 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"from langchain_experimental.graph_transformers import LLMGraphTransformer\n",
"# from langchain_experimental.graph_transformers import LLMGraphTransformer\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"llm = ChatOpenAI(temperature=0, model_name=\"gpt-4-turbo\")\n",

View File

@@ -34,7 +34,7 @@ These are the core building blocks you can use when building applications.
[Chat Models](/docs/concepts/chat_models) are newer forms of language models that take messages in and output a message.
See [supported integrations](/docs/integrations/chat/) for details on getting started with chat models from a specific provider.
- [How to: init any model in one line](/docs/how_to/chat_models_universal_init/)
- [How to: initialize any model in one line](/docs/how_to/chat_models_universal_init/)
- [How to: work with local models](/docs/how_to/local_llms)
- [How to: do function/tool calling](/docs/how_to/tool_calling)
- [How to: get models to return structured output](/docs/how_to/structured_output)

View File

@@ -45,8 +45,8 @@
"A few frameworks for this have emerged to support inference of open-source LLMs on various devices:\n",
"\n",
"1. [`llama.cpp`](https://github.com/ggerganov/llama.cpp): C++ implementation of llama inference code with [weight optimization / quantization](https://finbarr.ca/how-is-llama-cpp-possible/)\n",
"2. [`gpt4all`](https://docs.gpt4all.io/index.html): Optimized C backend for inference\n",
"3. [`Ollama`](https://ollama.ai/): Bundles model weights and environment into an app that runs on device and serves the LLM\n",
"2. [`gpt4all`](https://github.com/nomic-ai/gpt4all): Optimized C backend for inference\n",
"3. [`ollama`](https://github.com/ollama/ollama): Bundles model weights and environment into an app that runs on device and serves the LLM\n",
"4. [`llamafile`](https://github.com/Mozilla-Ocho/llamafile): Bundles model weights and everything needed to run the model in a single file, allowing you to run the LLM locally from this file without any additional installation steps\n",
"\n",
"In general, these frameworks will do a few things:\n",
@@ -74,12 +74,12 @@
"\n",
"## Quickstart\n",
"\n",
"[`Ollama`](https://ollama.ai/) is one way to easily run inference on macOS.\n",
"[Ollama](https://ollama.ai/) is one way to easily run inference on macOS.\n",
" \n",
"The instructions [here](https://github.com/jmorganca/ollama?tab=readme-ov-file#ollama) provide details, which we summarize:\n",
"The instructions [here](https://github.com/ollama/ollama?tab=readme-ov-file#ollama) provide details, which we summarize:\n",
" \n",
"* [Download and run](https://ollama.ai/download) the app\n",
"* From command line, fetch a model from this [list of options](https://github.com/jmorganca/ollama): e.g., `ollama pull llama3.1:8b`\n",
"* From command line, fetch a model from this [list of options](https://ollama.com/search): e.g., `ollama pull llama3.1:8b`\n",
"* When the app is running, all models are automatically served on `localhost:11434`\n"
]
},
@@ -111,11 +111,11 @@
}
],
"source": [
"from langchain_ollama import OllamaLLM\n",
"from langchain_ollama import ChatOllama\n",
"\n",
"llm = OllamaLLM(model=\"llama3.1:8b\")\n",
"llm = ChatOllama(model=\"gpt-oss:20b\")\n",
"\n",
"llm.invoke(\"The first man on the moon was ...\")"
"llm.invoke(\"The first man on the moon was ...\").content"
]
},
{
@@ -149,40 +149,7 @@
],
"source": [
"for chunk in llm.stream(\"The first man on the moon was ...\"):\n",
" print(chunk, end=\"|\", flush=True)"
]
},
{
"cell_type": "markdown",
"id": "e5731060",
"metadata": {},
"source": [
"Ollama also includes a chat model wrapper that handles formatting conversation turns:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "f14a778a",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"AIMessage(content='The answer is a historic one!\\n\\nThe first man to walk on the Moon was Neil Armstrong, an American astronaut and commander of the Apollo 11 mission. On July 20, 1969, Armstrong stepped out of the lunar module Eagle onto the surface of the Moon, famously declaring:\\n\\n\"That\\'s one small step for man, one giant leap for mankind.\"\\n\\nArmstrong was followed by fellow astronaut Edwin \"Buzz\" Aldrin, who also walked on the Moon during the mission. Michael Collins remained in orbit around the Moon in the command module Columbia.\\n\\nNeil Armstrong passed away on August 25, 2012, but his legacy as a pioneering astronaut and engineer continues to inspire people around the world!', response_metadata={'model': 'llama3.1:8b', 'created_at': '2024-08-01T00:38:29.176717Z', 'message': {'role': 'assistant', 'content': ''}, 'done_reason': 'stop', 'done': True, 'total_duration': 10681861417, 'load_duration': 34270292, 'prompt_eval_count': 19, 'prompt_eval_duration': 6209448000, 'eval_count': 141, 'eval_duration': 4432022000}, id='run-7bed57c5-7f54-4092-912c-ae49073dcd48-0', usage_metadata={'input_tokens': 19, 'output_tokens': 141, 'total_tokens': 160})"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from langchain_ollama import ChatOllama\n",
"\n",
"chat_model = ChatOllama(model=\"llama3.1:8b\")\n",
"\n",
"chat_model.invoke(\"Who was the first man on the moon?\")"
" print(chunk.text(), end=\"|\", flush=True)"
]
},
{
@@ -200,7 +167,7 @@
"\n",
"### Running Apple silicon GPU\n",
"\n",
"`Ollama` and [`llamafile`](https://github.com/Mozilla-Ocho/llamafile?tab=readme-ov-file#gpu-support) will automatically utilize the GPU on Apple devices.\n",
"`ollama` and [`llamafile`](https://github.com/Mozilla-Ocho/llamafile?tab=readme-ov-file#gpu-support) will automatically utilize the GPU on Apple devices.\n",
" \n",
"Other frameworks require the user to set up the environment to utilize the Apple GPU.\n",
"\n",
@@ -212,15 +179,15 @@
"\n",
"In particular, ensure that conda is using the correct virtual environment that you created (`miniforge3`).\n",
"\n",
"E.g., for me:\n",
"e.g.,\n",
"\n",
"```\n",
"```shell\n",
"conda activate /Users/rlm/miniforge3/envs/llama\n",
"```\n",
"\n",
"With the above confirmed, then:\n",
"\n",
"```\n",
"```shell\n",
"CMAKE_ARGS=\"-DLLAMA_METAL=on\" FORCE_CMAKE=1 pip install -U llama-cpp-python --no-cache-dir\n",
"```"
]
@@ -234,17 +201,13 @@
"\n",
"There are various ways to gain access to quantized model weights.\n",
"\n",
"1. [`HuggingFace`](https://huggingface.co/TheBloke) - Many quantized model are available for download and can be run with framework such as [`llama.cpp`](https://github.com/ggerganov/llama.cpp). You can also download models in [`llamafile` format](https://huggingface.co/models?other=llamafile) from HuggingFace.\n",
"2. [`gpt4all`](https://gpt4all.io/index.html) - The model explorer offers a leaderboard of metrics and associated quantized models available for download \n",
"3. [`Ollama`](https://github.com/jmorganca/ollama) - Several models can be accessed directly via `pull`\n",
"1. [HuggingFace](https://huggingface.co/TheBloke) - Many quantized model are available for download and can be run with framework such as [`llama.cpp`](https://github.com/ggerganov/llama.cpp). You can also download models in [`llamafile` format](https://huggingface.co/models?other=llamafile) from HuggingFace.\n",
"2. [gpt4all](https://gpt4all.io/index.html) - The model explorer offers a leaderboard of metrics and associated quantized models available for download \n",
"3. [ollama](https://github.com/ollama/ollama) - Several models can be accessed directly via `pull`\n",
"\n",
"### Ollama\n",
"\n",
"With [Ollama](https://github.com/jmorganca/ollama), fetch a model via `ollama pull <model family>:<tag>`:\n",
"\n",
"* E.g., for Llama 2 7b: `ollama pull llama2` will download the most basic version of the model (e.g., smallest # parameters and 4 bit quantization)\n",
"* We can also specify a particular version from the [model list](https://github.com/jmorganca/ollama?tab=readme-ov-file#model-library), e.g., `ollama pull llama2:13b`\n",
"* See the full set of parameters on the [API reference page](https://python.langchain.com/api_reference/community/llms/langchain_community.llms.ollama.Ollama.html)"
"With [Ollama](https://github.com/ollama/ollama), fetch a model via `ollama pull <model family>:<tag>`:"
]
},
{
@@ -265,7 +228,7 @@
}
],
"source": [
"llm = OllamaLLM(model=\"llama2:13b\")\n",
"llm = ChatOllama(model=\"gpt-oss:20b\")\n",
"llm.invoke(\"The first man on the moon was ... think step by step\")"
]
},
@@ -684,12 +647,6 @@
"\n",
"In addition, [here](https://blog.langchain.dev/using-langsmith-to-support-fine-tuning-of-open-source-llms/) is an overview on fine-tuning, which can utilize open-source LLMs."
]
},
{
"cell_type": "markdown",
"id": "14c2c170",
"metadata": {},
"source": []
}
],
"metadata": {

View File

@@ -15,7 +15,7 @@
"id": "f2195672-0cab-4967-ba8a-c6544635547d",
"metadata": {},
"source": [
"# How deal with high cardinality categoricals when doing query analysis\n",
"# How to deal with high-cardinality categoricals when doing query analysis\n",
"\n",
"You may want to do query analysis to create a filter on a categorical column. One of the difficulties here is that you usually need to specify the EXACT categorical value. The issue is you need to make sure the LLM generates that categorical value exactly. This can be done relatively easy with prompting when there are only a few values that are valid. When there are a high number of valid values then it becomes more difficult, as those values may not fit in the LLM context, or (if they do) there may be too many for the LLM to properly attend to.\n",
"\n",

View File

@@ -74,12 +74,12 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": null,
"id": "a88ff70c",
"metadata": {},
"outputs": [],
"source": [
"from langchain_experimental.text_splitter import SemanticChunker\n",
"# from langchain_experimental.text_splitter import SemanticChunker\n",
"from langchain_openai.embeddings import OpenAIEmbeddings\n",
"\n",
"text_splitter = SemanticChunker(OpenAIEmbeddings())"

View File

@@ -612,56 +612,11 @@
},
{
"cell_type": "code",
"execution_count": 18,
"execution_count": null,
"id": "35ea904e-795f-411b-bef8-6484dbb6e35c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"\n",
"\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
"\u001b[32;1m\u001b[1;3m\n",
"Invoking: `python_repl_ast` with `{'query': \"df[['Age', 'Fare']].corr().iloc[0,1]\"}`\n",
"\n",
"\n",
"\u001b[0m\u001b[36;1m\u001b[1;3m0.11232863699941621\u001b[0m\u001b[32;1m\u001b[1;3m\n",
"Invoking: `python_repl_ast` with `{'query': \"df[['Fare', 'Survived']].corr().iloc[0,1]\"}`\n",
"\n",
"\n",
"\u001b[0m\u001b[36;1m\u001b[1;3m0.2561785496289603\u001b[0m\u001b[32;1m\u001b[1;3mThe correlation between Age and Fare is approximately 0.112, and the correlation between Fare and Survival is approximately 0.256.\n",
"\n",
"Therefore, the correlation between Fare and Survival (0.256) is greater than the correlation between Age and Fare (0.112).\u001b[0m\n",
"\n",
"\u001b[1m> Finished chain.\u001b[0m\n"
]
},
{
"data": {
"text/plain": [
"{'input': \"What's the correlation between age and fare? is that greater than the correlation between fare and survival?\",\n",
" 'output': 'The correlation between Age and Fare is approximately 0.112, and the correlation between Fare and Survival is approximately 0.256.\\n\\nTherefore, the correlation between Fare and Survival (0.256) is greater than the correlation between Age and Fare (0.112).'}"
]
},
"execution_count": 18,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from langchain_experimental.agents import create_pandas_dataframe_agent\n",
"\n",
"agent = create_pandas_dataframe_agent(\n",
" llm, df, agent_type=\"openai-tools\", verbose=True, allow_dangerous_code=True\n",
")\n",
"agent.invoke(\n",
" {\n",
" \"input\": \"What's the correlation between age and fare? is that greater than the correlation between fare and survival?\"\n",
" }\n",
")"
]
"outputs": [],
"source": "from langchain_experimental.agents import create_pandas_dataframe_agent\n\nagent = create_pandas_dataframe_agent(\n llm, df, agent_type=\"openai-tools\", verbose=True, allow_dangerous_code=True\n)\nagent.invoke(\n {\n \"input\": \"What's the correlation between age and fare? is that greater than the correlation between fare and survival?\"\n }\n)"
},
{
"cell_type": "markdown",
@@ -786,4 +741,4 @@
},
"nbformat": 4,
"nbformat_minor": 5
}
}

View File

@@ -614,6 +614,7 @@
" HumanMessage(\"Now about caterpillars\", name=\"example_user\"),\n",
" AIMessage(\n",
" \"\",\n",
" name=\"example_assistant\",\n",
" tool_calls=[\n",
" {\n",
" \"name\": \"joke\",\n",
@@ -909,7 +910,7 @@
" ),\n",
" (\"human\", \"{query}\"),\n",
" ]\n",
").partial(schema=People.schema())\n",
").partial(schema=People.model_json_schema())\n",
"\n",
"\n",
"# Custom parser\n",

File diff suppressed because it is too large Load Diff

View File

@@ -17,25 +17,29 @@
"source": [
"# ChatOllama\n",
"\n",
"[Ollama](https://ollama.ai/) allows you to run open-source large language models, such as Llama 2, locally.\n",
"[Ollama](https://ollama.com/) allows you to run open-source large language models, such as `gpt-oss`, locally.\n",
"\n",
"Ollama bundles model weights, configuration, and data into a single package, defined by a Modelfile.\n",
"`ollama` bundles model weights, configuration, and data into a single package, defined by a Modelfile.\n",
"\n",
"It optimizes setup and configuration details, including GPU usage.\n",
"\n",
"For a complete list of supported models and model variants, see the [Ollama model library](https://github.com/jmorganca/ollama#model-library).\n",
"For a complete list of supported models and model variants, see the [Ollama model library](https://ollama.com/search).\n",
"\n",
":::warning\n",
"This page is for the new v1 `ChatOllama` class with standard content block output. If you are looking for the legacy v0 `Ollama` class, see the [v0.3 documentation](https://python.langchain.com/v0.3/docs/integrations/chat/ollama/).\n",
":::\n",
"\n",
"## Overview\n",
"### Integration details\n",
"\n",
"| Class | Package | Local | Serializable | [JS support](https://js.langchain.com/v0.2/docs/integrations/chat/ollama) | Package downloads | Package latest |\n",
"| Class | Package | Local | Serializable | [JS support](https://js.langchain.com/docs/integrations/chat/ollama/) | Package downloads | Package latest |\n",
"| :--- | :--- | :---: | :---: | :---: | :---: | :---: |\n",
"| [ChatOllama](https://python.langchain.com/v0.2/api_reference/ollama/chat_models/langchain_ollama.chat_models.ChatOllama.html) | [langchain-ollama](https://python.langchain.com/v0.2/api_reference/ollama/index.html) | ✅ | ❌ | ✅ | ![PyPI - Downloads](https://img.shields.io/pypi/dm/langchain-ollama?style=flat-square&label=%20) | ![PyPI - Version](https://img.shields.io/pypi/v/langchain-ollama?style=flat-square&label=%20) |\n",
"| [ChatOllama](https://python.langchain.com/api_reference/ollama/chat_models/langchain_ollama.chat_models.ChatOllama.html#chatollama) | [langchain-ollama](https://python.langchain.com/api_reference/ollama/index.html) | ✅ | ❌ | ✅ | ![PyPI - Downloads](https://img.shields.io/pypi/dm/langchain-ollama?style=flat-square&label=%20) | ![PyPI - Version](https://img.shields.io/pypi/v/langchain-ollama?style=flat-square&label=%20) |\n",
"\n",
"### Model features\n",
"| [Tool calling](/docs/how_to/tool_calling/) | [Structured output](/docs/how_to/structured_output/) | JSON mode | [Image input](/docs/how_to/multimodal_inputs/) | Audio input | Video input | [Token-level streaming](/docs/how_to/chat_streaming/) | Native async | [Token usage](/docs/how_to/chat_token_usage_tracking/) | [Logprobs](/docs/how_to/logprobs/) |\n",
"| :---: |:----------------------------------------------------:| :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: |\n",
"| ✅ | ✅ | ✅ | | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ |\n",
"| ✅ | ✅ | ✅ | | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ |\n",
"\n",
"## Setup\n",
"\n",
@@ -45,17 +49,17 @@
" * macOS users can install via Homebrew with `brew install ollama` and start with `brew services start ollama`\n",
"* Fetch available LLM model via `ollama pull <name-of-model>`\n",
" * View a list of available models via the [model library](https://ollama.ai/library)\n",
" * e.g., `ollama pull llama3`\n",
" * e.g., `ollama pull gpt-oss:20b`\n",
"* This will download the default tagged version of the model. Typically, the default points to the latest, smallest sized-parameter model.\n",
"\n",
"> On Mac, the models will be download to `~/.ollama/models`\n",
">\n",
"> On Linux (or WSL), the models will be stored at `/usr/share/ollama/.ollama/models`\n",
"\n",
"* Specify the exact version of the model of interest as such `ollama pull vicuna:13b-v1.5-16k-q4_0` (View the [various tags for the `Vicuna`](https://ollama.ai/library/vicuna/tags) model in this instance)\n",
"* Specify the exact version of the model of interest as such `ollama pull gpt-oss:20b`\n",
"* To view all pulled models, use `ollama list`\n",
"* To chat directly with a model from the command line, use `ollama run <name-of-model>`\n",
"* View the [Ollama documentation](https://github.com/ollama/ollama/tree/main/docs) for more commands. You can run `ollama help` in the terminal to see available commands.\n"
"* View the [Ollama documentation](https://github.com/ollama/ollama/blob/main/docs/README.md) for more commands. You can run `ollama help` in the terminal to see available commands.\n"
]
},
{
@@ -102,7 +106,11 @@
"id": "b18bd692076f7cf7",
"metadata": {},
"source": [
"Make sure you're using the latest Ollama version for structured outputs. Update by running:"
":::warning\n",
"Make sure you're using the latest Ollama client version!\n",
":::\n",
"\n",
"Update by running:"
]
},
{
@@ -127,15 +135,16 @@
},
{
"cell_type": "code",
"execution_count": 9,
"execution_count": 2,
"id": "cb09c344-1836-4e0c-acf8-11d13ac1dbae",
"metadata": {},
"outputs": [],
"source": [
"from langchain_ollama import ChatOllama\n",
"from langchain_ollama.v1 import ChatOllama\n",
"\n",
"llm = ChatOllama(\n",
" model=\"llama3.1\",\n",
" model=\"gpt-oss:20b\",\n",
" validate_model_on_init=True,\n",
" temperature=0,\n",
" # other params...\n",
")"
@@ -158,46 +167,56 @@
},
"outputs": [
{
"data": {
"text/plain": [
"AIMessage(content='The translation of \"I love programming\" in French is:\\n\\n\"J\\'adore le programmation.\"', additional_kwargs={}, response_metadata={'model': 'llama3.1', 'created_at': '2025-06-25T18:43:00.483666Z', 'done': True, 'done_reason': 'stop', 'total_duration': 619971208, 'load_duration': 27793125, 'prompt_eval_count': 35, 'prompt_eval_duration': 36354583, 'eval_count': 22, 'eval_duration': 555182667, 'model_name': 'llama3.1'}, id='run--348bb5ef-9dd9-4271-bc7e-a9ddb54c28c1-0', usage_metadata={'input_tokens': 35, 'output_tokens': 22, 'total_tokens': 57})"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
"name": "stdout",
"output_type": "stream",
"text": [
"AIMessage(type='ai', name=None, id='lc_run--5521db11-a5eb-4e46-956c-1455151cdaa3-0', lc_version='v1', content=[{'type': 'text', 'text': 'The translation of \"I love programming\" to French is:\\n\\n\"Je aime le programmation\"\\n\\nHowever, a more common and idiomatic way to express this in French would be:\\n\\n\"J\\'aime programmer\"\\n\\nThis phrase uses the verb \"aimer\" (to love) in the present tense, which is more suitable for expressing a general feeling or preference.'}], usage_metadata={'input_tokens': 34, 'output_tokens': 73, 'total_tokens': 107}, response_metadata={'model_name': 'llama3.2', 'created_at': '2025-08-08T23:07:44.439483Z', 'done': True, 'done_reason': 'stop', 'total_duration': 1410566833, 'load_duration': 28419542, 'prompt_eval_count': 34, 'prompt_eval_duration': 141642125, 'eval_count': 73, 'eval_duration': 1240075000}, parsed=None)\n",
"\n",
"Content:\n",
"The translation of \"I love programming\" to French is:\n",
"\n",
"\"Je aime le programmation\"\n",
"\n",
"However, a more common and idiomatic way to express this in French would be:\n",
"\n",
"\"J'aime programmer\"\n",
"\n",
"This phrase uses the verb \"aimer\" (to love) in the present tense, which is more suitable for expressing a general feeling or preference.\n"
]
}
],
"source": [
"messages = [\n",
" (\n",
" \"system\",\n",
" \"You are a helpful assistant that translates English to French. Translate the user sentence.\",\n",
" ),\n",
" (\"human\", \"I love programming.\"),\n",
"]\n",
"ai_msg = llm.invoke(messages)\n",
"ai_msg"
"ai_msg = llm.invoke(\"Translate 'I love programming' to French.\")\n",
"print(f\"{ai_msg}\\n\")\n",
"print(f\"Content:\\n{ai_msg.text}\")"
]
},
{
"cell_type": "markdown",
"id": "ede35e47",
"metadata": {},
"source": [
"## Streaming"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "d86145b3-bfef-46e8-b227-4dda5c9c2705",
"execution_count": 10,
"id": "77474829",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"The translation of \"I love programming\" in French is:\n",
"\n",
"\"J'adore le programmation.\"\n"
"Hi| there|!| I|'m| just| a| chat|bot|,| so| I| don|'t| have| feelings|,| but| I|'m| here| and| ready| to| help| you| with| anything| you| need|!| How| can| I| assist| you| today|?| 😊|"
]
}
],
"source": [
"print(ai_msg.content)"
"for chunk in llm.stream(\"How are you doing?\"):\n",
" if chunk.text:\n",
" print(chunk.text, end=\"|\", flush=True)"
]
},
{
@@ -219,10 +238,10 @@
{
"data": {
"text/plain": [
"AIMessage(content='\"Programmieren ist meine Leidenschaft.\"\\n\\n(I translated \"programming\" to the German word \"Programmieren\", and added \"ist meine Leidenschaft\" which means \"is my passion\")', additional_kwargs={}, response_metadata={'model': 'llama3.1', 'created_at': '2025-06-25T18:43:29.350032Z', 'done': True, 'done_reason': 'stop', 'total_duration': 1194744459, 'load_duration': 26982500, 'prompt_eval_count': 30, 'prompt_eval_duration': 117043458, 'eval_count': 41, 'eval_duration': 1049892167, 'model_name': 'llama3.1'}, id='run--efc6436e-2346-43d9-8118-3c20b3cdf0d0-0', usage_metadata={'input_tokens': 30, 'output_tokens': 41, 'total_tokens': 71})"
"'Ich liebe Programmierung.'"
]
},
"execution_count": 7,
"execution_count": 19,
"metadata": {},
"output_type": "execute_result"
}
@@ -241,13 +260,15 @@
")\n",
"\n",
"chain = prompt | llm\n",
"chain.invoke(\n",
"result = chain.invoke(\n",
" {\n",
" \"input_language\": \"English\",\n",
" \"output_language\": \"German\",\n",
" \"input\": \"I love programming.\",\n",
" }\n",
")"
")\n",
"\n",
"result.text"
]
},
{
@@ -257,10 +278,10 @@
"source": [
"## Tool calling\n",
"\n",
"We can use [tool calling](/docs/concepts/tool_calling/) with an LLM [that has been fine-tuned for tool use](https://ollama.com/search?&c=tools) such as `llama3.1`:\n",
"We can use [tool calling](/docs/concepts/tool_calling/) with an LLM [that has been fine-tuned for tool use](https://ollama.com/search?&c=tools) such as `gpt-oss`:\n",
"\n",
"```\n",
"ollama pull llama3.1\n",
"ollama pull gpt-oss:20b\n",
"```\n",
"\n",
"Details on creating custom tools are available in [this guide](/docs/how_to/custom_tools/). Below, we demonstrate how to create a tool using the `@tool` decorator on a normal python function."
@@ -276,16 +297,16 @@
"name": "stdout",
"output_type": "stream",
"text": [
"[{'name': 'validate_user', 'args': {'addresses': ['123 Fake St, Boston, MA', '234 Pretend Boulevard, Houston, TX'], 'user_id': '123'}, 'id': 'aef33a32-a34b-4b37-b054-e0d85584772f', 'type': 'tool_call'}]\n"
"[{'type': 'tool_call', 'id': 'f365489e-1dc4-4d60-aaff-e56290ae4f99', 'name': 'validate_user', 'args': {'addresses': ['123 Fake St in Boston MA', '234 Pretend Boulevard in Houston TX'], 'user_id': 123}}]\n"
]
}
],
"source": [
"from typing import List\n",
"\n",
"from langchain_core.messages import AIMessage\n",
"from langchain_core.v1.messages import AIMessage\n",
"from langchain_core.tools import tool\n",
"from langchain_ollama import ChatOllama\n",
"from langchain_ollama.v1 import ChatOllama\n",
"\n",
"\n",
"@tool\n",
@@ -300,7 +321,8 @@
"\n",
"\n",
"llm = ChatOllama(\n",
" model=\"llama3.1\",\n",
" model=\"gpt-oss:20b\",\n",
" validate_model_on_init=True,\n",
" temperature=0,\n",
").bind_tools([validate_user])\n",
"\n",
@@ -314,6 +336,50 @@
" print(result.tool_calls)"
]
},
{
"cell_type": "markdown",
"id": "4321b6a8",
"metadata": {},
"source": [
"## Structured output"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "20f8ae70",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Name: Alice, Age: 28, Job: Software Engineer\n"
]
}
],
"source": [
"from langchain_ollama.v1 import ChatOllama\n",
"from pydantic import BaseModel, Field\n",
"\n",
"llm = ChatOllama(model=\"llama3.2\", validate_model_on_init=True, temperature=0)\n",
"\n",
"\n",
"class Person(BaseModel):\n",
" \"\"\"Information about a person.\"\"\"\n",
"\n",
" name: str = Field(description=\"The person's full name\")\n",
" age: int = Field(description=\"The person's age in years\")\n",
" occupation: str = Field(description=\"The person's job or profession\")\n",
"\n",
"\n",
"structured_llm = llm.with_structured_output(Person)\n",
"response: Person = structured_llm.invoke(\n",
" \"Tell me about a fictional software engineer named Alice who is 28 years old.\"\n",
")\n",
"print(f\"Name: {response.name}, Age: {response.age}, Job: {response.occupation}\")"
]
},
{
"cell_type": "markdown",
"id": "4c5e0197",
@@ -321,11 +387,9 @@
"source": [
"## Multi-modal\n",
"\n",
"Ollama has support for multi-modal LLMs, such as [bakllava](https://ollama.com/library/bakllava) and [llava](https://ollama.com/library/llava).\n",
"Ollama has limited support for multi-modal LLMs, such as [gemma3](https://ollama.com/library/gemma3).\n",
"\n",
" ollama pull bakllava\n",
"\n",
"Be sure to update Ollama so that you have the most recent version to support multi-modal."
"### Image input"
]
},
{
@@ -408,15 +472,15 @@
"name": "stdout",
"output_type": "stream",
"text": [
"90%\n"
"Based on the image, the dollar-based gross retention rate is **90%**.\n"
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"from langchain_ollama import ChatOllama\n",
"from langchain_core.v1.messages import HumanMessage\n",
"from langchain_ollama.v1 import ChatOllama\n",
"\n",
"llm = ChatOllama(model=\"bakllava\", temperature=0)\n",
"llm = ChatOllama(model=\"gemma3:4b\", validate_model_on_init=True, temperature=0)\n",
"\n",
"\n",
"def prompt_func(data):\n",
@@ -424,8 +488,9 @@
" image = data[\"image\"]\n",
"\n",
" image_part = {\n",
" \"type\": \"image_url\",\n",
" \"image_url\": f\"data:image/jpeg;base64,{image}\",\n",
" \"type\": \"image\",\n",
" \"base64\": f\"data:image/jpeg;base64,{image}\",\n",
" \"mime_type\": \"image/jpeg\",\n",
" }\n",
"\n",
" content_parts = []\n",
@@ -435,7 +500,7 @@
" content_parts.append(image_part)\n",
" content_parts.append(text_part)\n",
"\n",
" return [HumanMessage(content=content_parts)]\n",
" return [HumanMessage(content_parts)]\n",
"\n",
"\n",
"from langchain_core.output_parsers import StrOutputParser\n",
@@ -454,11 +519,9 @@
"id": "fb6a331f-1507-411f-89e5-c4d598154f3c",
"metadata": {},
"source": [
"## Reasoning models and custom message roles\n",
"## Reasoning models\n",
"\n",
"Some models, such as IBM's [Granite 3.2](https://ollama.com/library/granite3.2), support custom message roles to enable thinking processes.\n",
"\n",
"To access Granite 3.2's thinking features, pass a message with a `\"control\"` role with content set to `\"thinking\"`. Because `\"control\"` is a non-standard message role, we can use a [ChatMessage](https://python.langchain.com/api_reference/core/messages/langchain_core.messages.chat.ChatMessage.html) object to implement it:"
"Many models support outputting their reasoning process in addition to the final answer. This is useful for debugging and understanding how the model arrived at its conclusion. This train of thought reasoning is available in models such as `gpt-oss`, `qwen3:8b`, and `deepseek-r1`. To enable reasoning output, set the `reasoning` parameter to `True` either when instantiating the model or during invocation."
]
},
{
@@ -471,30 +534,25 @@
"name": "stdout",
"output_type": "stream",
"text": [
"Here is my thought process:\n",
"The user is asking for the value of 3 raised to the power of 3, which is a basic exponentiation operation.\n",
"\n",
"Here is my response:\n",
"\n",
"3^3 (read as \"3 to the power of 3\") equals 27. \n",
"\n",
"This calculation is performed by multiplying 3 by itself three times: 3*3*3 = 27.\n"
"Response including reasoning: [{'type': 'reasoning', 'reasoning': \"Okay, so I need to figure out what 3^3 is. Let me start by recalling what exponents mean. From what I remember, when you have a number raised to a power, like a^b, it means you multiply the number by itself b times. So, for example, 2^3 would be 2 multiplied by itself three times: 2 × 2 × 2. Let me check if that's right. Yeah, I think that's correct. So applying that to 3^3, it should be 3 multiplied by itself three times.\\n\\nWait, let me make sure I'm not confusing the base and the exponent. The base is the number being multiplied, and the exponent is how many times it's multiplied. So in 3^3, the base is 3 and the exponent is 3. That means I need to multiply 3 by itself three times. Let me write that out step by step.\\n\\nFirst, multiply the first two 3s: 3 × 3. What's 3 times 3? That's 9. Okay, so the first multiplication gives me 9. Now, I need to multiply that result by the third 3. So 9 × 3. Let me calculate that. 9 times 3 is... 27. So putting it all together, 3 × 3 × 3 equals 27. \\n\\nWait, let me verify that again. Maybe I should do it in a different way to make sure I didn't make a mistake. Let's break it down. 3^3 is the same as 3 × 3 × 3. Let me compute 3 × 3 first, which is 9, and then multiply that by 3. 9 × 3 is indeed 27. Hmm, that seems right. \\n\\nAlternatively, I can think of exponents as repeated multiplication. So 3^1 is 3, 3^2 is 3 × 3 = 9, and 3^3 is 3 × 3 × 3 = 27. Yeah, that progression makes sense. Each time the exponent increases by 1, you multiply by the base again. So starting from 3^1 = 3, then 3^2 is 3 × 3 = 9, then 3^3 is 9 × 3 = 27. \\n\\nIs there another way to check this? Maybe using exponent rules. For example, if I know that 3^2 is 9, then multiplying by another 3 would give me 3^3. Since 9 × 3 is 27, that confirms it again. \\n\\nAlternatively, maybe I can use logarithms or something else, but that might be overcomplicating. Since exponents are straightforward multiplication, I think my initial calculation is correct. \\n\\nWait, just to be thorough, maybe I can use a calculator to verify. Let me imagine pressing 3, then the exponent key, then 3. If I do that, it should give me 27. Yeah, that's what I remember. So all methods point to 27. \\n\\nI think I've checked it multiple ways: breaking down the multiplication step by step, using the exponent progression, and even considering a calculator verification. All of them lead to the same answer. Therefore, I'm confident that 3^3 equals 27.\\n\"}, {'type': 'text', 'text': 'To determine the value of $3^3$, we start by understanding what an exponent represents. The expression $a^b$ means multiplying the base $a$ by itself $b$ times. \\n\\n### Step-by-Step Calculation:\\n1. **Identify the base and exponent**: \\n In $3^3$, the base is **3**, and the exponent is **3**. This means we multiply 3 by itself three times.\\n\\n2. **Perform the multiplication**: \\n - First, multiply the first two 3s: \\n $3 \\\\times 3 = 9$ \\n - Next, multiply the result by the third 3: \\n $9 \\\\times 3 = 27$\\n\\n3. **Verify the result**: \\n - $3^1 = 3$ \\n - $3^2 = 3 \\\\times 3 = 9$ \\n - $3^3 = 3 \\\\times 3 \\\\times 3 = 27$ \\n This progression confirms the calculation.\\n\\n### Final Answer:\\n$$\\n3^3 = \\\\boxed{27}\\n$$'}]\n",
"Response without reasoning: [{'type': 'text', 'text': \"Sure! Let's break down what **3³** means and how to calculate it step by step.\\n\\n---\\n\\n### Step 1: Understand the notation\\nThe expression **3³** means **3 multiplied by itself three times**. The small number (3) is called the **exponent**, and it tells us how many times the base number (3) is used as a factor.\\n\\nSo:\\n$$\\n3^3 = 3 \\\\times 3 \\\\times 3\\n$$\\n\\n---\\n\\n### Step 2: Perform the multiplication step by step\\n\\n1. Multiply the first two 3s:\\n $$\\n 3 \\\\times 3 = 9\\n $$\\n\\n2. Now multiply the result by the third 3:\\n $$\\n 9 \\\\times 3 = 27\\n $$\\n\\n---\\n\\n### Step 3: Final Answer\\n\\n$$\\n3^3 = 27\\n$$\\n\\n---\\n\\n### Summary\\n- **3³** means **3 × 3 × 3**\\n- **3 × 3 = 9**\\n- **9 × 3 = 27**\\n- So, **3³ = 27**\\n\\nLet me know if you'd like to explore exponents further!\"}]\n"
]
}
],
"source": [
"from langchain_core.messages import ChatMessage, HumanMessage\n",
"from langchain_ollama import ChatOllama\n",
"from langchain_ollama.v1 import ChatOllama\n",
"\n",
"llm = ChatOllama(model=\"granite3.2:8b\")\n",
"# All outputs from `llm` will include reasoning unless overridden during invocation\n",
"llm = ChatOllama(model=\"qwen3:8b\", validate_model_on_init=True, reasoning=True)\n",
"\n",
"messages = [\n",
" ChatMessage(role=\"control\", content=\"thinking\"),\n",
" HumanMessage(\"What is 3^3?\"),\n",
"]\n",
"response_a = llm.invoke(\"What is 3^3? Explain your reasoning step by step.\")\n",
"print(f\"Response including reasoning: {response_a.content}\")\n",
"\n",
"response = llm.invoke(messages)\n",
"print(response.content)"
"# Test override; note no ReasoningContentBlock in the response\n",
"response_b = llm.invoke(\n",
" \"What is 3^3? Explain your reasoning step by step.\", reasoning=False\n",
")\n",
"print(f\"Response without reasoning: {response_b.content}\")"
]
},
{
@@ -502,7 +560,7 @@
"id": "6271d032-da40-44d4-9b52-58370e164be3",
"metadata": {},
"source": [
"Note that the model exposes its thought process in addition to its final response."
"Note that the model exposes its thought process as a `ReasoningContentBlock` addition to its final response."
]
},
{

View File

@@ -447,6 +447,163 @@
")"
]
},
{
"cell_type": "markdown",
"id": "c5d9d19d-8ab1-4d9d-b3a0-56ee4e89c528",
"metadata": {},
"source": [
"### Custom tools\n",
"\n",
":::info Requires ``langchain-openai>=0.3.29``\n",
"\n",
":::\n",
"\n",
"[Custom tools](https://platform.openai.com/docs/guides/function-calling#custom-tools) support tools with arbitrary string inputs. They can be particularly useful when you expect your string arguments to be long or complex."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "a47c809b-852f-46bd-8b9e-d9534c17213d",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"Use the tool to calculate 3^3.\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"[{'id': 'rs_6894ff5747c0819d9b02fc5645b0be9c000169fd9fb68d99', 'summary': [], 'type': 'reasoning'}, {'call_id': 'call_7SYwMSQPbbEqFcKlKOpXeEux', 'input': 'print(3**3)', 'name': 'execute_code', 'type': 'custom_tool_call', 'id': 'ctc_6894ff5b9f54819d8155a63638d34103000169fd9fb68d99', 'status': 'completed'}]\n",
"Tool Calls:\n",
" execute_code (call_7SYwMSQPbbEqFcKlKOpXeEux)\n",
" Call ID: call_7SYwMSQPbbEqFcKlKOpXeEux\n",
" Args:\n",
" __arg1: print(3**3)\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: execute_code\n",
"\n",
"[{'type': 'custom_tool_call_output', 'output': '27'}]\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"[{'type': 'text', 'text': '27', 'annotations': [], 'id': 'msg_6894ff5db3b8819d9159b3a370a25843000169fd9fb68d99'}]\n"
]
}
],
"source": [
"from langchain_openai import ChatOpenAI, custom_tool\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"\n",
"@custom_tool\n",
"def execute_code(code: str) -> str:\n",
" \"\"\"Execute python code.\"\"\"\n",
" return \"27\"\n",
"\n",
"\n",
"llm = ChatOpenAI(model=\"gpt-5\", output_version=\"responses/v1\")\n",
"\n",
"agent = create_react_agent(llm, [execute_code])\n",
"\n",
"input_message = {\"role\": \"user\", \"content\": \"Use the tool to calculate 3^3.\"}\n",
"for step in agent.stream(\n",
" {\"messages\": [input_message]},\n",
" stream_mode=\"values\",\n",
"):\n",
" step[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "5ef93be6-6d4c-4eea-acfd-248774074082",
"metadata": {},
"source": [
"<details>\n",
"<summary>Context-free grammars</summary>\n",
"\n",
"OpenAI supports the specification of a [context-free grammar](https://platform.openai.com/docs/guides/function-calling#context-free-grammars) for custom tool inputs in `lark` or `regex` format. See [OpenAI docs](https://platform.openai.com/docs/guides/function-calling#context-free-grammars) for details. The `format` parameter can be passed into `@custom_tool` as shown below:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "2ae04586-be33-49c6-8947-7867801d868f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"Use the tool to calculate 3^3.\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"[{'id': 'rs_689500828a8481a297ff0f98e328689c0681550c89797f43', 'summary': [], 'type': 'reasoning'}, {'call_id': 'call_jzH01RVhu6EFz7yUrOFXX55s', 'input': '3 * 3 * 3', 'name': 'do_math', 'type': 'custom_tool_call', 'id': 'ctc_6895008d57bc81a2b84d0993517a66b90681550c89797f43', 'status': 'completed'}]\n",
"Tool Calls:\n",
" do_math (call_jzH01RVhu6EFz7yUrOFXX55s)\n",
" Call ID: call_jzH01RVhu6EFz7yUrOFXX55s\n",
" Args:\n",
" __arg1: 3 * 3 * 3\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: do_math\n",
"\n",
"[{'type': 'custom_tool_call_output', 'output': '27'}]\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"[{'type': 'text', 'text': '27', 'annotations': [], 'id': 'msg_6895009776b881a2a25f0be8507d08f20681550c89797f43'}]\n"
]
}
],
"source": [
"from langchain_openai import ChatOpenAI, custom_tool\n",
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"grammar = \"\"\"\n",
"start: expr\n",
"expr: term (SP ADD SP term)* -> add\n",
"| term\n",
"term: factor (SP MUL SP factor)* -> mul\n",
"| factor\n",
"factor: INT\n",
"SP: \" \"\n",
"ADD: \"+\"\n",
"MUL: \"*\"\n",
"%import common.INT\n",
"\"\"\"\n",
"\n",
"format_ = {\"type\": \"grammar\", \"syntax\": \"lark\", \"definition\": grammar}\n",
"\n",
"\n",
"# highlight-next-line\n",
"@custom_tool(format=format_)\n",
"def do_math(input_string: str) -> str:\n",
" \"\"\"Do a mathematical operation.\"\"\"\n",
" return \"27\"\n",
"\n",
"\n",
"llm = ChatOpenAI(model=\"gpt-5\", output_version=\"responses/v1\")\n",
"\n",
"agent = create_react_agent(llm, [do_math])\n",
"\n",
"input_message = {\"role\": \"user\", \"content\": \"Use the tool to calculate 3^3.\"}\n",
"for step in agent.stream(\n",
" {\"messages\": [input_message]},\n",
" stream_mode=\"values\",\n",
"):\n",
" step[\"messages\"][-1].pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "c63430c9-c7b0-4e92-a491-3f165dddeb8f",
"metadata": {},
"source": [
"</details>"
]
},
{
"cell_type": "markdown",
"id": "84833dd0-17e9-4269-82ed-550639d65751",

View File

@@ -132,12 +132,13 @@
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.documents import Document\n",
"from langchain_experimental.graph_transformers import LLMGraphTransformer\n",
"\n",
"# from langchain_experimental.graph_transformers import LLMGraphTransformer\n",
"from langchain_openai import ChatOpenAI\n",
"\n",
"# Define the LLMGraphTransformer\n",

View File

@@ -548,12 +548,12 @@
},
{
"cell_type": "code",
"execution_count": 14,
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from langchain_core.documents import Document\n",
"from langchain_experimental.graph_transformers import LLMGraphTransformer"
"# from langchain_experimental.graph_transformers import LLMGraphTransformer"
]
},
{

View File

@@ -1,4 +1,4 @@
# ChatGradient
# DigitalOcean Gradient
This will help you getting started with DigitalOcean Gradient [chat models](/docs/concepts/chat_models).

View File

@@ -1,14 +1,16 @@
# Ollama
>[Ollama](https://ollama.com/) allows you to run open-source large language models,
> such as [Llama3.1](https://ai.meta.com/blog/meta-llama-3-1/), locally.
> such as [gpt-oss](https://ollama.com/library/gpt-oss), locally.
>
>`Ollama` bundles model weights, configuration, and data into a single package, defined by a Modelfile.
>It optimizes setup and configuration details, including GPU usage.
>For a complete list of supported models and model variants, see the [Ollama model library](https://ollama.ai/library).
>The `ollama` [package](https://pypi.org/project/ollama/0.5.3/) bundles model weights,
> configuration, and data into a single package, defined by a Modelfile. It optimizes
> setup and configuration details, including GPU usage.
>For a complete list of supported models and model variants, see the
> [Ollama model library](https://ollama.com/search).
See [this guide](/docs/how_to/local_llms) for more details
on how to use `Ollama` with LangChain.
See [this guide](/docs/how_to/local_llms/#ollama) for more details
on how to use `ollama` with LangChain.
## Installation and Setup
### Ollama installation
@@ -23,10 +25,10 @@ Ollama will start as a background service automatically, if this is disabled, ru
ollama serve
```
After starting ollama, run `ollama pull <name-of-model>` to download a model from the [Ollama model library](https://ollama.ai/library):
After starting ollama, run `ollama pull <name-of-model>` to download a model from the [Ollama model library](https://ollama.com/library):
```bash
ollama pull llama3.1
ollama pull gpt-oss:20b
```
- This will download the default tagged version of the model. Typically, the default points to the latest, smallest sized-parameter model.

View File

@@ -29,8 +29,8 @@
" Please refer to the instructions in:\n",
" [www.jaguardb.com](http://www.jaguardb.com)\n",
" For quick setup in docker environment:\n",
" docker pull jaguardb/jaguardb_with_http\n",
" docker run -d -p 8888:8888 -p 8080:8080 --name jaguardb_with_http jaguardb/jaguardb_with_http\n",
" docker pull jaguardb/jaguardb\n",
" docker run -d -p 8888:8888 -p 8080:8080 --name jaguardb jaguardb/jaguardb\n",
"\n",
"2. You must install the http client package for JaguarDB:\n",
" ```\n",

View File

@@ -591,7 +591,7 @@
},
{
"cell_type": "code",
"execution_count": 36,
"execution_count": null,
"metadata": {
"azdata_cell_guid": "d9127900-0942-48f1-bd4d-081c7fa3fcae",
"language": "python"
@@ -606,7 +606,7 @@
}
],
"source": [
"from langchain.document_loaders import AzureBlobStorageFileLoader\n",
"from langchain_community.document_loaders import AzureBlobStorageFileLoader\n",
"from langchain.text_splitter import RecursiveCharacterTextSplitter\n",
"from langchain_core.documents import Document\n",
"\n",

View File

@@ -0,0 +1,107 @@
---
sidebar_position: 3
---
# How to update your code
*Last updated: 08.08.25*
If you maintain custom callbacks or output parsers, type checkers may raise errors if
they do not accept the new message types as inputs. This guide describes how to
address those issues.
If you do not maintain custom callbacks or output parsers, there are no breaking
changes. See our guide on the [new message types](/docs/versions/v0_4/messages) to learn
about new features introduced in v0.4.
## Custom callbacks
[BaseCallbackHandler](https://python.langchain.com/api_reference/core/callbacks/langchain_core.callbacks.base.BaseCallbackHandler.html)
now includes an attribute `accepts_new_messages` that defaults to False. When this
attribute is False, the callback system in langchain-core will automatically convert
new message types to old, so there should be no runtime errors. You can update callback
signatures as below to fix type-checking errors:
```python
from langchain_core.v1.messages import AIMessage, AIMessageChunk, MessageV1
def on_chat_model_start(
self,
serialized: dict[str, Any],
# highlight-next-line
messages: Union[list[list[BaseMessage]], list[MessageV1]],
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
tags: Optional[list[str]] = None,
metadata: Optional[dict[str, Any]] = None,
**kwargs: Any,
) -> Any:
def on_llm_new_token(
self,
token: str,
*,
chunk: Optional[
# highlight-next-line
Union[GenerationChunk, ChatGenerationChunk, AIMessageChunk]
] = None,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
**kwargs: Any,
) -> Any:
def on_llm_end(
self,
# highlight-next-line
response: Union[LLMResult, AIMessage],
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
**kwargs: Any,
) -> Any:
```
You can also safely type-ignore mypy `override` errors here unless you switch
`accepts_new_messages` to True.
## Custom output parsers
All output parsers in `langchain-core` have been updated to accept the new message
types.
If you maintain a custom output parser, `langchain-core` exposes a
`convert_from_v1_message` function so that your parser can easily operate on the new
message types:
```python
from langchain_core.messages.utils import convert_from_v1_message
from langchain_core.v1.messages import AIMessage
def parse_result(
self,
# highlight-next-line
result: Union[list[Generation], AIMessage],
*,
partial: bool = False,
) -> Union[list[AgentAction], AgentFinish]:
# highlight-start
if isinstance(result, AIMessage):
result = [ChatGeneration(message=convert_from_v1_message(result))]
# highlight-end
...
def _transform(
# higlight-next-line
self, input: Iterator[Union[str, BaseMessage, AIMessage]]
) -> Iterator[AddableDict]:
for chunk in input:
# higlight-start
if isinstance(chunk, AIMessage):
chunk = convert_from_v1_message(chunk)
# higlight-end
...
```
This will allow your parser to work as before. You can also update the parser to
natively handle the new message types to save this conversion step. See our guide on
the [new message types](/docs/versions/v0_4/messages) for details.

View File

@@ -0,0 +1,81 @@
---
sidebar_position: 1
---
# LangChain v0.4
*Last updated: 08.08.25*
## What's changed
LangChain v0.4 allows developers to opt-in to new message types that will become default
in LangChain v1.0. LangChain v1.0 will be released this fall. These messages provide
fully typed, provider-agnostic content, introducing standard content blocks for
reasoning, citations, server-side tool calls, and other LLM features. They also offer
performance benefits over existing message classes.
New message types have been added to a `v1` namespace in `langchain-core`. Select
integration packages now also expose a `v1` namespace containing chat models that
work with the new message types.
Input types for callbacks and output parsers have been widened to accept the new message
types. If you maintain custom callbacks or output parsers, type checkers may raise
errors if they do not accept the new message types as inputs. Refer to
[this guide](/docs/versions/v0_4/how_to_update) for how to address those issues. This
is the only breaking change.
## What's new
You can access the new chat models through [init_chat_model](/docs/how_to/chat_models_universal_init/) by setting `message_version="v1"`:
```python
from langchain.chat_models import init_chat_model
llm = init_chat_model("openai:gpt-5", message_version="v1")
input_message = {"role": "user", "content": "Hello, world!"}
llm.invoke([input_message])
```
You can also access the `v1` namespaces directly:
```python
from langchain_core.v1.messages import HumanMessage
from langchain_openai.v1 import ChatOpenAI
input_message = HumanMessage("Hello, world!")
llm.invoke([input_message])
```
:::info New message details
See our guide on the [new message types](/docs/versions/v0_4/messages) for details.
:::
## How to update your code
If you maintain custom callbacks or output parsers, type checkers may raise errors if
they do not accept the new message types as inputs. Refer to
[this guide](/docs/versions/v0_4/how_to_update) for how to address those issues.
If you do not maintain custom callbacks or output parsers, there are no breaking
changes. See our guide on the [new message types](/docs/versions/v0_4/messages) to learn
about new features introduced in v0.4.
### Base packages
| Package | Latest | Recommended constraint |
|--------------------------|--------|------------------------|
| langchain | 0.4.0 | >=0.4,&lt;1.0 |
| langchain-community | 0.4.0 | >=0.4,&lt;1.0 |
| langchain-text-splitters | 0.4.0 | >=0.4,&lt;1.0 |
| langchain-core | 0.4.0 | >=0.4,&lt;1.0 |
| langchain-experimental | 0.4.0 | >=0.4,&lt;1.0 |
### Integration packages
...

View File

@@ -0,0 +1,429 @@
---
sidebar_position: 2
---
# LangChain v1.0 message types
*Last updated: 08.08.25*
LangChain v0.4 allows developers to opt-in to new message types that will become default
in LangChain v1.0. LangChain v1.0 will be released this fall.
These messages should be considered a beta feature and are subject to change in
LangChain v1.0, although we do not anticipate any significant changes.
## Benefits
The new message types offer improvements in performance, type-safety, and consistency
across OpenAI, Anthropic, Gemini, and other providers.
### Performance
Importantly, the new messages are Python dataclasses, saving some runtime from
instantiating (layers of) Pydantic BaseModels.
LangChain v0.4 introduces a new `BaseChatModel` class in `langchain_core.v1.chat_models`
that is faster and leaner than the existing `BaseChatModel` class, offering significant
reductions in overhead above provider SDKs.
### Type-safety
Message content is typed as
```python
import langchain_core.messages.content_blocks as types
content: list[types.ContentBlock]
```
where we have introduced standard types for text, reasoning, citations, server-side
tool executions (e.g., web search and code interpreters). These include
[tool calls](https://python.langchain.com/docs/concepts/tool_calling/) and the
[multi-modal types](/docs/how_to/multimodal_inputs/) introduced in earlier versions
of LangChain. There are no breaking changes associated with the existing content types.
**This is the most significant change from the existing message classes**, which permit
strings, lists of strings, or lists of untyped dicts as content. We have added a
`.text` getter so that developers can easily recover string content. Consequently, we
have deprecated `.text()` (as a method) in favor of the new property.
`.tool_calls`, instead of an attribute, is now also a getter with an associated setter,
so that usage is largely the same. See [usage comparison](#usage-comparison), below,
for details.
### Consistency
Many chat models can generate a variety of content in a single conversational turn,
including reasoning, tool calls and responses, images, text with citations, and other
structured objects. We have standardized these types, resulting in improved
inter-operability of messages across models.
## Usage comparison
| Task | Previous | New |
|-------------------------|----------------------------------------|------------------------------------------------------------------|
| Get text content (str) | `message.content` or `message.text()` | `message.text` |
| Get content blocks | `message.content` | `message.content` |
| Get `additional_kwargs` | `message.additional_kwargs` | `[block for block in message.content if block["type"] == "..."]` |
Getting `response_metadata` and `tool_calls` has not changed.
### Changes in content blocks
For providers that generate `list[dict]` content, the dict elements have changed to
conform to the new content block types. Refer to the
[API reference](https://python.langchain.com/api_reference/core/messages.html) for
details. Below we show some examples.
Importantly:
- Where provider-specific fields map to fields on standard types, LangChain manages
the translation.
- Where provider-specific fields do not map to fields on standard types, LangChain
stores them in an `"extras"` key (see below for examples).
<details>
<summary>Citations and web search</summary>
<div className="row">
<div className="col col--6" style={{minWidth: 0}}>
**Old content**
```python
from langchain.chat_models import init_chat_model
llm = init_chat_model("openai:gpt-5-mini", output_version="responses/v1")
llm_with_tools = llm.bind_tools([{"type": "web_search_preview"}])
response = llm_with_tools.invoke("What was a positive news story from today?")
response.content
```
```
[
{
"type": "reasoning",
"id": "rs_abc123",
"summary": []
},
{
"type": "web_search_call",
"id": "ws_abc123",
"action": {
"query": "positive news today August 8 2025 'good news' 'Aug 8 2025' 'today' ",
"type": "search"
},
"status": "completed"
},
{
"type": "text",
"text": "Here are two positive news items from today...",
"annotations": [
{
"type": "url_citation",
"end_index": 455,
"start_index": 196,
"title": "Document title",
"url": "<document url>"
},
{
"type": "url_citation",
"end_index": 1022,
"start_index": 707,
"title": "Another Document",
"url": "<another document url>"
},
],
"id": "msg_abc123"
}
]
```
</div>
<div className="col col--6" style={{minWidth: 0}}>
**New content**
```python
from langchain.chat_models import init_chat_model
llm = init_chat_model("openai:gpt-5-mini", message_version="v1")
llm_with_tools = llm.bind_tools([{"type": "web_search_preview"}])
response = llm_with_tools.invoke("What was a positive news story from today?")
response.content
```
```
[
{
"type": "reasoning",
"id": "rs_abc123"
},
{
"type": "web_search_call",
"id": "ws_abc123",
"query": "positive news August 8 2025 'good news' 'today' ",
"extras": {
"action": {"type": "search"},
"status": "completed",
}
},
{
"type": "web_search_result",
"id": "ws_abc123"
},
{
"type": "text",
"text": "Here are two positive news items from today...",
"annotations": [
{
"type": "citation",
"end_index": 455,
"start_index": 196,
"title": "Document title",
"url": "<document url>"
},
{
"type": "citation",
"end_index": 1022,
"start_index": 707,
"title": "Another Document",
"url": "<another document url>"
}
],
"id": "msg_abc123"
}
]
```
</div>
</div>
</details>
<details>
<summary>Reasoning</summary>
<div className="row">
<div className="col col--6" style={{minWidth: 0}}>
**Old content**
```python
from langchain.chat_models import init_chat_model
llm = init_chat_model(
"openai:gpt-5",
reasoning={"effort": "medium", "summary": "auto"},
output_version="responses/v1",
)
response = llm.invoke(
"What was the third tallest building in the world in the year 2000?"
)
response.content
```
```
[
{
"type": "reasoning",
"id": "rs_abc123",
"summary": [
{
"text": "The user is asking about...",
"type": "summary_text"
},
{
"text": "We should consider...",
"type": "summary_text"
}
]
},
{
"type": "text",
"text": "In the year 2000 the third-tallest building in the world was...",
"id": "msg_abc123"
}
]
```
</div>
<div className="col col--6" style={{minWidth: 0}}>
**New content**
```python
from langchain.chat_models import init_chat_model
llm = init_chat_model(
"openai:gpt-5",
reasoning={"effort": "medium", "summary": "auto"},
message_version="v1",
)
response = llm.invoke(
"What was the third tallest building in the world in the year 2000?"
)
response.content
```
```
[
{
"type": "reasoning",
"reasoning": "The user is asking about...",
"id": "rs_abc123"
},
{
"type": "reasoning",
"reasoning": "We should consider...",
"id": "rs_abc123"
},
{
"type": "text",
"text": "In the year 2000 the third-tallest building in the world was...",
"id": "msg_abc123"
}
]
```
</div>
</div>
</details>
<details>
<summary>Non-standard blocks</summary>
Where content blocks from specific providers do not map to a standard type, they are
structured into a `"non_standard"` block:
```python
{
"type": "non_standard",
"value": original_block,
}
```
<div className="row">
<div className="col col--6" style={{minWidth: 0}}>
**Old content**
```python
from langchain.chat_models import init_chat_model
llm = init_chat_model("openai:gpt-5-mini", output_version="responses/v1")
llm_with_tools = llm.bind_tools(
[
{
"type": "file_search",
"vector_store_ids": ["vs_67d0baa0544c8191be194a85e19cbf92"],
}
]
)
response = llm_with_tools.invoke("What is deep research by OpenAI?")
response.content
```
```
[
{
"type": "reasoning",
"id": "rs_abc123",
"summary": []
},
{
"type": "file_search_call",
"id": "fs_abc123",
"queries": [
"What is deep research by OpenAI?",
"deep research OpenAI definition"
],
"status": "completed"
},
{
"type": "reasoning",
"id": "rs_def456",
"summary": []
},
{
"type": "text",
"text": "Deep research is...",
"annotations": [
{
"type": "file_citation",
"file_id": "file-abc123",
"filename": "sample_file.pdf",
"index": 305
},
{
"type": "file_citation",
"file_id": "file-abc123",
"filename": "sample_file.pdf",
"index": 675
},
],
"id": "msg_abc123"
}
]
```
</div>
<div className="col col--6" style={{minWidth: 0}}>
**New content**
```python
from langchain.chat_models import init_chat_model
llm = init_chat_model("openai:gpt-5-mini", message_version="v1")
llm_with_tools = llm.bind_tools(
[
{
"type": "file_search",
"vector_store_ids": ["vs_67d0baa0544c8191be194a85e19cbf92"],
}
]
)
response = llm_with_tools.invoke("What is deep research by OpenAI?")
response.content
```
```
[
{
"type": "reasoning",
"id": "rs_abc123",
"summary": []
},
{
"type": "non_standard",
"value": {
"type": "file_search_call",
"id": "fs_abc123",
"queries": [
"What is deep research by OpenAI?",
"deep research OpenAI definition"
],
"status": "completed"
}
},
{
"type": "reasoning",
"id": "rs_def456",
"summary": []
},
{
"type": "text",
"text": "Deep research is...",
"annotations": [
{
"type": "citation",
"title": "sample_file.pdf",
"extras": {
"file_id": "file-abc123",
"index": 305
}
},
{
"type": "citation",
"title": "sample_file.pdf",
"extras": {
"file_id": "file-abc123",
"index": 675
}
},
],
"id": "msg_abc123"
}
]
```
</div>
</div>
</details>
## Feature gaps
The new message types do not yet support LangChain's caching layer. Support will be
added in the coming weeks.

View File

@@ -224,13 +224,17 @@ const config = {
},
{
type: "dropdown",
label: "v0.3",
label: "v0.4",
position: "right",
items: [
{
label: "v0.3",
label: "v0.4",
href: "/docs/introduction",
},
{
label: "v0.3",
href: "https://python.langchain.com/v0.3/docs/introduction/",
},
{
label: "v0.2",
href: "https://python.langchain.com/v0.2/docs/introduction",

View File

@@ -1,5 +1,6 @@
from datetime import datetime, timedelta, timezone
from pathlib import Path
import re
import requests
from ruamel.yaml import YAML
@@ -11,10 +12,18 @@ PACKAGE_YML = Path(__file__).parents[2] / "libs" / "packages.yml"
def _get_downloads(p: dict) -> int:
url = f"https://pypistats.org/api/packages/{p['name']}/recent?period=month"
r = requests.get(url)
r.raise_for_status()
return r.json()["data"]["last_month"]
url = f"https://pepy.tech/badge/{p['name']}/month"
svg = requests.get(url, timeout=10).text
texts = re.findall(r"<text[^>]*>([^<]+)</text>", svg)
latest = texts[-1].strip() if texts else "0"
# parse "1.2k", "3.4M", "12,345" -> int
latest = latest.replace(",", "")
if latest.endswith(("k", "K")):
return int(float(latest[:-1]) * 1_000)
if latest.endswith(("m", "M")):
return int(float(latest[:-1]) * 1_000_000)
return int(float(latest) if "." in latest else int(latest))
current_datetime = datetime.now(timezone.utc)

View File

@@ -101,7 +101,12 @@ def package_row(p: dict) -> str:
link = p["provider_page"]
title = p["name_title"]
provider = f"[{title}]({link})" if link else title
return f"| {provider} | [{p['name']}]({p['package_url']}) | ![PyPI - Downloads](https://img.shields.io/pypi/dm/{p['name']}?style=flat-square&label=%20&color=blue) | ![PyPI - Version](https://img.shields.io/pypi/v/{p['name']}?style=flat-square&label=%20&color=orange) | {js} |"
return (
f"| {provider} | [{p['name']}]({p['package_url']}) | "
f"![Downloads](https://static.pepy.tech/badge/{p['name']}/month) | "
f"![PyPI - Version](https://img.shields.io/pypi/v/{p['name']}?style=flat-square&label=%20&color=orange) | "
f"{js} |"
)
def table() -> str:

View File

@@ -82,6 +82,14 @@ module.exports = {
collapsed: false,
collapsible: false,
items: [
{
type: "category",
label: "v0.4",
items: [{
type: 'autogenerated',
dirName: 'versions/v0_4',
}],
},
{
type: 'doc',
id: 'versions/v0_3/index',
@@ -418,7 +426,7 @@ module.exports = {
},
],
},
],
link: {
type: "generated-index",

View File

@@ -231,6 +231,13 @@ ${llmVarName} = ChatWatsonx(
model: "llama-3.1-sonar-small-128k-online",
apiKeyName: "PPLX_API_KEY",
packageName: "langchain-perplexity",
},
{
value: "deepseek",
label: "DeepSeek",
model: "deepseek-chat",
apiKeyName: "DEEPSEEK_API_KEY",
packageName: "langchain-deepseek",
}
].map((item) => ({
...item,

View File

@@ -23,11 +23,19 @@
{
"source": "/v0.2/:path(.*/?)*",
"destination": "https://langchain-v02.vercel.app/v0.2/:path*"
},
{
"source": "/v0.3",
"destination": "https://langchain-v03.vercel.app/v0.3"
},
{
"source": "/v0.3/:path(.*/?)*",
"destination": "https://langchain-v03.vercel.app/v0.3/:path*"
}
],
"redirects": [
{
"source": "/v0.3/docs/:path(.*/?)*",
"source": "/v0.4/docs/:path(.*/?)*",
"destination": "/docs/:path*"
},
{

View File

@@ -35,6 +35,7 @@ embeddings.embed_query("What is the meaning of life?")
```
## LLMs
`__ModuleName__LLM` class exposes LLMs from __ModuleName__.
```python

View File

@@ -1,3 +1,3 @@
version: 0.0.1
patterns:
- name: github.com/getgrit/stdlib#*
- name: github.com/getgrit/stdlib#*

View File

@@ -27,16 +27,16 @@ langchain app add __package_name__
```
And add the following code to your `server.py` file:
```python
__app_route_code__
```
(Optional) Let's now configure LangSmith.
LangSmith will help us trace, monitor and debug LangChain applications.
You can sign up for LangSmith [here](https://smith.langchain.com/).
(Optional) Let's now configure LangSmith.
LangSmith will help us trace, monitor and debug LangChain applications.
You can sign up for LangSmith [here](https://smith.langchain.com/).
If you don't have access, you can skip this section
```shell
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY=<your-api-key>
@@ -49,11 +49,11 @@ If you are inside this directory, then you can spin up a LangServe instance dire
langchain serve
```
This will start the FastAPI app with a server is running locally at
This will start the FastAPI app with a server is running locally at
[http://localhost:8000](http://localhost:8000)
We can see all templates at [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs)
We can access the playground at [http://127.0.0.1:8000/__package_name__/playground](http://127.0.0.1:8000/__package_name__/playground)
We can access the playground at [http://127.0.0.1:8000/__package_name__/playground](http://127.0.0.1:8000/__package_name__/playground)
We can access the template from code with:
@@ -61,4 +61,4 @@ We can access the template from code with:
from langserve.client import RemoteRunnable
runnable = RemoteRunnable("http://localhost:8000/__package_name__")
```
```

View File

@@ -11,7 +11,7 @@ pip install -U langchain-cli
## Adding packages
```bash
# adding packages from
# adding packages from
# https://github.com/langchain-ai/langchain/tree/master/templates
langchain app add $PROJECT_NAME
@@ -31,10 +31,10 @@ langchain app remove my/custom/path/rag
```
## Setup LangSmith (Optional)
LangSmith will help us trace, monitor and debug LangChain applications.
You can sign up for LangSmith [here](https://smith.langchain.com/).
If you don't have access, you can skip this section
LangSmith will help us trace, monitor and debug LangChain applications.
You can sign up for LangSmith [here](https://smith.langchain.com/).
If you don't have access, you can skip this section
```shell
export LANGSMITH_TRACING=true

View File

@@ -144,10 +144,9 @@ def beta(
obj.__init__ = functools.wraps(obj.__init__)( # type: ignore[misc]
warn_if_direct_instance
)
return cast("T", obj)
return obj
elif isinstance(obj, property):
# note(erick): this block doesn't seem to be used?
if not _obj_type:
_obj_type = "attribute"
wrapped = None
@@ -168,6 +167,7 @@ def beta(
self.__orig_fget = fget
self.__orig_fset = fset
self.__orig_fdel = fdel
self.__doc__ = doc
def __get__(
self, instance: Any, owner: Union[type, None] = None

View File

@@ -225,7 +225,7 @@ def deprecated(
obj.__init__ = functools.wraps(obj.__init__)( # type: ignore[misc]
warn_if_direct_instance
)
return cast("T", obj)
return obj
elif isinstance(obj, FieldInfoV1):
wrapped = None

View File

@@ -7,6 +7,8 @@ from typing import TYPE_CHECKING, Any, Optional, Union
from typing_extensions import Self
from langchain_core.v1.messages import AIMessage, AIMessageChunk, MessageV1
if TYPE_CHECKING:
from collections.abc import Sequence
from uuid import UUID
@@ -66,7 +68,9 @@ class LLMManagerMixin:
self,
token: str,
*,
chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]] = None,
chunk: Optional[
Union[GenerationChunk, ChatGenerationChunk, AIMessageChunk]
] = None,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
**kwargs: Any,
@@ -75,8 +79,8 @@ class LLMManagerMixin:
Args:
token (str): The new token.
chunk (GenerationChunk | ChatGenerationChunk): The new generated chunk,
containing content and other information.
chunk (GenerationChunk | ChatGenerationChunk | AIMessageChunk): The new
generated chunk, containing content and other information.
run_id (UUID): The run ID. This is the ID of the current run.
parent_run_id (UUID): The parent run ID. This is the ID of the parent run.
kwargs (Any): Additional keyword arguments.
@@ -84,7 +88,7 @@ class LLMManagerMixin:
def on_llm_end(
self,
response: LLMResult,
response: Union[LLMResult, AIMessage],
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
@@ -93,7 +97,7 @@ class LLMManagerMixin:
"""Run when LLM ends running.
Args:
response (LLMResult): The response which was generated.
response (LLMResult | AIMessage): The response which was generated.
run_id (UUID): The run ID. This is the ID of the current run.
parent_run_id (UUID): The parent run ID. This is the ID of the parent run.
kwargs (Any): Additional keyword arguments.
@@ -261,7 +265,7 @@ class CallbackManagerMixin:
def on_chat_model_start(
self,
serialized: dict[str, Any],
messages: list[list[BaseMessage]],
messages: Union[list[list[BaseMessage]], list[MessageV1]],
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
@@ -439,6 +443,9 @@ class BaseCallbackHandler(
run_inline: bool = False
"""Whether to run the callback inline."""
accepts_new_messages: bool = False
"""Whether the callback accepts new message format."""
@property
def ignore_llm(self) -> bool:
"""Whether to ignore LLM callbacks."""
@@ -509,7 +516,7 @@ class AsyncCallbackHandler(BaseCallbackHandler):
async def on_chat_model_start(
self,
serialized: dict[str, Any],
messages: list[list[BaseMessage]],
messages: Union[list[list[BaseMessage]], list[MessageV1]],
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
@@ -540,7 +547,9 @@ class AsyncCallbackHandler(BaseCallbackHandler):
self,
token: str,
*,
chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]] = None,
chunk: Optional[
Union[GenerationChunk, ChatGenerationChunk, AIMessageChunk]
] = None,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
tags: Optional[list[str]] = None,
@@ -550,8 +559,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
Args:
token (str): The new token.
chunk (GenerationChunk | ChatGenerationChunk): The new generated chunk,
containing content and other information.
chunk (GenerationChunk | ChatGenerationChunk | AIMessageChunk): The new
generated chunk, containing content and other information.
run_id (UUID): The run ID. This is the ID of the current run.
parent_run_id (UUID): The parent run ID. This is the ID of the parent run.
tags (Optional[list[str]]): The tags.
@@ -560,7 +569,7 @@ class AsyncCallbackHandler(BaseCallbackHandler):
async def on_llm_end(
self,
response: LLMResult,
response: Union[LLMResult, AIMessage],
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
@@ -570,7 +579,7 @@ class AsyncCallbackHandler(BaseCallbackHandler):
"""Run when LLM ends running.
Args:
response (LLMResult): The response which was generated.
response (LLMResult | AIMessage): The response which was generated.
run_id (UUID): The run ID. This is the ID of the current run.
parent_run_id (UUID): The parent run ID. This is the ID of the parent run.
tags (Optional[list[str]]): The tags.
@@ -594,8 +603,8 @@ class AsyncCallbackHandler(BaseCallbackHandler):
parent_run_id: The parent run ID. This is the ID of the parent run.
tags: The tags.
kwargs (Any): Additional keyword arguments.
- response (LLMResult): The response which was generated before
the error occurred.
- response (LLMResult | AIMessage): The response which was generated
before the error occurred.
"""
async def on_chain_start(

View File

@@ -49,7 +49,7 @@ class FileCallbackHandler(BaseCallbackHandler):
mode: The file open mode. Defaults to ``'a'`` (append).
color: Default color for text output. Defaults to ``None``.
Note:
.. note::
When not used as a context manager, a deprecation warning will be issued
on first use. The file will be opened immediately in ``__init__`` and closed
in ``__del__`` or when ``close()`` is called explicitly.
@@ -65,6 +65,7 @@ class FileCallbackHandler(BaseCallbackHandler):
filename: Path to the output file.
mode: File open mode (e.g., ``'w'``, ``'a'``, ``'x'``). Defaults to ``'a'``.
color: Default text color for output. Defaults to ``None``.
"""
self.filename = filename
self.mode = mode
@@ -82,9 +83,10 @@ class FileCallbackHandler(BaseCallbackHandler):
Returns:
The FileCallbackHandler instance.
Note:
.. note::
The file is already opened in ``__init__``, so this just marks that
the handler is being used as a context manager.
"""
self._file_opened_in_context = True
return self
@@ -101,6 +103,7 @@ class FileCallbackHandler(BaseCallbackHandler):
exc_type: Exception type if an exception occurred.
exc_val: Exception value if an exception occurred.
exc_tb: Exception traceback if an exception occurred.
"""
self.close()
@@ -113,6 +116,7 @@ class FileCallbackHandler(BaseCallbackHandler):
This method is safe to call multiple times and will only close
the file if it's currently open.
"""
if hasattr(self, "file") and self.file and not self.file.closed:
self.file.close()
@@ -133,6 +137,7 @@ class FileCallbackHandler(BaseCallbackHandler):
Raises:
RuntimeError: If the file is closed or not available.
"""
global _GLOBAL_DEPRECATION_WARNED # noqa: PLW0603
if not self._file_opened_in_context and not _GLOBAL_DEPRECATION_WARNED:
@@ -163,6 +168,7 @@ class FileCallbackHandler(BaseCallbackHandler):
serialized: The serialized chain information.
inputs: The inputs to the chain.
**kwargs: Additional keyword arguments that may contain ``'name'``.
"""
name = (
kwargs.get("name")
@@ -178,6 +184,7 @@ class FileCallbackHandler(BaseCallbackHandler):
Args:
outputs: The outputs of the chain.
**kwargs: Additional keyword arguments.
"""
self._write("\n> Finished chain.", end="\n")
@@ -192,6 +199,7 @@ class FileCallbackHandler(BaseCallbackHandler):
color: Color override for this specific output. If ``None``, uses
``self.color``.
**kwargs: Additional keyword arguments.
"""
self._write(action.log, color=color or self.color)
@@ -213,6 +221,7 @@ class FileCallbackHandler(BaseCallbackHandler):
observation_prefix: Optional prefix to write before the output.
llm_prefix: Optional prefix to write after the output.
**kwargs: Additional keyword arguments.
"""
if observation_prefix is not None:
self._write(f"\n{observation_prefix}")
@@ -232,6 +241,7 @@ class FileCallbackHandler(BaseCallbackHandler):
``self.color``.
end: String appended after the text. Defaults to ``""``.
**kwargs: Additional keyword arguments.
"""
self._write(text, color=color or self.color, end=end)
@@ -246,5 +256,6 @@ class FileCallbackHandler(BaseCallbackHandler):
color: Color override for this specific output. If ``None``, uses
``self.color``.
**kwargs: Additional keyword arguments.
"""
self._write(finish.log, color=color or self.color, end="\n")

View File

@@ -11,15 +11,7 @@ from abc import ABC, abstractmethod
from concurrent.futures import ThreadPoolExecutor
from contextlib import asynccontextmanager, contextmanager
from contextvars import copy_context
from typing import (
TYPE_CHECKING,
Any,
Callable,
Optional,
TypeVar,
Union,
cast,
)
from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union, cast
from uuid import UUID
from langsmith.run_helpers import get_tracing_context
@@ -37,8 +29,16 @@ from langchain_core.callbacks.base import (
)
from langchain_core.callbacks.stdout import StdOutCallbackHandler
from langchain_core.messages import BaseMessage, get_buffer_string
from langchain_core.messages.utils import convert_from_v1_message
from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, LLMResult
from langchain_core.tracers.schemas import Run
from langchain_core.utils.env import env_var_is_set
from langchain_core.v1.messages import (
AIMessage,
AIMessageChunk,
MessageV1,
MessageV1Types,
)
if TYPE_CHECKING:
from collections.abc import AsyncGenerator, Coroutine, Generator, Sequence
@@ -47,7 +47,7 @@ if TYPE_CHECKING:
from langchain_core.agents import AgentAction, AgentFinish
from langchain_core.documents import Document
from langchain_core.outputs import ChatGenerationChunk, GenerationChunk, LLMResult
from langchain_core.outputs import GenerationChunk
from langchain_core.runnables.config import RunnableConfig
logger = logging.getLogger(__name__)
@@ -92,7 +92,8 @@ def trace_as_chain_group(
metadata (dict[str, Any], optional): The metadata to apply to all runs.
Defaults to None.
Note: must have LANGCHAIN_TRACING_V2 env var set to true to see the trace in LangSmith.
.. note:
Must have ``LANGCHAIN_TRACING_V2`` env var set to true to see the trace in LangSmith.
Returns:
CallbackManagerForChainGroup: The callback manager for the chain group.
@@ -177,7 +178,8 @@ async def atrace_as_chain_group(
Returns:
AsyncCallbackManager: The async callback manager for the chain group.
Note: must have LANGCHAIN_TRACING_V2 env var set to true to see the trace in LangSmith.
.. note:
Must have ``LANGCHAIN_TRACING_V2`` env var set to true to see the trace in LangSmith.
Example:
.. code-block:: python
@@ -234,6 +236,7 @@ def shielded(func: Func) -> Func:
Returns:
Callable: The shielded function
"""
@functools.wraps(func)
@@ -243,6 +246,46 @@ def shielded(func: Func) -> Func:
return cast("Func", wrapped)
def _convert_llm_events(
event_name: str, args: tuple[Any, ...], kwargs: dict[str, Any]
) -> tuple[tuple[Any, ...], dict[str, Any]]:
args_list = list(args)
if (
event_name == "on_chat_model_start"
and isinstance(args_list[1], list)
and args_list[1]
and isinstance(args_list[1][0], MessageV1Types)
):
batch = [
convert_from_v1_message(item)
for item in args_list[1]
if isinstance(item, MessageV1Types)
]
args_list[1] = [batch]
elif (
event_name == "on_llm_new_token"
and "chunk" in kwargs
and isinstance(kwargs["chunk"], MessageV1Types)
):
chunk = kwargs["chunk"]
kwargs["chunk"] = ChatGenerationChunk(text=chunk.text, message=chunk)
elif event_name == "on_llm_end" and isinstance(args_list[0], MessageV1Types):
args_list[0] = LLMResult(
generations=[
[
ChatGeneration(
text=args_list[0].text,
message=convert_from_v1_message(args_list[0]),
)
]
]
)
else:
pass
return tuple(args_list), kwargs
def handle_event(
handlers: list[BaseCallbackHandler],
event_name: str,
@@ -252,15 +295,17 @@ def handle_event(
) -> None:
"""Generic event handler for CallbackManager.
Note: This function is used by LangServe to handle events.
.. note::
This function is used by ``LangServe`` to handle events.
Args:
handlers: The list of handlers that will handle the event.
event_name: The name of the event (e.g., "on_llm_start").
event_name: The name of the event (e.g., ``'on_llm_start'``).
ignore_condition_name: Name of the attribute defined on handler
that if True will cause the handler to be skipped for the given event.
*args: The arguments to pass to the event handler.
**kwargs: The keyword arguments to pass to the event handler
"""
coros: list[Coroutine[Any, Any, Any]] = []
@@ -271,6 +316,8 @@ def handle_event(
if ignore_condition_name is None or not getattr(
handler, ignore_condition_name
):
if not handler.accepts_new_messages:
args, kwargs = _convert_llm_events(event_name, args, kwargs)
event = getattr(handler, event_name)(*args, **kwargs)
if asyncio.iscoroutine(event):
coros.append(event)
@@ -365,6 +412,8 @@ async def _ahandle_event_for_handler(
) -> None:
try:
if ignore_condition_name is None or not getattr(handler, ignore_condition_name):
if not handler.accepts_new_messages:
args, kwargs = _convert_llm_events(event_name, args, kwargs)
event = getattr(handler, event_name)
if asyncio.iscoroutinefunction(event):
await event(*args, **kwargs)
@@ -415,17 +464,19 @@ async def ahandle_event(
*args: Any,
**kwargs: Any,
) -> None:
"""Async generic event handler for AsyncCallbackManager.
"""Async generic event handler for ``AsyncCallbackManager``.
Note: This function is used by LangServe to handle events.
.. note::
This function is used by ``LangServe`` to handle events.
Args:
handlers: The list of handlers that will handle the event.
event_name: The name of the event (e.g., "on_llm_start").
event_name: The name of the event (e.g., ``'on_llm_start'``).
ignore_condition_name: Name of the attribute defined on handler
that if True will cause the handler to be skipped for the given event.
*args: The arguments to pass to the event handler.
**kwargs: The keyword arguments to pass to the event handler.
"""
for handler in [h for h in handlers if h.run_inline]:
await _ahandle_event_for_handler(
@@ -477,6 +528,7 @@ class BaseRunManager(RunManagerMixin):
Defaults to None.
inheritable_metadata (Optional[dict[str, Any]]): The inheritable metadata.
Defaults to None.
"""
self.run_id = run_id
self.handlers = handlers
@@ -493,6 +545,7 @@ class BaseRunManager(RunManagerMixin):
Returns:
BaseRunManager: The noop manager.
"""
return cls(
run_id=uuid.uuid4(),
@@ -545,6 +598,7 @@ class RunManager(BaseRunManager):
Args:
retry_state (RetryCallState): The retry state.
**kwargs (Any): Additional keyword arguments.
"""
if not self.handlers:
return
@@ -572,6 +626,7 @@ class ParentRunManager(RunManager):
Returns:
CallbackManager: The child callback manager.
"""
manager = CallbackManager(handlers=[], parent_run_id=self.run_id)
manager.set_handlers(self.inheritable_handlers)
@@ -591,6 +646,7 @@ class AsyncRunManager(BaseRunManager, ABC):
Returns:
RunManager: The sync RunManager.
"""
async def on_text(
@@ -606,6 +662,7 @@ class AsyncRunManager(BaseRunManager, ABC):
Returns:
Any: The result of the callback.
"""
if not self.handlers:
return
@@ -630,6 +687,7 @@ class AsyncRunManager(BaseRunManager, ABC):
Args:
retry_state (RetryCallState): The retry state.
**kwargs (Any): Additional keyword arguments.
"""
if not self.handlers:
return
@@ -657,6 +715,7 @@ class AsyncParentRunManager(AsyncRunManager):
Returns:
AsyncCallbackManager: The child callback manager.
"""
manager = AsyncCallbackManager(handlers=[], parent_run_id=self.run_id)
manager.set_handlers(self.inheritable_handlers)
@@ -674,7 +733,9 @@ class CallbackManagerForLLMRun(RunManager, LLMManagerMixin):
self,
token: str,
*,
chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]] = None,
chunk: Optional[
Union[GenerationChunk, ChatGenerationChunk, AIMessageChunk]
] = None,
**kwargs: Any,
) -> None:
"""Run when LLM generates a new token.
@@ -684,6 +745,7 @@ class CallbackManagerForLLMRun(RunManager, LLMManagerMixin):
chunk (Optional[Union[GenerationChunk, ChatGenerationChunk]], optional):
The chunk. Defaults to None.
**kwargs (Any): Additional keyword arguments.
"""
if not self.handlers:
return
@@ -699,12 +761,13 @@ class CallbackManagerForLLMRun(RunManager, LLMManagerMixin):
**kwargs,
)
def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None:
def on_llm_end(self, response: Union[LLMResult, AIMessage], **kwargs: Any) -> None:
"""Run when LLM ends running.
Args:
response (LLMResult): The LLM result.
response (LLMResult | AIMessage): The LLM result.
**kwargs (Any): Additional keyword arguments.
"""
if not self.handlers:
return
@@ -729,8 +792,9 @@ class CallbackManagerForLLMRun(RunManager, LLMManagerMixin):
Args:
error (Exception or KeyboardInterrupt): The error.
kwargs (Any): Additional keyword arguments.
- response (LLMResult): The response which was generated before
the error occurred.
- response (LLMResult | AIMessage): The response which was generated
before the error occurred.
"""
if not self.handlers:
return
@@ -754,6 +818,7 @@ class AsyncCallbackManagerForLLMRun(AsyncRunManager, LLMManagerMixin):
Returns:
CallbackManagerForLLMRun: The sync RunManager.
"""
return CallbackManagerForLLMRun(
run_id=self.run_id,
@@ -770,7 +835,9 @@ class AsyncCallbackManagerForLLMRun(AsyncRunManager, LLMManagerMixin):
self,
token: str,
*,
chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]] = None,
chunk: Optional[
Union[GenerationChunk, ChatGenerationChunk, AIMessageChunk]
] = None,
**kwargs: Any,
) -> None:
"""Run when LLM generates a new token.
@@ -780,6 +847,7 @@ class AsyncCallbackManagerForLLMRun(AsyncRunManager, LLMManagerMixin):
chunk (Optional[Union[GenerationChunk, ChatGenerationChunk]], optional):
The chunk. Defaults to None.
**kwargs (Any): Additional keyword arguments.
"""
if not self.handlers:
return
@@ -796,12 +864,15 @@ class AsyncCallbackManagerForLLMRun(AsyncRunManager, LLMManagerMixin):
)
@shielded
async def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None:
async def on_llm_end(
self, response: Union[LLMResult, AIMessage], **kwargs: Any
) -> None:
"""Run when LLM ends running.
Args:
response (LLMResult): The LLM result.
response (LLMResult | AIMessage): The LLM result.
**kwargs (Any): Additional keyword arguments.
"""
if not self.handlers:
return
@@ -827,10 +898,8 @@ class AsyncCallbackManagerForLLMRun(AsyncRunManager, LLMManagerMixin):
Args:
error (Exception or KeyboardInterrupt): The error.
kwargs (Any): Additional keyword arguments.
- response (LLMResult): The response which was generated before
the error occurred.
- response (LLMResult | AIMessage): The response which was generated
before the error occurred.
"""
if not self.handlers:
@@ -856,6 +925,7 @@ class CallbackManagerForChainRun(ParentRunManager, ChainManagerMixin):
Args:
outputs (Union[dict[str, Any], Any]): The outputs of the chain.
**kwargs (Any): Additional keyword arguments.
"""
if not self.handlers:
return
@@ -880,6 +950,7 @@ class CallbackManagerForChainRun(ParentRunManager, ChainManagerMixin):
Args:
error (Exception or KeyboardInterrupt): The error.
**kwargs (Any): Additional keyword arguments.
"""
if not self.handlers:
return
@@ -903,6 +974,7 @@ class CallbackManagerForChainRun(ParentRunManager, ChainManagerMixin):
Returns:
Any: The result of the callback.
"""
if not self.handlers:
return
@@ -926,6 +998,7 @@ class CallbackManagerForChainRun(ParentRunManager, ChainManagerMixin):
Returns:
Any: The result of the callback.
"""
if not self.handlers:
return
@@ -970,6 +1043,7 @@ class AsyncCallbackManagerForChainRun(AsyncParentRunManager, ChainManagerMixin):
Args:
outputs (Union[dict[str, Any], Any]): The outputs of the chain.
**kwargs (Any): Additional keyword arguments.
"""
if not self.handlers:
return
@@ -995,6 +1069,7 @@ class AsyncCallbackManagerForChainRun(AsyncParentRunManager, ChainManagerMixin):
Args:
error (Exception or KeyboardInterrupt): The error.
**kwargs (Any): Additional keyword arguments.
"""
if not self.handlers:
return
@@ -1018,6 +1093,7 @@ class AsyncCallbackManagerForChainRun(AsyncParentRunManager, ChainManagerMixin):
Returns:
Any: The result of the callback.
"""
if not self.handlers:
return
@@ -1041,6 +1117,7 @@ class AsyncCallbackManagerForChainRun(AsyncParentRunManager, ChainManagerMixin):
Returns:
Any: The result of the callback.
"""
if not self.handlers:
return
@@ -1069,6 +1146,7 @@ class CallbackManagerForToolRun(ParentRunManager, ToolManagerMixin):
Args:
output (Any): The output of the tool.
**kwargs (Any): The keyword arguments to pass to the event handler
"""
if not self.handlers:
return
@@ -1093,6 +1171,7 @@ class CallbackManagerForToolRun(ParentRunManager, ToolManagerMixin):
Args:
error (Exception or KeyboardInterrupt): The error.
**kwargs (Any): Additional keyword arguments.
"""
if not self.handlers:
return
@@ -1134,6 +1213,7 @@ class AsyncCallbackManagerForToolRun(AsyncParentRunManager, ToolManagerMixin):
Args:
output (Any): The output of the tool.
**kwargs (Any): Additional keyword arguments.
"""
if not self.handlers:
return
@@ -1158,6 +1238,7 @@ class AsyncCallbackManagerForToolRun(AsyncParentRunManager, ToolManagerMixin):
Args:
error (Exception or KeyboardInterrupt): The error.
**kwargs (Any): Additional keyword arguments.
"""
if not self.handlers:
return
@@ -1186,6 +1267,7 @@ class CallbackManagerForRetrieverRun(ParentRunManager, RetrieverManagerMixin):
Args:
documents (Sequence[Document]): The retrieved documents.
**kwargs (Any): Additional keyword arguments.
"""
if not self.handlers:
return
@@ -1210,6 +1292,7 @@ class CallbackManagerForRetrieverRun(ParentRunManager, RetrieverManagerMixin):
Args:
error (BaseException): The error.
**kwargs (Any): Additional keyword arguments.
"""
if not self.handlers:
return
@@ -1236,6 +1319,7 @@ class AsyncCallbackManagerForRetrieverRun(
Returns:
CallbackManagerForRetrieverRun: The sync RunManager.
"""
return CallbackManagerForRetrieverRun(
run_id=self.run_id,
@@ -1257,6 +1341,7 @@ class AsyncCallbackManagerForRetrieverRun(
Args:
documents (Sequence[Document]): The retrieved documents.
**kwargs (Any): Additional keyword arguments.
"""
if not self.handlers:
return
@@ -1282,6 +1367,7 @@ class AsyncCallbackManagerForRetrieverRun(
Args:
error (BaseException): The error.
**kwargs (Any): Additional keyword arguments.
"""
if not self.handlers:
return
@@ -1318,6 +1404,7 @@ class CallbackManager(BaseCallbackManager):
Returns:
list[CallbackManagerForLLMRun]: A callback manager for each
prompt as an LLM run.
"""
managers = []
for i, prompt in enumerate(prompts):
@@ -1354,7 +1441,7 @@ class CallbackManager(BaseCallbackManager):
def on_chat_model_start(
self,
serialized: dict[str, Any],
messages: list[list[BaseMessage]],
messages: Union[list[list[BaseMessage]], list[MessageV1]],
run_id: Optional[UUID] = None,
**kwargs: Any,
) -> list[CallbackManagerForLLMRun]:
@@ -1362,14 +1449,41 @@ class CallbackManager(BaseCallbackManager):
Args:
serialized (dict[str, Any]): The serialized LLM.
messages (list[list[BaseMessage]]): The list of messages.
messages (list[list[BaseMessage | MessageV1]]): The list of messages.
run_id (UUID, optional): The ID of the run. Defaults to None.
**kwargs (Any): Additional keyword arguments.
Returns:
list[CallbackManagerForLLMRun]: A callback manager for each
list of messages as an LLM run.
"""
if messages and isinstance(messages[0], MessageV1Types):
run_id_ = run_id if run_id is not None else uuid.uuid4()
handle_event(
self.handlers,
"on_chat_model_start",
"ignore_chat_model",
serialized,
messages,
run_id=run_id_,
parent_run_id=self.parent_run_id,
tags=self.tags,
metadata=self.metadata,
**kwargs,
)
return [
CallbackManagerForLLMRun(
run_id=run_id_,
handlers=self.handlers,
inheritable_handlers=self.inheritable_handlers,
parent_run_id=self.parent_run_id,
tags=self.tags,
inheritable_tags=self.inheritable_tags,
metadata=self.metadata,
inheritable_metadata=self.inheritable_metadata,
)
]
managers = []
for message_list in messages:
if run_id is not None:
@@ -1422,6 +1536,7 @@ class CallbackManager(BaseCallbackManager):
Returns:
CallbackManagerForChainRun: The callback manager for the chain run.
"""
if run_id is None:
run_id = uuid.uuid4()
@@ -1476,6 +1591,7 @@ class CallbackManager(BaseCallbackManager):
Returns:
CallbackManagerForToolRun: The callback manager for the tool run.
"""
if run_id is None:
run_id = uuid.uuid4()
@@ -1522,6 +1638,7 @@ class CallbackManager(BaseCallbackManager):
run_id (UUID, optional): The ID of the run. Defaults to None.
parent_run_id (UUID, optional): The ID of the parent run. Defaults to None.
**kwargs (Any): Additional keyword arguments.
"""
if run_id is None:
run_id = uuid.uuid4()
@@ -1569,6 +1686,7 @@ class CallbackManager(BaseCallbackManager):
run_id: The ID of the run. Defaults to None.
.. versionadded:: 0.2.14
"""
if not self.handlers:
return
@@ -1623,6 +1741,7 @@ class CallbackManager(BaseCallbackManager):
Returns:
CallbackManager: The configured callback manager.
"""
return _configure(
cls,
@@ -1657,6 +1776,7 @@ class CallbackManagerForChainGroup(CallbackManager):
parent_run_id (Optional[UUID]): The ID of the parent run. Defaults to None.
parent_run_manager (CallbackManagerForChainRun): The parent run manager.
**kwargs (Any): Additional keyword arguments.
"""
super().__init__(
handlers,
@@ -1745,6 +1865,7 @@ class CallbackManagerForChainGroup(CallbackManager):
Args:
outputs (Union[dict[str, Any], Any]): The outputs of the chain.
**kwargs (Any): Additional keyword arguments.
"""
self.ended = True
return self.parent_run_manager.on_chain_end(outputs, **kwargs)
@@ -1759,6 +1880,7 @@ class CallbackManagerForChainGroup(CallbackManager):
Args:
error (Exception or KeyboardInterrupt): The error.
**kwargs (Any): Additional keyword arguments.
"""
self.ended = True
return self.parent_run_manager.on_chain_error(error, **kwargs)
@@ -1864,7 +1986,7 @@ class AsyncCallbackManager(BaseCallbackManager):
async def on_chat_model_start(
self,
serialized: dict[str, Any],
messages: list[list[BaseMessage]],
messages: Union[list[list[BaseMessage]], list[MessageV1]],
run_id: Optional[UUID] = None,
**kwargs: Any,
) -> list[AsyncCallbackManagerForLLMRun]:
@@ -1872,7 +1994,7 @@ class AsyncCallbackManager(BaseCallbackManager):
Args:
serialized (dict[str, Any]): The serialized LLM.
messages (list[list[BaseMessage]]): The list of messages.
messages (list[list[BaseMessage | MessageV1]]): The list of messages.
run_id (UUID, optional): The ID of the run. Defaults to None.
**kwargs (Any): Additional keyword arguments.
@@ -1881,10 +2003,51 @@ class AsyncCallbackManager(BaseCallbackManager):
async callback managers, one for each LLM Run
corresponding to each inner message list.
"""
if messages and isinstance(messages[0], MessageV1Types):
run_id_ = run_id if run_id is not None else uuid.uuid4()
inline_tasks = []
non_inline_tasks = []
for handler in self.handlers:
task = ahandle_event(
[handler],
"on_chat_model_start",
"ignore_chat_model",
serialized,
messages,
run_id=run_id_,
parent_run_id=self.parent_run_id,
tags=self.tags,
metadata=self.metadata,
**kwargs,
)
if handler.run_inline:
inline_tasks.append(task)
else:
non_inline_tasks.append(task)
managers = [
AsyncCallbackManagerForLLMRun(
run_id=run_id_,
handlers=self.handlers,
inheritable_handlers=self.inheritable_handlers,
parent_run_id=self.parent_run_id,
tags=self.tags,
inheritable_tags=self.inheritable_tags,
metadata=self.metadata,
inheritable_metadata=self.inheritable_metadata,
)
]
# Run inline tasks sequentially
for task in inline_tasks:
await task
# Run non-inline tasks concurrently
if non_inline_tasks:
await asyncio.gather(*non_inline_tasks)
return managers
inline_tasks = []
non_inline_tasks = []
managers = []
for message_list in messages:
if run_id is not None:
run_id_ = run_id

View File

@@ -3,7 +3,7 @@
from __future__ import annotations
import sys
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Union
from typing_extensions import override
@@ -13,6 +13,7 @@ if TYPE_CHECKING:
from langchain_core.agents import AgentAction, AgentFinish
from langchain_core.messages import BaseMessage
from langchain_core.outputs import LLMResult
from langchain_core.v1.messages import AIMessage, MessageV1
class StreamingStdOutCallbackHandler(BaseCallbackHandler):
@@ -32,7 +33,7 @@ class StreamingStdOutCallbackHandler(BaseCallbackHandler):
def on_chat_model_start(
self,
serialized: dict[str, Any],
messages: list[list[BaseMessage]],
messages: Union[list[list[BaseMessage]], list[MessageV1]],
**kwargs: Any,
) -> None:
"""Run when LLM starts running.
@@ -54,7 +55,7 @@ class StreamingStdOutCallbackHandler(BaseCallbackHandler):
sys.stdout.write(token)
sys.stdout.flush()
def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None:
def on_llm_end(self, response: Union[LLMResult, AIMessage], **kwargs: Any) -> None:
"""Run when LLM ends running.
Args:

View File

@@ -4,14 +4,16 @@ import threading
from collections.abc import Generator
from contextlib import contextmanager
from contextvars import ContextVar
from typing import Any, Optional
from typing import Any, Optional, Union
from typing_extensions import override
from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.messages import AIMessage
from langchain_core.messages.ai import UsageMetadata, add_usage
from langchain_core.messages.utils import convert_from_v1_message
from langchain_core.outputs import ChatGeneration, LLMResult
from langchain_core.v1.messages import AIMessage as AIMessageV1
class UsageMetadataCallbackHandler(BaseCallbackHandler):
@@ -58,9 +60,17 @@ class UsageMetadataCallbackHandler(BaseCallbackHandler):
return str(self.usage_metadata)
@override
def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None:
def on_llm_end(
self, response: Union[LLMResult, AIMessageV1], **kwargs: Any
) -> None:
"""Collect token usage."""
# Check for usage_metadata (langchain-core >= 0.2.2)
if isinstance(response, AIMessageV1):
response = LLMResult(
generations=[
[ChatGeneration(message=convert_from_v1_message(response))]
]
)
try:
generation = response.generations[0][0]
except IndexError:

View File

@@ -117,9 +117,9 @@ class BaseChatMessageHistory(ABC):
def add_user_message(self, message: Union[HumanMessage, str]) -> None:
"""Convenience method for adding a human message string to the store.
Please note that this is a convenience method. Code should favor the
bulk add_messages interface instead to save on round-trips to the underlying
persistence layer.
.. note::
This is a convenience method. Code should favor the bulk ``add_messages``
interface instead to save on round-trips to the persistence layer.
This method may be deprecated in a future release.
@@ -134,9 +134,9 @@ class BaseChatMessageHistory(ABC):
def add_ai_message(self, message: Union[AIMessage, str]) -> None:
"""Convenience method for adding an AI message string to the store.
Please note that this is a convenience method. Code should favor the bulk
add_messages interface instead to save on round-trips to the underlying
persistence layer.
.. note::
This is a convenience method. Code should favor the bulk ``add_messages``
interface instead to save on round-trips to the persistence layer.
This method may be deprecated in a future release.

View File

@@ -19,17 +19,18 @@ if TYPE_CHECKING:
class BaseDocumentCompressor(BaseModel, ABC):
"""Base class for document compressors.
This abstraction is primarily used for
post-processing of retrieved documents.
This abstraction is primarily used for post-processing of retrieved documents.
Documents matching a given query are first retrieved.
Then the list of documents can be further processed.
For example, one could re-rank the retrieved documents
using an LLM.
For example, one could re-rank the retrieved documents using an LLM.
.. note::
Users should favor using a RunnableLambda instead of sub-classing from this
interface.
**Note** users should favor using a RunnableLambda
instead of sub-classing from this interface.
"""
@abstractmethod
@@ -48,6 +49,7 @@ class BaseDocumentCompressor(BaseModel, ABC):
Returns:
The compressed documents.
"""
async def acompress_documents(
@@ -65,6 +67,7 @@ class BaseDocumentCompressor(BaseModel, ABC):
Returns:
The compressed documents.
"""
return await run_in_executor(
None, self.compress_documents, documents, query, callbacks

View File

@@ -488,8 +488,8 @@ class DeleteResponse(TypedDict, total=False):
failed: Sequence[str]
"""The IDs that failed to be deleted.
Please note that deleting an ID that
does not exist is **NOT** considered a failure.
.. warning::
Deleting an ID that does not exist is **NOT** considered a failure.
"""
num_failed: int

View File

@@ -1,8 +1,10 @@
import copy
import re
from collections.abc import Sequence
from typing import Optional
from langchain_core.messages import BaseMessage
from langchain_core.v1.messages import MessageV1
def _is_openai_data_block(block: dict) -> bool:
@@ -138,3 +140,37 @@ def _normalize_messages(messages: Sequence[BaseMessage]) -> list[BaseMessage]:
formatted_messages.append(formatted_message)
return formatted_messages
def _normalize_messages_v1(messages: Sequence[MessageV1]) -> list[MessageV1]:
"""Extend support for message formats.
Chat models implement support for images in OpenAI Chat Completions format, as well
as other multimodal data as standard data blocks. This function extends support to
audio and file data in OpenAI Chat Completions format by converting them to standard
data blocks.
"""
formatted_messages = []
for message in messages:
formatted_message = message
if isinstance(message.content, list):
for idx, block in enumerate(message.content):
if (
isinstance(block, dict)
# Subset to (PDF) files and audio, as most relevant chat models
# support images in OAI format (and some may not yet support the
# standard data block format)
and block.get("type") in {"file", "input_audio"}
and _is_openai_data_block(block) # type: ignore[arg-type]
):
if formatted_message is message:
formatted_message = copy.copy(message)
# Also shallow-copy content
formatted_message.content = list(formatted_message.content)
formatted_message.content[idx] = ( # type: ignore[call-overload]
_convert_openai_format_to_data_block(block) # type: ignore[arg-type]
)
formatted_messages.append(formatted_message)
return formatted_messages

View File

@@ -31,6 +31,7 @@ from langchain_core.messages import (
from langchain_core.prompt_values import PromptValue
from langchain_core.runnables import Runnable, RunnableSerializable
from langchain_core.utils import get_pydantic_field_names
from langchain_core.v1.messages import AIMessage as AIMessageV1
if TYPE_CHECKING:
from langchain_core.outputs import LLMResult
@@ -57,8 +58,8 @@ class LangSmithParams(TypedDict, total=False):
def get_tokenizer() -> Any:
"""Get a GPT-2 tokenizer instance.
This function is cached to avoid re-loading the tokenizer
every time it is called.
This function is cached to avoid re-loading the tokenizer every time it is called.
"""
try:
from transformers import GPT2TokenizerFast # type: ignore[import-not-found]
@@ -85,7 +86,9 @@ def _get_token_ids_default_method(text: str) -> list[int]:
LanguageModelInput = Union[PromptValue, str, Sequence[MessageLikeRepresentation]]
LanguageModelOutput = Union[BaseMessage, str]
LanguageModelLike = Runnable[LanguageModelInput, LanguageModelOutput]
LanguageModelOutputVar = TypeVar("LanguageModelOutputVar", BaseMessage, str)
LanguageModelOutputVar = TypeVar(
"LanguageModelOutputVar", BaseMessage, str, AIMessageV1
)
def _get_verbosity() -> bool:
@@ -99,7 +102,8 @@ class BaseLanguageModel(
):
"""Abstract base class for interfacing with language models.
All language model wrappers inherited from BaseLanguageModel.
All language model wrappers inherited from ``BaseLanguageModel``.
"""
cache: Union[BaseCache, bool, None] = Field(default=None, exclude=True)
@@ -108,9 +112,10 @@ class BaseLanguageModel(
* If true, will use the global cache.
* If false, will not use a cache
* If None, will use the global cache if it's set, otherwise no cache.
* If instance of BaseCache, will use the provided cache.
* If instance of ``BaseCache``, will use the provided cache.
Caching is not currently supported for streaming methods of models.
"""
verbose: bool = Field(default_factory=_get_verbosity, exclude=True, repr=False)
"""Whether to print out response text."""
@@ -140,6 +145,7 @@ class BaseLanguageModel(
Returns:
The verbosity setting to use.
"""
if verbose is None:
return _get_verbosity()
@@ -195,7 +201,8 @@ class BaseLanguageModel(
Returns:
An LLMResult, which contains a list of candidate Generations for each input
prompt and additional model provider-specific output.
prompt and additional model provider-specific output.
"""
@abstractmethod
@@ -229,8 +236,9 @@ class BaseLanguageModel(
to the model provider API call.
Returns:
An LLMResult, which contains a list of candidate Generations for each input
prompt and additional model provider-specific output.
An ``LLMResult``, which contains a list of candidate Generations for each
input prompt and additional model provider-specific output.
"""
def with_structured_output(
@@ -248,8 +256,8 @@ class BaseLanguageModel(
) -> str:
"""Pass a single string input to the model and return a string.
Use this method when passing in raw text. If you want to pass in specific
types of chat messages, use predict_messages.
Use this method when passing in raw text. If you want to pass in specific types
of chat messages, use predict_messages.
Args:
text: String input to pass to the model.
@@ -260,6 +268,7 @@ class BaseLanguageModel(
Returns:
Top model prediction as a string.
"""
@deprecated("0.1.7", alternative="invoke", removal="1.0")
@@ -274,7 +283,7 @@ class BaseLanguageModel(
"""Pass a message sequence to the model and return a message.
Use this method when passing in chat messages. If you want to pass in raw text,
use predict.
use predict.
Args:
messages: A sequence of chat messages corresponding to a single model input.
@@ -285,6 +294,7 @@ class BaseLanguageModel(
Returns:
Top model prediction as a message.
"""
@deprecated("0.1.7", alternative="ainvoke", removal="1.0")
@@ -295,7 +305,7 @@ class BaseLanguageModel(
"""Asynchronously pass a string to the model and return a string.
Use this method when calling pure text generation models and only the top
candidate generation is needed.
candidate generation is needed.
Args:
text: String input to pass to the model.
@@ -306,6 +316,7 @@ class BaseLanguageModel(
Returns:
Top model prediction as a string.
"""
@deprecated("0.1.7", alternative="ainvoke", removal="1.0")
@@ -319,8 +330,8 @@ class BaseLanguageModel(
) -> BaseMessage:
"""Asynchronously pass messages to the model and return a message.
Use this method when calling chat models and only the top
candidate generation is needed.
Use this method when calling chat models and only the top candidate generation
is needed.
Args:
messages: A sequence of chat messages corresponding to a single model input.
@@ -331,6 +342,7 @@ class BaseLanguageModel(
Returns:
Top model prediction as a message.
"""
@property
@@ -346,7 +358,8 @@ class BaseLanguageModel(
Returns:
A list of ids corresponding to the tokens in the text, in order they occur
in the text.
in the text.
"""
if self.custom_get_token_ids is not None:
return self.custom_get_token_ids(text)
@@ -362,6 +375,7 @@ class BaseLanguageModel(
Returns:
The integer number of tokens in the text.
"""
return len(self.get_token_ids(text))
@@ -374,16 +388,18 @@ class BaseLanguageModel(
Useful for checking if an input fits in a model's context window.
**Note**: the base implementation of get_num_tokens_from_messages ignores
tool schemas.
.. note::
The base implementation of ``get_num_tokens_from_messages`` ignores tool
schemas.
Args:
messages: The message inputs to tokenize.
tools: If provided, sequence of dict, BaseModel, function, or BaseTools
to be converted to tool schemas.
tools: If provided, sequence of dict, ``BaseModel``, function, or
``BaseTools`` to be converted to tool schemas.
Returns:
The sum of the number of tokens across the messages.
"""
if tools is not None:
warnings.warn(
@@ -396,6 +412,7 @@ class BaseLanguageModel(
def _all_required_field_names(cls) -> set:
"""DEPRECATED: Kept for backwards compatibility.
Use get_pydantic_field_names.
Use ``get_pydantic_field_names``.
"""
return get_pydantic_field_names(cls)

View File

@@ -97,17 +97,18 @@ def _generate_response_from_error(error: BaseException) -> list[ChatGeneration]:
def _format_for_tracing(messages: list[BaseMessage]) -> list[BaseMessage]:
"""Format messages for tracing in on_chat_model_start.
"""Format messages for tracing in ``on_chat_model_start``.
- Update image content blocks to OpenAI Chat Completions format (backward
compatibility).
- Add "type" key to content blocks that have a single key.
- Add ``type`` key to content blocks that have a single key.
Args:
messages: List of messages to format.
Returns:
List of messages formatted for tracing.
"""
messages_to_trace = []
for message in messages:
@@ -153,10 +154,11 @@ def generate_from_stream(stream: Iterator[ChatGenerationChunk]) -> ChatResult:
"""Generate from a stream.
Args:
stream: Iterator of ChatGenerationChunk.
stream: Iterator of ``ChatGenerationChunk``.
Returns:
ChatResult: Chat result.
"""
generation = next(stream, None)
if generation:
@@ -180,10 +182,11 @@ async def agenerate_from_stream(
"""Async generate from a stream.
Args:
stream: Iterator of ChatGenerationChunk.
stream: Iterator of ``ChatGenerationChunk``.
Returns:
ChatResult: Chat result.
"""
chunks = [chunk async for chunk in stream]
return await run_in_executor(None, generate_from_stream, iter(chunks))
@@ -311,15 +314,16 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
provided. This offers the best of both worlds.
- If False (default), will always use streaming case if available.
The main reason for this flag is that code might be written using ``.stream()`` and
The main reason for this flag is that code might be written using ``stream()`` and
a user may want to swap out a given model for another model whose the implementation
does not properly support streaming.
"""
@model_validator(mode="before")
@classmethod
def raise_deprecation(cls, values: dict) -> Any:
"""Raise deprecation warning if callback_manager is used.
"""Raise deprecation warning if ``callback_manager`` is used.
Args:
values (Dict): Values to validate.
@@ -328,7 +332,8 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
Dict: Validated values.
Raises:
DeprecationWarning: If callback_manager is used.
DeprecationWarning: If ``callback_manager`` is used.
"""
if values.get("callback_manager") is not None:
warnings.warn(
@@ -653,6 +658,7 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
Returns:
List of ChatGeneration objects.
"""
converted_generations = []
for gen in cache_val:
@@ -778,7 +784,8 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
Returns:
An LLMResult, which contains a list of candidate Generations for each input
prompt and additional model provider-specific output.
prompt and additional model provider-specific output.
"""
ls_structured_output_format = kwargs.pop(
"ls_structured_output_format", None
@@ -892,7 +899,8 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
Returns:
An LLMResult, which contains a list of candidate Generations for each input
prompt and additional model provider-specific output.
prompt and additional model provider-specific output.
"""
ls_structured_output_format = kwargs.pop(
"ls_structured_output_format", None
@@ -1248,6 +1256,7 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
Returns:
The model output message.
"""
generation = self.generate(
[messages], stop=stop, callbacks=callbacks, **kwargs
@@ -1288,6 +1297,7 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
Returns:
The model output string.
"""
return self.predict(message, stop=stop, **kwargs)
@@ -1307,6 +1317,7 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
Returns:
The predicted output string.
"""
stop_ = None if stop is None else list(stop)
result = self([HumanMessage(content=text)], stop=stop_, **kwargs)
@@ -1382,6 +1393,7 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
Returns:
A Runnable that returns a message.
"""
raise NotImplementedError
@@ -1544,8 +1556,10 @@ class BaseChatModel(BaseLanguageModel[BaseMessage], ABC):
class SimpleChatModel(BaseChatModel):
"""Simplified implementation for a chat model to inherit from.
**Note** This implementation is primarily here for backwards compatibility.
For new implementations, please use `BaseChatModel` directly.
.. note::
This implementation is primarily here for backwards compatibility. For new
implementations, please use ``BaseChatModel`` directly.
"""
def _generate(

View File

@@ -3,7 +3,7 @@
import asyncio
import re
import time
from collections.abc import AsyncIterator, Iterator
from collections.abc import AsyncIterator, Iterable, Iterator
from typing import Any, Optional, Union, cast
from typing_extensions import override
@@ -16,6 +16,10 @@ from langchain_core.language_models.chat_models import BaseChatModel, SimpleChat
from langchain_core.messages import AIMessage, AIMessageChunk, BaseMessage
from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult
from langchain_core.runnables import RunnableConfig
from langchain_core.v1.chat_models import BaseChatModel as BaseChatModelV1
from langchain_core.v1.messages import AIMessage as AIMessageV1
from langchain_core.v1.messages import AIMessageChunk as AIMessageChunkV1
from langchain_core.v1.messages import MessageV1
class FakeMessagesListChatModel(BaseChatModel):
@@ -223,11 +227,12 @@ class GenericFakeChatModel(BaseChatModel):
This can be expanded to accept other types like Callables / dicts / strings
to make the interface more generic if needed.
Note: if you want to pass a list, you can use `iter` to convert it to an iterator.
.. note::
if you want to pass a list, you can use ``iter`` to convert it to an iterator.
Please note that 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.
.. 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
@@ -367,3 +372,69 @@ class ParrotFakeChatModel(BaseChatModel):
@property
def _llm_type(self) -> str:
return "parrot-fake-chat-model"
class GenericFakeChatModelV1(BaseChatModelV1):
"""Generic fake chat model that can be used to test the chat model interface."""
messages: Optional[Iterator[Union[AIMessageV1, str]]] = None
message_chunks: Optional[Iterable[Union[AIMessageChunkV1, str]]] = None
@override
def _invoke(
self,
messages: list[MessageV1],
run_manager: Optional[CallbackManagerForLLMRun] = None,
**kwargs: Any,
) -> AIMessageV1:
"""Top Level call."""
if self.messages is None:
error_msg = "Messages iterator is not set."
raise ValueError(error_msg)
message = next(self.messages)
return AIMessageV1(content=message) if isinstance(message, str) else message
@override
def _stream(
self,
messages: list[MessageV1],
run_manager: Optional[CallbackManagerForLLMRun] = None,
**kwargs: Any,
) -> Iterator[AIMessageChunkV1]:
"""Top Level call."""
if self.message_chunks is None:
error_msg = "Message chunks iterator is not set."
raise ValueError(error_msg)
for chunk in self.message_chunks:
if isinstance(chunk, str):
yield AIMessageChunkV1(chunk)
else:
yield chunk
@property
def _llm_type(self) -> str:
return "generic-fake-chat-model"
class ParrotFakeChatModelV1(BaseChatModelV1):
"""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
def _invoke(
self,
messages: list[MessageV1],
stop: Optional[list[str]] = None,
run_manager: Optional[CallbackManagerForLLMRun] = None,
**kwargs: Any,
) -> AIMessageV1:
"""Top Level call."""
if isinstance(messages[-1], AIMessageV1):
return messages[-1]
return AIMessageV1(content=messages[-1].content)
@property
def _llm_type(self) -> str:
return "parrot-fake-chat-model"

View File

@@ -1,11 +1,14 @@
"""Dump objects to json."""
import dataclasses
import inspect
import json
from typing import Any
from pydantic import BaseModel
from langchain_core.load.serializable import Serializable, to_json_not_implemented
from langchain_core.v1.messages import MessageV1Types
def default(obj: Any) -> Any:
@@ -19,6 +22,24 @@ def default(obj: Any) -> Any:
"""
if isinstance(obj, Serializable):
return obj.to_json()
# Handle v1 message classes
if type(obj) in MessageV1Types:
# Get the constructor signature to only include valid parameters
init_sig = inspect.signature(type(obj).__init__)
valid_params = set(init_sig.parameters.keys()) - {"self"}
# Filter dataclass fields to only include constructor params
all_fields = dataclasses.asdict(obj)
kwargs = {k: v for k, v in all_fields.items() if k in valid_params}
return {
"lc": 1,
"type": "constructor",
"id": ["langchain_core", "v1", "messages", type(obj).__name__],
"kwargs": kwargs,
}
return to_json_not_implemented(obj)
@@ -73,10 +94,9 @@ def dumps(obj: Any, *, pretty: bool = False, **kwargs: Any) -> str:
def dumpd(obj: Any) -> Any:
"""Return a dict representation of an object.
Note:
Unfortunately this function is not as efficient as it could be
because it first dumps the object to a json string and then loads it
back into a dictionary.
.. note::
Unfortunately this function is not as efficient as it could be because it first
dumps the object to a json string and then loads it back into a dictionary.
Args:
obj: The object to dump.

View File

@@ -156,8 +156,13 @@ class Reviver:
cls = getattr(mod, name)
# The class must be a subclass of Serializable.
if not issubclass(cls, Serializable):
# Import MessageV1Types lazily to avoid circular import:
# load.load -> v1.messages -> messages.ai -> messages.base ->
# load.serializable -> load.__init__ -> load.load
from langchain_core.v1.messages import MessageV1Types
# The class must be a subclass of Serializable or a v1 message class.
if not (issubclass(cls, Serializable) or cls in MessageV1Types):
msg = f"Invalid namespace: {value}"
raise ValueError(msg)

View File

@@ -33,9 +33,31 @@ if TYPE_CHECKING:
)
from langchain_core.messages.chat import ChatMessage, ChatMessageChunk
from langchain_core.messages.content_blocks import (
Annotation,
AudioContentBlock,
Citation,
CodeInterpreterCall,
CodeInterpreterOutput,
CodeInterpreterResult,
ContentBlock,
DataContentBlock,
FileContentBlock,
ImageContentBlock,
NonStandardAnnotation,
NonStandardContentBlock,
PlainTextContentBlock,
ReasoningContentBlock,
TextContentBlock,
VideoContentBlock,
WebSearchCall,
WebSearchResult,
convert_to_openai_data_block,
convert_to_openai_image_block,
is_data_content_block,
is_reasoning_block,
is_text_block,
is_tool_call_block,
is_tool_call_chunk,
)
from langchain_core.messages.function import FunctionMessage, FunctionMessageChunk
from langchain_core.messages.human import HumanMessage, HumanMessageChunk
@@ -65,24 +87,42 @@ if TYPE_CHECKING:
__all__ = (
"AIMessage",
"AIMessageChunk",
"Annotation",
"AnyMessage",
"AudioContentBlock",
"BaseMessage",
"BaseMessageChunk",
"ChatMessage",
"ChatMessageChunk",
"Citation",
"CodeInterpreterCall",
"CodeInterpreterOutput",
"CodeInterpreterResult",
"ContentBlock",
"DataContentBlock",
"FileContentBlock",
"FunctionMessage",
"FunctionMessageChunk",
"HumanMessage",
"HumanMessageChunk",
"ImageContentBlock",
"InvalidToolCall",
"MessageLikeRepresentation",
"NonStandardAnnotation",
"NonStandardContentBlock",
"PlainTextContentBlock",
"ReasoningContentBlock",
"RemoveMessage",
"SystemMessage",
"SystemMessageChunk",
"TextContentBlock",
"ToolCall",
"ToolCallChunk",
"ToolMessage",
"ToolMessageChunk",
"VideoContentBlock",
"WebSearchCall",
"WebSearchResult",
"_message_from_dict",
"convert_to_messages",
"convert_to_openai_data_block",
@@ -91,6 +131,10 @@ __all__ = (
"filter_messages",
"get_buffer_string",
"is_data_content_block",
"is_reasoning_block",
"is_text_block",
"is_tool_call_block",
"is_tool_call_chunk",
"merge_content",
"merge_message_runs",
"message_chunk_to_message",
@@ -103,25 +147,43 @@ __all__ = (
_dynamic_imports = {
"AIMessage": "ai",
"AIMessageChunk": "ai",
"Annotation": "content_blocks",
"AudioContentBlock": "content_blocks",
"BaseMessage": "base",
"BaseMessageChunk": "base",
"merge_content": "base",
"message_to_dict": "base",
"messages_to_dict": "base",
"Citation": "content_blocks",
"ContentBlock": "content_blocks",
"ChatMessage": "chat",
"ChatMessageChunk": "chat",
"CodeInterpreterCall": "content_blocks",
"CodeInterpreterOutput": "content_blocks",
"CodeInterpreterResult": "content_blocks",
"DataContentBlock": "content_blocks",
"FileContentBlock": "content_blocks",
"FunctionMessage": "function",
"FunctionMessageChunk": "function",
"HumanMessage": "human",
"HumanMessageChunk": "human",
"NonStandardAnnotation": "content_blocks",
"NonStandardContentBlock": "content_blocks",
"PlainTextContentBlock": "content_blocks",
"ReasoningContentBlock": "content_blocks",
"RemoveMessage": "modifier",
"SystemMessage": "system",
"SystemMessageChunk": "system",
"WebSearchCall": "content_blocks",
"WebSearchResult": "content_blocks",
"ImageContentBlock": "content_blocks",
"InvalidToolCall": "tool",
"TextContentBlock": "content_blocks",
"ToolCall": "tool",
"ToolCallChunk": "tool",
"ToolMessage": "tool",
"ToolMessageChunk": "tool",
"VideoContentBlock": "content_blocks",
"AnyMessage": "utils",
"MessageLikeRepresentation": "utils",
"_message_from_dict": "utils",
@@ -132,6 +194,10 @@ _dynamic_imports = {
"filter_messages": "utils",
"get_buffer_string": "utils",
"is_data_content_block": "content_blocks",
"is_reasoning_block": "content_blocks",
"is_text_block": "content_blocks",
"is_tool_call_block": "content_blocks",
"is_tool_call_chunk": "content_blocks",
"merge_message_runs": "utils",
"message_chunk_to_message": "utils",
"messages_from_dict": "utils",

View File

@@ -8,11 +8,7 @@ from typing import Any, Literal, Optional, Union, cast
from pydantic import model_validator
from typing_extensions import NotRequired, Self, TypedDict, override
from langchain_core.messages.base import (
BaseMessage,
BaseMessageChunk,
merge_content,
)
from langchain_core.messages.base import BaseMessage, BaseMessageChunk, merge_content
from langchain_core.messages.tool import (
InvalidToolCall,
ToolCall,
@@ -20,23 +16,26 @@ from langchain_core.messages.tool import (
default_tool_chunk_parser,
default_tool_parser,
)
from langchain_core.messages.tool import (
invalid_tool_call as create_invalid_tool_call,
)
from langchain_core.messages.tool import (
tool_call as create_tool_call,
)
from langchain_core.messages.tool import (
tool_call_chunk as create_tool_call_chunk,
)
from langchain_core.messages.tool import invalid_tool_call as create_invalid_tool_call
from langchain_core.messages.tool import tool_call as create_tool_call
from langchain_core.messages.tool import tool_call_chunk as create_tool_call_chunk
from langchain_core.utils._merge import merge_dicts, merge_lists
from langchain_core.utils.json import parse_partial_json
from langchain_core.utils.usage import _dict_int_op
logger = logging.getLogger(__name__)
_LC_AUTO_PREFIX = "lc_"
"""LangChain auto-generated ID prefix for messages and content blocks."""
_LC_ID_PREFIX = "run-"
_LC_ID_PREFIX = f"{_LC_AUTO_PREFIX}run-"
"""Internal tracing/callback system identifier.
Used for:
- Tracing. Every LangChain operation (LLM call, chain execution, tool use, etc.)
gets a unique run_id (UUID)
- Enables tracking parent-child relationships between operations
"""
class InputTokenDetails(TypedDict, total=False):
@@ -428,17 +427,27 @@ def add_ai_message_chunks(
chunk_id = None
candidates = [left.id] + [o.id for o in others]
# first pass: pick the first non-run-* id
# first pass: pick the first provider-assigned id (non-run-* and non-lc_*)
for id_ in candidates:
if id_ and not id_.startswith(_LC_ID_PREFIX):
if (
id_
and not id_.startswith(_LC_ID_PREFIX)
and not id_.startswith(_LC_AUTO_PREFIX)
):
chunk_id = id_
break
else:
# second pass: no provider-assigned id found, just take the first non-null
# second pass: prefer lc_run-* ids over lc_* ids
for id_ in candidates:
if id_:
if id_ and id_.startswith(_LC_ID_PREFIX):
chunk_id = id_
break
else:
# third pass: take any remaining id (auto-generated lc_* ids)
for id_ in candidates:
if id_:
chunk_id = id_
break
return left.__class__(
example=left.example,

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ class RemoveMessage(BaseMessage):
def __init__(
self,
id: str, # noqa: A002
id: str,
**kwargs: Any,
) -> None:
"""Create a RemoveMessage.

View File

@@ -5,9 +5,12 @@ from typing import Any, Literal, Optional, Union
from uuid import UUID
from pydantic import Field, model_validator
from typing_extensions import NotRequired, TypedDict, override
from typing_extensions import override
from langchain_core.messages.base import BaseMessage, BaseMessageChunk, merge_content
from langchain_core.messages.content_blocks import InvalidToolCall as InvalidToolCall
from langchain_core.messages.content_blocks import ToolCall as ToolCall
from langchain_core.messages.content_blocks import ToolCallChunk as ToolCallChunk
from langchain_core.utils._merge import merge_dicts, merge_obj
@@ -177,42 +180,11 @@ class ToolMessageChunk(ToolMessage, BaseMessageChunk):
return super().__add__(other)
class ToolCall(TypedDict):
"""Represents a request to call a tool.
Example:
.. code-block:: python
{
"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".
"""
name: str
"""The name of the tool to be called."""
args: dict[str, Any]
"""The arguments to the tool call."""
id: Optional[str]
"""An identifier associated with the tool call.
An identifier is needed to associate a tool call request with a tool
call result in events when multiple concurrent tool calls are made.
"""
type: NotRequired[Literal["tool_call"]]
def tool_call(
*,
name: str,
args: dict[str, Any],
id: Optional[str], # noqa: A002
id: Optional[str],
) -> ToolCall:
"""Create a tool call.
@@ -224,43 +196,11 @@ def tool_call(
return ToolCall(name=name, args=args, id=id, type="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__),
all string attributes are concatenated. Chunks are only merged if their
values of `index` are equal and not None.
Example:
.. code-block:: python
left_chunks = [ToolCallChunk(name="foo", args='{"a":', index=0)]
right_chunks = [ToolCallChunk(name=None, args='1}', index=0)]
(
AIMessageChunk(content="", tool_call_chunks=left_chunks)
+ AIMessageChunk(content="", tool_call_chunks=right_chunks)
).tool_call_chunks == [ToolCallChunk(name='foo', args='{"a":1}', index=0)]
"""
name: Optional[str]
"""The name of the tool to be called."""
args: Optional[str]
"""The arguments to the tool call."""
id: Optional[str]
"""An identifier associated with the tool call."""
index: Optional[int]
"""The index of the tool call in a sequence."""
type: NotRequired[Literal["tool_call_chunk"]]
def tool_call_chunk(
*,
name: Optional[str] = None,
args: Optional[str] = None,
id: Optional[str] = None, # noqa: A002
id: Optional[str] = None,
index: Optional[int] = None,
) -> ToolCallChunk:
"""Create a tool call chunk.
@@ -276,29 +216,11 @@ 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
(e.g., invalid JSON arguments.)
"""
name: Optional[str]
"""The name of the tool to be called."""
args: Optional[str]
"""The arguments to the tool call."""
id: Optional[str]
"""An identifier associated with the tool call."""
error: Optional[str]
"""An error message associated with the tool call."""
type: NotRequired[Literal["invalid_tool_call"]]
def invalid_tool_call(
*,
name: Optional[str] = None,
args: Optional[str] = None,
id: Optional[str] = None, # noqa: A002
id: Optional[str] = None,
error: Optional[str] = None,
) -> InvalidToolCall:
"""Create an invalid tool call.

View File

@@ -35,11 +35,18 @@ from langchain_core.messages import convert_to_openai_data_block, is_data_conten
from langchain_core.messages.ai import AIMessage, AIMessageChunk
from langchain_core.messages.base import BaseMessage, BaseMessageChunk
from langchain_core.messages.chat import ChatMessage, ChatMessageChunk
from langchain_core.messages.content_blocks import ContentBlock
from langchain_core.messages.function import FunctionMessage, FunctionMessageChunk
from langchain_core.messages.human import HumanMessage, HumanMessageChunk
from langchain_core.messages.modifier import RemoveMessage
from langchain_core.messages.system import SystemMessage, SystemMessageChunk
from langchain_core.messages.tool import ToolCall, ToolMessage, ToolMessageChunk
from langchain_core.v1.messages import AIMessage as AIMessageV1
from langchain_core.v1.messages import AIMessageChunk as AIMessageChunkV1
from langchain_core.v1.messages import HumanMessage as HumanMessageV1
from langchain_core.v1.messages import MessageV1, MessageV1Types, ResponseMetadata
from langchain_core.v1.messages import SystemMessage as SystemMessageV1
from langchain_core.v1.messages import ToolMessage as ToolMessageV1
if TYPE_CHECKING:
from langchain_text_splitters import TextSplitter
@@ -203,7 +210,7 @@ def message_chunk_to_message(chunk: BaseMessageChunk) -> BaseMessage:
MessageLikeRepresentation = Union[
BaseMessage, list[str], tuple[str, str], str, dict[str, Any]
BaseMessage, list[str], tuple[str, str], str, dict[str, Any], MessageV1
]
@@ -213,7 +220,7 @@ def _create_message_from_message_type(
name: Optional[str] = None,
tool_call_id: Optional[str] = None,
tool_calls: Optional[list[dict[str, Any]]] = None,
id: Optional[str] = None, # noqa: A002
id: Optional[str] = None,
**additional_kwargs: Any,
) -> BaseMessage:
"""Create a message from a message type and content string.
@@ -294,6 +301,130 @@ def _create_message_from_message_type(
return message
def _create_message_from_message_type_v1(
message_type: str,
content: str,
name: Optional[str] = None,
tool_call_id: Optional[str] = None,
tool_calls: Optional[list[dict[str, Any]]] = None,
id: Optional[str] = None,
**kwargs: Any,
) -> MessageV1:
"""Create a message from a message type and content string.
Args:
message_type: (str) the type of the message (e.g., "human", "ai", etc.).
content: (str) the content string.
name: (str) the name of the message. Default is None.
tool_call_id: (str) the tool call id. Default is None.
tool_calls: (list[dict[str, Any]]) the tool calls. Default is None.
id: (str) the id of the message. Default is None.
kwargs: (dict[str, Any]) additional keyword arguments.
Returns:
a message of the appropriate type.
Raises:
ValueError: if the message type is not one of "human", "user", "ai",
"assistant", "tool", "system", or "developer".
"""
if name is not None:
kwargs["name"] = name
if tool_call_id is not None:
kwargs["tool_call_id"] = tool_call_id
if kwargs and (response_metadata := kwargs.pop("response_metadata", None)):
kwargs["response_metadata"] = response_metadata
if id is not None:
kwargs["id"] = id
if tool_calls is not None:
kwargs["tool_calls"] = []
for tool_call in tool_calls:
# Convert OpenAI-format tool call to LangChain format.
if "function" in tool_call:
args = tool_call["function"]["arguments"]
if isinstance(args, str):
args = json.loads(args, strict=False)
kwargs["tool_calls"].append(
{
"name": tool_call["function"]["name"],
"args": args,
"id": tool_call["id"],
"type": "tool_call",
}
)
else:
kwargs["tool_calls"].append(tool_call)
if message_type in {"human", "user"}:
message: MessageV1 = HumanMessageV1(content=content, **kwargs)
elif message_type in {"ai", "assistant"}:
message = AIMessageV1(content=content, **kwargs)
elif message_type in {"system", "developer"}:
if message_type == "developer":
kwargs["custom_role"] = "developer"
message = SystemMessageV1(content=content, **kwargs)
elif message_type == "tool":
artifact = kwargs.pop("artifact", None)
message = ToolMessageV1(content=content, artifact=artifact, **kwargs)
else:
msg = (
f"Unexpected message type: '{message_type}'. Use one of 'human',"
f" 'user', 'ai', 'assistant', 'function', 'tool', 'system', or 'developer'."
)
msg = create_message(message=msg, error_code=ErrorCode.MESSAGE_COERCION_FAILURE)
raise ValueError(msg)
return message
def convert_from_v1_message(message: MessageV1) -> BaseMessage:
"""Compatibility layer to convert v1 messages to current messages.
Args:
message: MessageV1 instance to convert.
Returns:
BaseMessage: Converted message instance.
"""
content = cast("Union[str, list[str | dict]]", message.content)
if isinstance(message, AIMessageV1):
return AIMessage(
content=content,
id=message.id,
name=message.name,
tool_calls=message.tool_calls,
response_metadata=cast("dict", message.response_metadata),
)
if isinstance(message, AIMessageChunkV1):
return AIMessageChunk(
content=content,
id=message.id,
name=message.name,
tool_call_chunks=message.tool_call_chunks,
response_metadata=cast("dict", message.response_metadata),
)
if isinstance(message, HumanMessageV1):
return HumanMessage(
content=content,
id=message.id,
name=message.name,
)
if isinstance(message, SystemMessageV1):
return SystemMessage(
content=content,
id=message.id,
)
if isinstance(message, ToolMessageV1):
return ToolMessage(
content=content,
id=message.id,
tool_call_id=message.tool_call_id,
artifact=message.artifact,
name=message.name,
status=message.status,
)
message = f"Unsupported message type: {type(message)}"
raise NotImplementedError(message)
def _convert_to_message(message: MessageLikeRepresentation) -> BaseMessage:
"""Instantiate a message from a variety of message formats.
@@ -341,6 +472,143 @@ def _convert_to_message(message: MessageLikeRepresentation) -> BaseMessage:
message_ = _create_message_from_message_type(
msg_type, msg_content, **msg_kwargs
)
elif isinstance(message, MessageV1Types):
message_ = convert_from_v1_message(message)
else:
msg = f"Unsupported message type: {type(message)}"
msg = create_message(message=msg, error_code=ErrorCode.MESSAGE_COERCION_FAILURE)
raise NotImplementedError(msg)
return message_
def _convert_from_v0_to_v1(message: BaseMessage) -> MessageV1:
"""Convert a v0 message to a v1 message."""
if isinstance(message, HumanMessage): # Checking for v0 HumanMessage
return HumanMessageV1(message.content, id=message.id, name=message.name) # type: ignore[arg-type]
if isinstance(message, AIMessage): # Checking for v0 AIMessage
return AIMessageV1(
content=message.content, # type: ignore[arg-type]
id=message.id,
name=message.name,
lc_version="v1",
response_metadata=message.response_metadata, # type: ignore[arg-type]
usage_metadata=message.usage_metadata,
tool_calls=message.tool_calls,
invalid_tool_calls=message.invalid_tool_calls,
)
if isinstance(message, SystemMessage): # Checking for v0 SystemMessage
return SystemMessageV1(
message.content, # type: ignore[arg-type]
id=message.id,
name=message.name,
)
if isinstance(message, ToolMessage): # Checking for v0 ToolMessage
return ToolMessageV1(
message.content, # type: ignore[arg-type]
message.tool_call_id,
id=message.id,
name=message.name,
artifact=message.artifact,
status=message.status,
)
msg = f"Unsupported v0 message type for conversion to v1: {type(message)}"
raise NotImplementedError(msg)
def _safe_convert_from_v0_to_v1(message: BaseMessage) -> MessageV1:
"""Convert a v0 message to a v1 message."""
from langchain_core.messages.content_blocks import create_text_block
if isinstance(message, HumanMessage): # Checking for v0 HumanMessage
content: list[ContentBlock] = [create_text_block(str(message.content))]
return HumanMessageV1(content, id=message.id, name=message.name)
if isinstance(message, AIMessage): # Checking for v0 AIMessage
content = [create_text_block(str(message.content))]
# Construct ResponseMetadata TypedDict from v0 response_metadata dict
# Since ResponseMetadata has total=False, we can safely cast the dict
response_metadata = cast("ResponseMetadata", message.response_metadata or {})
return AIMessageV1(
content=content,
id=message.id,
name=message.name,
lc_version="v1",
response_metadata=response_metadata,
usage_metadata=message.usage_metadata,
tool_calls=message.tool_calls,
invalid_tool_calls=message.invalid_tool_calls,
)
if isinstance(message, SystemMessage): # Checking for v0 SystemMessage
content = [create_text_block(str(message.content))]
return SystemMessageV1(content=content, id=message.id, name=message.name)
if isinstance(message, ToolMessage): # Checking for v0 ToolMessage
content = [create_text_block(str(message.content))]
return ToolMessageV1(
content,
message.tool_call_id,
id=message.id,
name=message.name,
artifact=message.artifact,
status=message.status,
)
msg = f"Unsupported v0 message type for conversion to v1: {type(message)}"
raise NotImplementedError(msg)
def _convert_to_message_v1(message: MessageLikeRepresentation) -> MessageV1:
"""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}")
- dict: a message dict with role and content keys
- string: shorthand for ("human", template); e.g., "{user_input}"
Args:
message: a representation of a message in one of the supported formats.
Returns:
an instance of a message or a message template.
Raises:
NotImplementedError: if the message type is not supported.
ValueError: if the message dict does not contain the required keys.
"""
if isinstance(message, MessageV1Types):
if isinstance(message, AIMessageChunkV1):
message_: MessageV1 = message.to_message()
else:
message_ = message
elif isinstance(message, BaseMessage):
# Convert v0 messages to v1 messages
message_ = _convert_from_v0_to_v1(message)
elif isinstance(message, str):
message_ = _create_message_from_message_type_v1("human", message)
elif isinstance(message, Sequence) and len(message) == 2:
# mypy doesn't realise this can't be a string given the previous branch
message_type_str, template = message # type: ignore[misc]
message_ = _create_message_from_message_type_v1(message_type_str, template)
elif isinstance(message, dict):
msg_kwargs = message.copy()
try:
try:
msg_type = msg_kwargs.pop("role")
except KeyError:
msg_type = msg_kwargs.pop("type")
# None msg content is not allowed
msg_content = msg_kwargs.pop("content") or ""
except KeyError as e:
msg = f"Message dict must contain 'role' and 'content' keys, got {message}"
msg = create_message(
message=msg, error_code=ErrorCode.MESSAGE_COERCION_FAILURE
)
raise ValueError(msg) from e
message_ = _create_message_from_message_type_v1(
msg_type, msg_content, **msg_kwargs
)
else:
msg = f"Unsupported message type: {type(message)}"
msg = create_message(message=msg, error_code=ErrorCode.MESSAGE_COERCION_FAILURE)
@@ -368,6 +636,25 @@ def convert_to_messages(
return [_convert_to_message(m) for m in messages]
def convert_to_messages_v1(
messages: Union[Iterable[MessageLikeRepresentation], PromptValue],
) -> list[MessageV1]:
"""Convert a sequence of messages to a list of messages.
Args:
messages: Sequence of messages to convert.
Returns:
list of messages (BaseMessages).
"""
# Import here to avoid circular imports
from langchain_core.prompt_values import PromptValue
if isinstance(messages, PromptValue):
return messages.to_messages(message_version="v1")
return [_convert_to_message_v1(m) for m in messages]
def _runnable_support(func: Callable) -> Callable:
@overload
def wrapped(
@@ -656,22 +943,23 @@ def trim_messages(
properties:
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"`.
In addition, generally a `ToolMessage` can only appear after an `AIMessage`
history starts with either (1) a ``HumanMessage`` or (2) a ``SystemMessage`` 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"`.
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
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
the first message in the history if present. To achieve this set the
`include_system=True`.
``include_system=True``.
**Note** The examples below show how to configure `trim_messages` to achieve
a behavior consistent with the above properties.
.. note::
The examples below show how to configure ``trim_messages`` to achieve a behavior
consistent with the above properties.
Args:
messages: Sequence of Message-like objects to trim.
@@ -1007,10 +1295,11 @@ def convert_to_openai_messages(
oai_messages: list = []
if is_single := isinstance(messages, (BaseMessage, dict, str)):
if is_single := isinstance(messages, (BaseMessage, dict, str, MessageV1Types)):
messages = [messages]
messages = convert_to_messages(messages)
# TODO: resolve type ignore here
messages = convert_to_messages(messages) # type: ignore[arg-type]
for i, message in enumerate(messages):
oai_msg: dict = {"role": _get_message_openai_role(message)}
@@ -1580,26 +1869,26 @@ def count_tokens_approximately(
chars_per_token: Number of characters per token to use for the approximation.
Default is 4 (one token corresponds to ~4 chars for common English text).
You can also specify float values for more fine-grained control.
See more here: https://platform.openai.com/tokenizer
`See more here. <https://platform.openai.com/tokenizer>`__
extra_tokens_per_message: Number of extra tokens to add per message.
Default is 3 (special tokens, including beginning/end of message).
You can also specify float values for more fine-grained control.
See more here:
https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
`See more here. <https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb>`__
count_name: Whether to include message names in the count.
Enabled by default.
Returns:
Approximate number of tokens in the messages.
Note:
This is a simple approximation that may not match the exact token count
used by specific models. For accurate counts, use model-specific tokenizers.
.. note::
This is a simple approximation that may not match the exact token count used by
specific models. For accurate counts, use model-specific tokenizers.
Warning:
This function does not currently support counting image tokens.
.. versionadded:: 0.3.46
"""
token_count = 0.0
for message in convert_to_messages(messages):

View File

@@ -11,6 +11,7 @@ from typing import (
Optional,
TypeVar,
Union,
cast,
)
from typing_extensions import override
@@ -20,19 +21,22 @@ from langchain_core.messages import AnyMessage, BaseMessage
from langchain_core.outputs import ChatGeneration, Generation
from langchain_core.runnables import Runnable, RunnableConfig, RunnableSerializable
from langchain_core.runnables.config import run_in_executor
from langchain_core.v1.messages import AIMessage, MessageV1, MessageV1Types
if TYPE_CHECKING:
from langchain_core.prompt_values import PromptValue
T = TypeVar("T")
OutputParserLike = Runnable[LanguageModelOutput, T]
OutputParserLike = Runnable[Union[LanguageModelOutput, AIMessage], T]
class BaseLLMOutputParser(ABC, Generic[T]):
"""Abstract base class for parsing the outputs of a model."""
@abstractmethod
def parse_result(self, result: list[Generation], *, partial: bool = False) -> T:
def parse_result(
self, result: Union[list[Generation], AIMessage], *, partial: bool = False
) -> T:
"""Parse a list of candidate model Generations into a specific format.
Args:
@@ -46,7 +50,7 @@ class BaseLLMOutputParser(ABC, Generic[T]):
"""
async def aparse_result(
self, result: list[Generation], *, partial: bool = False
self, result: Union[list[Generation], AIMessage], *, partial: bool = False
) -> T:
"""Async parse a list of candidate model Generations into a specific format.
@@ -71,7 +75,7 @@ class BaseGenerationOutputParser(
@override
def InputType(self) -> Any:
"""Return the input type for the parser."""
return Union[str, AnyMessage]
return Union[str, AnyMessage, MessageV1]
@property
@override
@@ -84,7 +88,7 @@ class BaseGenerationOutputParser(
@override
def invoke(
self,
input: Union[str, BaseMessage],
input: Union[str, BaseMessage, MessageV1],
config: Optional[RunnableConfig] = None,
**kwargs: Any,
) -> T:
@@ -97,9 +101,16 @@ class BaseGenerationOutputParser(
config,
run_type="parser",
)
if isinstance(input, MessageV1Types):
return self._call_with_config(
lambda inner_input: self.parse_result(inner_input),
input,
config,
run_type="parser",
)
return self._call_with_config(
lambda inner_input: self.parse_result([Generation(text=inner_input)]),
input,
cast("str", input),
config,
run_type="parser",
)
@@ -120,6 +131,13 @@ class BaseGenerationOutputParser(
config,
run_type="parser",
)
if isinstance(input, MessageV1Types):
return await self._acall_with_config(
lambda inner_input: self.aparse_result(inner_input),
input,
config,
run_type="parser",
)
return await self._acall_with_config(
lambda inner_input: self.aparse_result([Generation(text=inner_input)]),
input,
@@ -129,7 +147,7 @@ class BaseGenerationOutputParser(
class BaseOutputParser(
BaseLLMOutputParser, RunnableSerializable[LanguageModelOutput, T]
BaseLLMOutputParser, RunnableSerializable[Union[LanguageModelOutput, AIMessage], T]
):
"""Base class to parse the output of an LLM call.
@@ -162,7 +180,7 @@ class BaseOutputParser(
@override
def InputType(self) -> Any:
"""Return the input type for the parser."""
return Union[str, AnyMessage]
return Union[str, AnyMessage, MessageV1]
@property
@override
@@ -189,7 +207,7 @@ class BaseOutputParser(
@override
def invoke(
self,
input: Union[str, BaseMessage],
input: Union[str, BaseMessage, MessageV1],
config: Optional[RunnableConfig] = None,
**kwargs: Any,
) -> T:
@@ -202,9 +220,16 @@ class BaseOutputParser(
config,
run_type="parser",
)
if isinstance(input, MessageV1Types):
return self._call_with_config(
lambda inner_input: self.parse_result(inner_input),
input,
config,
run_type="parser",
)
return self._call_with_config(
lambda inner_input: self.parse_result([Generation(text=inner_input)]),
input,
cast("str", input),
config,
run_type="parser",
)
@@ -212,7 +237,7 @@ class BaseOutputParser(
@override
async def ainvoke(
self,
input: Union[str, BaseMessage],
input: Union[str, BaseMessage, MessageV1],
config: Optional[RunnableConfig] = None,
**kwargs: Optional[Any],
) -> T:
@@ -225,15 +250,24 @@ class BaseOutputParser(
config,
run_type="parser",
)
if isinstance(input, MessageV1Types):
return await self._acall_with_config(
lambda inner_input: self.aparse_result(inner_input),
input,
config,
run_type="parser",
)
return await self._acall_with_config(
lambda inner_input: self.aparse_result([Generation(text=inner_input)]),
input,
cast("str", input),
config,
run_type="parser",
)
@override
def parse_result(self, result: list[Generation], *, partial: bool = False) -> T:
def parse_result(
self, result: Union[list[Generation], AIMessage], *, partial: bool = False
) -> T:
"""Parse a list of candidate model Generations into a specific format.
The return value is parsed from only the first Generation in the result, which
@@ -248,6 +282,8 @@ class BaseOutputParser(
Returns:
Structured output.
"""
if isinstance(result, AIMessage):
return self.parse(result.text)
return self.parse(result[0].text)
@abstractmethod
@@ -262,7 +298,7 @@ class BaseOutputParser(
"""
async def aparse_result(
self, result: list[Generation], *, partial: bool = False
self, result: Union[list[Generation], AIMessage], *, partial: bool = False
) -> T:
"""Async parse a list of candidate model Generations into a specific format.

View File

@@ -21,6 +21,7 @@ from langchain_core.utils.json import (
parse_json_markdown,
parse_partial_json,
)
from langchain_core.v1.messages import AIMessage
# Union type needs to be last assignment to PydanticBaseModel to make mypy happy.
PydanticBaseModel = Union[BaseModel, pydantic.BaseModel]
@@ -53,7 +54,9 @@ class JsonOutputParser(BaseCumulativeTransformOutputParser[Any]):
return pydantic_object.schema()
return None
def parse_result(self, result: list[Generation], *, partial: bool = False) -> Any:
def parse_result(
self, result: Union[list[Generation], AIMessage], *, partial: bool = False
) -> Any:
"""Parse the result of an LLM call to a JSON object.
Args:
@@ -70,7 +73,7 @@ class JsonOutputParser(BaseCumulativeTransformOutputParser[Any]):
Raises:
OutputParserException: If the output is not valid JSON.
"""
text = result[0].text
text = result.text if isinstance(result, AIMessage) else result[0].text
text = text.strip()
if partial:
try:

View File

@@ -13,6 +13,7 @@ from typing_extensions import override
from langchain_core.messages import BaseMessage
from langchain_core.output_parsers.transform import BaseTransformOutputParser
from langchain_core.v1.messages import AIMessage
if TYPE_CHECKING:
from collections.abc import AsyncIterator, Iterator
@@ -71,7 +72,7 @@ class ListOutputParser(BaseTransformOutputParser[list[str]]):
@override
def _transform(
self, input: Iterator[Union[str, BaseMessage]]
self, input: Iterator[Union[str, BaseMessage, AIMessage]]
) -> Iterator[list[str]]:
buffer = ""
for chunk in input:
@@ -81,6 +82,8 @@ class ListOutputParser(BaseTransformOutputParser[list[str]]):
if not isinstance(chunk_content, str):
continue
buffer += chunk_content
elif isinstance(chunk, AIMessage):
buffer += chunk.text
else:
# add current chunk to buffer
buffer += chunk
@@ -105,7 +108,7 @@ class ListOutputParser(BaseTransformOutputParser[list[str]]):
@override
async def _atransform(
self, input: AsyncIterator[Union[str, BaseMessage]]
self, input: AsyncIterator[Union[str, BaseMessage, AIMessage]]
) -> AsyncIterator[list[str]]:
buffer = ""
async for chunk in input:
@@ -115,6 +118,8 @@ class ListOutputParser(BaseTransformOutputParser[list[str]]):
if not isinstance(chunk_content, str):
continue
buffer += chunk_content
elif isinstance(chunk, AIMessage):
buffer += chunk.text
else:
# add current chunk to buffer
buffer += chunk

View File

@@ -17,6 +17,7 @@ from langchain_core.output_parsers import (
)
from langchain_core.output_parsers.json import parse_partial_json
from langchain_core.outputs import ChatGeneration, Generation
from langchain_core.v1.messages import AIMessage
class OutputFunctionsParser(BaseGenerationOutputParser[Any]):
@@ -26,7 +27,9 @@ class OutputFunctionsParser(BaseGenerationOutputParser[Any]):
"""Whether to only return the arguments to the function call."""
@override
def parse_result(self, result: list[Generation], *, partial: bool = False) -> Any:
def parse_result(
self, result: Union[list[Generation], AIMessage], *, partial: bool = False
) -> Any:
"""Parse the result of an LLM call to a JSON object.
Args:
@@ -39,6 +42,12 @@ class OutputFunctionsParser(BaseGenerationOutputParser[Any]):
Raises:
OutputParserException: If the output is not valid JSON.
"""
if isinstance(result, AIMessage):
msg = (
"This output parser does not support v1 AIMessages. Use "
"JsonOutputToolsParser instead."
)
raise TypeError(msg)
generation = result[0]
if not isinstance(generation, ChatGeneration):
msg = "This output parser can only be used with a chat generation."
@@ -77,7 +86,9 @@ class JsonOutputFunctionsParser(BaseCumulativeTransformOutputParser[Any]):
def _diff(self, prev: Optional[Any], next: Any) -> Any:
return jsonpatch.make_patch(prev, next).patch
def parse_result(self, result: list[Generation], *, partial: bool = False) -> Any:
def parse_result(
self, result: Union[list[Generation], AIMessage], *, partial: bool = False
) -> Any:
"""Parse the result of an LLM call to a JSON object.
Args:
@@ -90,6 +101,12 @@ class JsonOutputFunctionsParser(BaseCumulativeTransformOutputParser[Any]):
Raises:
OutputParserException: If the output is not valid JSON.
"""
if isinstance(result, AIMessage):
msg = (
"This output parser does not support v1 AIMessages. Use "
"JsonOutputToolsParser instead."
)
raise TypeError(msg)
if len(result) != 1:
msg = f"Expected exactly one result, but got {len(result)}"
raise OutputParserException(msg)
@@ -160,7 +177,9 @@ class JsonKeyOutputFunctionsParser(JsonOutputFunctionsParser):
key_name: str
"""The name of the key to return."""
def parse_result(self, result: list[Generation], *, partial: bool = False) -> Any:
def parse_result(
self, result: Union[list[Generation], AIMessage], *, partial: bool = False
) -> Any:
"""Parse the result of an LLM call to a JSON object.
Args:
@@ -254,7 +273,9 @@ class PydanticOutputFunctionsParser(OutputFunctionsParser):
return values
@override
def parse_result(self, result: list[Generation], *, partial: bool = False) -> Any:
def parse_result(
self, result: Union[list[Generation], AIMessage], *, partial: bool = False
) -> Any:
"""Parse the result of an LLM call to a JSON object.
Args:
@@ -294,7 +315,9 @@ class PydanticAttrOutputFunctionsParser(PydanticOutputFunctionsParser):
"""The name of the attribute to return."""
@override
def parse_result(self, result: list[Generation], *, partial: bool = False) -> Any:
def parse_result(
self, result: Union[list[Generation], AIMessage], *, partial: bool = False
) -> Any:
"""Parse the result of an LLM call to a JSON object.
Args:

View File

@@ -4,7 +4,7 @@ import copy
import json
import logging
from json import JSONDecodeError
from typing import Annotated, Any, Optional
from typing import Annotated, Any, Optional, Union
from pydantic import SkipValidation, ValidationError
@@ -16,6 +16,7 @@ from langchain_core.output_parsers.transform import BaseCumulativeTransformOutpu
from langchain_core.outputs import ChatGeneration, Generation
from langchain_core.utils.json import parse_partial_json
from langchain_core.utils.pydantic import TypeBaseModel
from langchain_core.v1.messages import AIMessage as AIMessageV1
logger = logging.getLogger(__name__)
@@ -156,7 +157,9 @@ class JsonOutputToolsParser(BaseCumulativeTransformOutputParser[Any]):
If no tool calls are found, None will be returned.
"""
def parse_result(self, result: list[Generation], *, partial: bool = False) -> Any:
def parse_result(
self, result: Union[list[Generation], AIMessageV1], *, partial: bool = False
) -> Any:
"""Parse the result of an LLM call to a list of tool calls.
Args:
@@ -173,31 +176,45 @@ class JsonOutputToolsParser(BaseCumulativeTransformOutputParser[Any]):
Raises:
OutputParserException: If the output is not valid JSON.
"""
generation = result[0]
if not isinstance(generation, ChatGeneration):
msg = "This output parser can only be used with a chat generation."
raise OutputParserException(msg)
message = generation.message
if isinstance(message, AIMessage) and message.tool_calls:
tool_calls = [dict(tc) for tc in message.tool_calls]
if isinstance(result, list):
generation = result[0]
if not isinstance(generation, ChatGeneration):
msg = (
"This output parser can only be used with a chat generation or "
"v1 AIMessage."
)
raise OutputParserException(msg)
message = generation.message
if isinstance(message, AIMessage) and message.tool_calls:
tool_calls = [dict(tc) for tc in message.tool_calls]
for tool_call in tool_calls:
if not self.return_id:
_ = tool_call.pop("id")
else:
try:
raw_tool_calls = copy.deepcopy(
message.additional_kwargs["tool_calls"]
)
except KeyError:
return []
tool_calls = parse_tool_calls(
raw_tool_calls,
partial=partial,
strict=self.strict,
return_id=self.return_id,
)
elif result.tool_calls:
# v1 message
tool_calls = [dict(tc) for tc in result.tool_calls]
for tool_call in tool_calls:
if not self.return_id:
_ = tool_call.pop("id")
else:
try:
raw_tool_calls = copy.deepcopy(message.additional_kwargs["tool_calls"])
except KeyError:
return []
tool_calls = parse_tool_calls(
raw_tool_calls,
partial=partial,
strict=self.strict,
return_id=self.return_id,
)
return []
# for backwards compatibility
for tc in tool_calls:
tc["type"] = tc.pop("name")
if self.first_tool_only:
return tool_calls[0] if tool_calls else None
return tool_calls
@@ -220,7 +237,9 @@ class JsonOutputKeyToolsParser(JsonOutputToolsParser):
key_name: str
"""The type of tools to return."""
def parse_result(self, result: list[Generation], *, partial: bool = False) -> Any:
def parse_result(
self, result: Union[list[Generation], AIMessageV1], *, partial: bool = False
) -> Any:
"""Parse the result of an LLM call to a list of tool calls.
Args:
@@ -234,32 +253,47 @@ class JsonOutputKeyToolsParser(JsonOutputToolsParser):
Returns:
The parsed tool calls.
"""
generation = result[0]
if not isinstance(generation, ChatGeneration):
msg = "This output parser can only be used with a chat generation."
raise OutputParserException(msg)
message = generation.message
if isinstance(message, AIMessage) and message.tool_calls:
parsed_tool_calls = [dict(tc) for tc in message.tool_calls]
if isinstance(result, list):
generation = result[0]
if not isinstance(generation, ChatGeneration):
msg = "This output parser can only be used with a chat generation."
raise OutputParserException(msg)
message = generation.message
if isinstance(message, AIMessage) and message.tool_calls:
parsed_tool_calls = [dict(tc) for tc in message.tool_calls]
for tool_call in parsed_tool_calls:
if not self.return_id:
_ = tool_call.pop("id")
else:
try:
raw_tool_calls = copy.deepcopy(
message.additional_kwargs["tool_calls"]
)
except KeyError:
if self.first_tool_only:
return None
return []
parsed_tool_calls = parse_tool_calls(
raw_tool_calls,
partial=partial,
strict=self.strict,
return_id=self.return_id,
)
elif result.tool_calls:
# v1 message
parsed_tool_calls = [dict(tc) for tc in result.tool_calls]
for tool_call in parsed_tool_calls:
if not self.return_id:
_ = tool_call.pop("id")
else:
try:
raw_tool_calls = copy.deepcopy(message.additional_kwargs["tool_calls"])
except KeyError:
if self.first_tool_only:
return None
return []
parsed_tool_calls = parse_tool_calls(
raw_tool_calls,
partial=partial,
strict=self.strict,
return_id=self.return_id,
)
if self.first_tool_only:
return None
return []
# For backwards compatibility
for tc in parsed_tool_calls:
tc["type"] = tc.pop("name")
if self.first_tool_only:
parsed_result = list(
filter(lambda x: x["type"] == self.key_name, parsed_tool_calls)
@@ -299,7 +333,9 @@ class PydanticToolsParser(JsonOutputToolsParser):
# TODO: Support more granular streaming of objects. Currently only streams once all
# Pydantic object fields are present.
def parse_result(self, result: list[Generation], *, partial: bool = False) -> Any:
def parse_result(
self, result: Union[list[Generation], AIMessageV1], *, partial: bool = False
) -> Any:
"""Parse the result of an LLM call to a list of Pydantic objects.
Args:
@@ -337,12 +373,19 @@ class PydanticToolsParser(JsonOutputToolsParser):
except (ValidationError, ValueError):
if partial:
continue
has_max_tokens_stop_reason = any(
generation.message.response_metadata.get("stop_reason")
== "max_tokens"
for generation in result
if isinstance(generation, ChatGeneration)
)
has_max_tokens_stop_reason = False
if isinstance(result, list):
has_max_tokens_stop_reason = any(
generation.message.response_metadata.get("stop_reason")
== "max_tokens"
for generation in result
if isinstance(generation, ChatGeneration)
)
else:
# v1 message
has_max_tokens_stop_reason = (
result.response_metadata.get("stop_reason") == "max_tokens"
)
if has_max_tokens_stop_reason:
logger.exception(_MAX_TOKENS_ERROR)
raise

View File

@@ -1,7 +1,7 @@
"""Output parsers using Pydantic."""
import json
from typing import Annotated, Generic, Optional
from typing import Annotated, Generic, Optional, Union
import pydantic
from pydantic import SkipValidation
@@ -14,6 +14,7 @@ from langchain_core.utils.pydantic import (
PydanticBaseModel,
TBaseModel,
)
from langchain_core.v1.messages import AIMessage
class PydanticOutputParser(JsonOutputParser, Generic[TBaseModel]):
@@ -43,7 +44,7 @@ class PydanticOutputParser(JsonOutputParser, Generic[TBaseModel]):
return OutputParserException(msg, llm_output=json_string)
def parse_result(
self, result: list[Generation], *, partial: bool = False
self, result: Union[list[Generation], AIMessage], *, partial: bool = False
) -> Optional[TBaseModel]:
"""Parse the result of an LLM call to a pydantic object.

View File

@@ -20,6 +20,7 @@ from langchain_core.outputs import (
GenerationChunk,
)
from langchain_core.runnables.config import run_in_executor
from langchain_core.v1.messages import AIMessage, AIMessageChunk
if TYPE_CHECKING:
from collections.abc import AsyncIterator, Iterator
@@ -32,23 +33,27 @@ class BaseTransformOutputParser(BaseOutputParser[T]):
def _transform(
self,
input: Iterator[Union[str, BaseMessage]], # noqa: A002
input: Iterator[Union[str, BaseMessage, AIMessage]],
) -> Iterator[T]:
for chunk in input:
if isinstance(chunk, BaseMessage):
yield self.parse_result([ChatGeneration(message=chunk)])
elif isinstance(chunk, AIMessage):
yield self.parse_result(chunk)
else:
yield self.parse_result([Generation(text=chunk)])
async def _atransform(
self,
input: AsyncIterator[Union[str, BaseMessage]], # noqa: A002
input: AsyncIterator[Union[str, BaseMessage, AIMessage]],
) -> AsyncIterator[T]:
async for chunk in input:
if isinstance(chunk, BaseMessage):
yield await run_in_executor(
None, self.parse_result, [ChatGeneration(message=chunk)]
)
elif isinstance(chunk, AIMessage):
yield await run_in_executor(None, self.parse_result, chunk)
else:
yield await run_in_executor(
None, self.parse_result, [Generation(text=chunk)]
@@ -57,7 +62,7 @@ class BaseTransformOutputParser(BaseOutputParser[T]):
@override
def transform(
self,
input: Iterator[Union[str, BaseMessage]],
input: Iterator[Union[str, BaseMessage, AIMessage]],
config: Optional[RunnableConfig] = None,
**kwargs: Any,
) -> Iterator[T]:
@@ -78,7 +83,7 @@ class BaseTransformOutputParser(BaseOutputParser[T]):
@override
async def atransform(
self,
input: AsyncIterator[Union[str, BaseMessage]],
input: AsyncIterator[Union[str, BaseMessage, AIMessage]],
config: Optional[RunnableConfig] = None,
**kwargs: Any,
) -> AsyncIterator[T]:
@@ -125,23 +130,42 @@ class BaseCumulativeTransformOutputParser(BaseTransformOutputParser[T]):
raise NotImplementedError
@override
def _transform(self, input: Iterator[Union[str, BaseMessage]]) -> Iterator[Any]:
def _transform(
self, input: Iterator[Union[str, BaseMessage, AIMessage]]
) -> Iterator[Any]:
prev_parsed = None
acc_gen: Union[GenerationChunk, ChatGenerationChunk, None] = None
acc_gen: Union[GenerationChunk, ChatGenerationChunk, AIMessageChunk, None] = (
None
)
for chunk in input:
chunk_gen: Union[GenerationChunk, ChatGenerationChunk]
chunk_gen: Union[GenerationChunk, ChatGenerationChunk, AIMessageChunk]
if isinstance(chunk, BaseMessageChunk):
chunk_gen = ChatGenerationChunk(message=chunk)
elif isinstance(chunk, BaseMessage):
chunk_gen = ChatGenerationChunk(
message=BaseMessageChunk(**chunk.model_dump())
)
elif isinstance(chunk, AIMessageChunk):
chunk_gen = chunk
elif isinstance(chunk, AIMessage):
chunk_gen = AIMessageChunk(
content=chunk.content,
id=chunk.id,
name=chunk.name,
lc_version=chunk.lc_version,
response_metadata=chunk.response_metadata,
usage_metadata=chunk.usage_metadata,
parsed=chunk.parsed,
)
else:
chunk_gen = GenerationChunk(text=chunk)
acc_gen = chunk_gen if acc_gen is None else acc_gen + chunk_gen # type: ignore[operator]
parsed = self.parse_result([acc_gen], partial=True)
if isinstance(acc_gen, AIMessageChunk):
parsed = self.parse_result(acc_gen, partial=True)
else:
parsed = self.parse_result([acc_gen], partial=True)
if parsed is not None and parsed != prev_parsed:
if self.diff:
yield self._diff(prev_parsed, parsed)
@@ -151,24 +175,41 @@ class BaseCumulativeTransformOutputParser(BaseTransformOutputParser[T]):
@override
async def _atransform(
self, input: AsyncIterator[Union[str, BaseMessage]]
self, input: AsyncIterator[Union[str, BaseMessage, AIMessage]]
) -> AsyncIterator[T]:
prev_parsed = None
acc_gen: Union[GenerationChunk, ChatGenerationChunk, None] = None
acc_gen: Union[GenerationChunk, ChatGenerationChunk, AIMessageChunk, None] = (
None
)
async for chunk in input:
chunk_gen: Union[GenerationChunk, ChatGenerationChunk]
chunk_gen: Union[GenerationChunk, ChatGenerationChunk, AIMessageChunk]
if isinstance(chunk, BaseMessageChunk):
chunk_gen = ChatGenerationChunk(message=chunk)
elif isinstance(chunk, BaseMessage):
chunk_gen = ChatGenerationChunk(
message=BaseMessageChunk(**chunk.model_dump())
)
elif isinstance(chunk, AIMessageChunk):
chunk_gen = chunk
elif isinstance(chunk, AIMessage):
chunk_gen = AIMessageChunk(
content=chunk.content,
id=chunk.id,
name=chunk.name,
lc_version=chunk.lc_version,
response_metadata=chunk.response_metadata,
usage_metadata=chunk.usage_metadata,
parsed=chunk.parsed,
)
else:
chunk_gen = GenerationChunk(text=chunk)
acc_gen = chunk_gen if acc_gen is None else acc_gen + chunk_gen # type: ignore[operator]
parsed = await self.aparse_result([acc_gen], partial=True)
if isinstance(acc_gen, AIMessageChunk):
parsed = await self.aparse_result(acc_gen, partial=True)
else:
parsed = await self.aparse_result([acc_gen], partial=True)
if parsed is not None and parsed != prev_parsed:
if self.diff:
yield await run_in_executor(None, self._diff, prev_parsed, parsed)

View File

@@ -12,8 +12,10 @@ from typing_extensions import override
from langchain_core.exceptions import OutputParserException
from langchain_core.messages import BaseMessage
from langchain_core.messages.utils import convert_from_v1_message
from langchain_core.output_parsers.transform import BaseTransformOutputParser
from langchain_core.runnables.utils import AddableDict
from langchain_core.v1.messages import AIMessage
XML_FORMAT_INSTRUCTIONS = """The output should be formatted as a XML file.
1. Output should conform to the tags below.
@@ -105,23 +107,27 @@ class _StreamingParser:
self.buffer = ""
# yield all events
try:
for event, elem in self.pull_parser.read_events():
if event == "start":
# update current path
self.current_path.append(elem.tag)
self.current_path_has_children = False
elif event == "end":
# remove last element from current path
#
self.current_path.pop()
# yield element
if not self.current_path_has_children:
yield nested_element(self.current_path, elem)
# prevent yielding of parent element
if self.current_path:
self.current_path_has_children = True
else:
self.xml_started = False
for raw_event in self.pull_parser.read_events():
if len(raw_event) <= 1:
continue
event, elem = raw_event
if isinstance(elem, ET.Element):
if event == "start":
# update current path
self.current_path.append(elem.tag)
self.current_path_has_children = False
elif event == "end":
# remove last element from current path
#
self.current_path.pop()
# yield element
if not self.current_path_has_children:
yield nested_element(self.current_path, elem)
# prevent yielding of parent element
if self.current_path:
self.current_path_has_children = True
else:
self.xml_started = False
except xml.etree.ElementTree.ParseError:
# This might be junk at the end of the XML input.
# Let's check whether the current path is empty.
@@ -240,21 +246,28 @@ class XMLOutputParser(BaseTransformOutputParser):
@override
def _transform(
self, input: Iterator[Union[str, BaseMessage]]
self, input: Iterator[Union[str, BaseMessage, AIMessage]]
) -> Iterator[AddableDict]:
streaming_parser = _StreamingParser(self.parser)
for chunk in input:
yield from streaming_parser.parse(chunk)
if isinstance(chunk, AIMessage):
yield from streaming_parser.parse(convert_from_v1_message(chunk))
else:
yield from streaming_parser.parse(chunk)
streaming_parser.close()
@override
async def _atransform(
self, input: AsyncIterator[Union[str, BaseMessage]]
self, input: AsyncIterator[Union[str, BaseMessage, AIMessage]]
) -> AsyncIterator[AddableDict]:
streaming_parser = _StreamingParser(self.parser)
async for chunk in input:
for output in streaming_parser.parse(chunk):
yield output
if isinstance(chunk, AIMessage):
for output in streaming_parser.parse(convert_from_v1_message(chunk)):
yield output
else:
for output in streaming_parser.parse(chunk):
yield output
streaming_parser.close()
def _root_to_dict(self, root: ET.Element) -> dict[str, Union[str, list[Any]]]:

View File

@@ -8,17 +8,65 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import Sequence
from typing import Literal, cast
from typing import Literal, Union, cast
from typing_extensions import TypedDict
from typing_extensions import TypedDict, overload
from langchain_core.load.serializable import Serializable
from langchain_core.messages import (
AIMessage,
AnyMessage,
BaseMessage,
HumanMessage,
SystemMessage,
ToolMessage,
get_buffer_string,
)
from langchain_core.messages import content_blocks as types
from langchain_core.v1.messages import AIMessage as AIMessageV1
from langchain_core.v1.messages import HumanMessage as HumanMessageV1
from langchain_core.v1.messages import MessageV1, ResponseMetadata
from langchain_core.v1.messages import SystemMessage as SystemMessageV1
from langchain_core.v1.messages import ToolMessage as ToolMessageV1
def _convert_to_v1(message: BaseMessage) -> MessageV1:
"""Best-effort conversion of a V0 AIMessage to V1."""
if isinstance(message.content, str):
content: list[types.ContentBlock] = []
if message.content:
content = [{"type": "text", "text": message.content}]
else:
content = []
for block in message.content:
if isinstance(block, str):
content.append({"type": "text", "text": block})
elif isinstance(block, dict):
content.append(cast("types.ContentBlock", block))
else:
pass
if isinstance(message, HumanMessage):
return HumanMessageV1(content=content)
if isinstance(message, AIMessage):
for tool_call in message.tool_calls:
content.append(tool_call)
return AIMessageV1(
content=content,
usage_metadata=message.usage_metadata,
response_metadata=cast("ResponseMetadata", message.response_metadata),
tool_calls=message.tool_calls,
)
if isinstance(message, SystemMessage):
return SystemMessageV1(content=content)
if isinstance(message, ToolMessage):
return ToolMessageV1(
tool_call_id=message.tool_call_id,
content=content,
artifact=message.artifact,
)
error_message = f"Unsupported message type: {type(message)}"
raise TypeError(error_message)
class PromptValue(Serializable, ABC):
@@ -46,8 +94,18 @@ class PromptValue(Serializable, ABC):
def to_string(self) -> str:
"""Return prompt value as string."""
@overload
def to_messages(
self, message_version: Literal["v0"] = "v0"
) -> list[BaseMessage]: ...
@overload
def to_messages(self, message_version: Literal["v1"]) -> list[MessageV1]: ...
@abstractmethod
def to_messages(self) -> list[BaseMessage]:
def to_messages(
self, message_version: Literal["v0", "v1"] = "v0"
) -> Union[Sequence[BaseMessage], Sequence[MessageV1]]:
"""Return prompt as a list of Messages."""
@@ -71,8 +129,20 @@ class StringPromptValue(PromptValue):
"""Return prompt as string."""
return self.text
def to_messages(self) -> list[BaseMessage]:
@overload
def to_messages(
self, message_version: Literal["v0"] = "v0"
) -> list[BaseMessage]: ...
@overload
def to_messages(self, message_version: Literal["v1"]) -> list[MessageV1]: ...
def to_messages(
self, message_version: Literal["v0", "v1"] = "v0"
) -> Union[Sequence[BaseMessage], Sequence[MessageV1]]:
"""Return prompt as messages."""
if message_version == "v1":
return [HumanMessageV1(content=self.text)]
return [HumanMessage(content=self.text)]
@@ -89,8 +159,24 @@ class ChatPromptValue(PromptValue):
"""Return prompt as string."""
return get_buffer_string(self.messages)
def to_messages(self) -> list[BaseMessage]:
"""Return prompt as a list of messages."""
@overload
def to_messages(
self, message_version: Literal["v0"] = "v0"
) -> list[BaseMessage]: ...
@overload
def to_messages(self, message_version: Literal["v1"]) -> list[MessageV1]: ...
def to_messages(
self, message_version: Literal["v0", "v1"] = "v0"
) -> Union[Sequence[BaseMessage], Sequence[MessageV1]]:
"""Return prompt as a list of messages.
Args:
message_version: The output version, either "v0" (default) or "v1".
"""
if message_version == "v1":
return [_convert_to_v1(m) for m in self.messages]
return list(self.messages)
@classmethod
@@ -125,8 +211,26 @@ class ImagePromptValue(PromptValue):
"""Return prompt (image URL) as string."""
return self.image_url["url"]
def to_messages(self) -> list[BaseMessage]:
@overload
def to_messages(
self, message_version: Literal["v0"] = "v0"
) -> list[BaseMessage]: ...
@overload
def to_messages(self, message_version: Literal["v1"]) -> list[MessageV1]: ...
def to_messages(
self, message_version: Literal["v0", "v1"] = "v0"
) -> Union[Sequence[BaseMessage], Sequence[MessageV1]]:
"""Return prompt (image URL) as messages."""
if message_version == "v1":
block: types.ImageContentBlock = {
"type": "image",
"url": self.image_url["url"],
}
if "detail" in self.image_url:
block["detail"] = self.image_url["detail"]
return [HumanMessageV1(content=[block])]
return [HumanMessage(content=[cast("dict", self.image_url)])]

File diff suppressed because it is too large Load Diff

View File

@@ -402,7 +402,7 @@ def call_func_with_variable_args(
Callable[[Input, CallbackManagerForChainRun], Output],
Callable[[Input, CallbackManagerForChainRun, RunnableConfig], Output],
],
input: Input, # noqa: A002
input: Input,
config: RunnableConfig,
run_manager: Optional[CallbackManagerForChainRun] = None,
**kwargs: Any,
@@ -439,7 +439,7 @@ def acall_func_with_variable_args(
Awaitable[Output],
],
],
input: Input, # noqa: A002
input: Input,
config: RunnableConfig,
run_manager: Optional[AsyncCallbackManagerForChainRun] = None,
**kwargs: Any,

View File

@@ -5,7 +5,7 @@ import inspect
import typing
from collections.abc import AsyncIterator, Iterator, Sequence
from functools import wraps
from typing import TYPE_CHECKING, Any, Optional, Union
from typing import TYPE_CHECKING, Any, Optional, Union, cast
from pydantic import BaseModel, ConfigDict
from typing_extensions import override
@@ -397,7 +397,7 @@ class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
)
)
to_return = {}
to_return: dict[int, Union[Output, BaseException]] = {}
run_again = dict(enumerate(inputs))
handled_exceptions: dict[int, BaseException] = {}
first_to_raise = None
@@ -447,7 +447,7 @@ class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
if not return_exceptions and sorted_handled_exceptions:
raise sorted_handled_exceptions[0][1]
to_return.update(handled_exceptions)
return [output for _, output in sorted(to_return.items())] # type: ignore[misc]
return [cast("Output", output) for _, output in sorted(to_return.items())]
@override
def stream(
@@ -569,7 +569,7 @@ class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
async for chunk in stream:
yield chunk
try:
output = output + chunk
output = output + chunk # type: ignore[operator]
except TypeError:
output = None
except BaseException as e:

View File

@@ -114,7 +114,7 @@ class Node(NamedTuple):
def copy(
self,
*,
id: Optional[str] = None, # noqa: A002
id: Optional[str] = None,
name: Optional[str] = None,
) -> Node:
"""Return a copy of the node with optional new id and name.
@@ -187,7 +187,7 @@ class MermaidDrawMethod(Enum):
def node_data_str(
id: str, # noqa: A002
id: str,
data: Union[type[BaseModel], RunnableType, None],
) -> str:
"""Convert the data of a node to a string.
@@ -328,7 +328,7 @@ class Graph:
def add_node(
self,
data: Union[type[BaseModel], RunnableType, None],
id: Optional[str] = None, # noqa: A002
id: Optional[str] = None,
*,
metadata: Optional[dict[str, Any]] = None,
) -> Node:

View File

@@ -68,13 +68,21 @@ from langchain_core.utils.pydantic import (
is_pydantic_v1_subclass,
is_pydantic_v2_subclass,
)
from langchain_core.v1.messages import ToolMessage as ToolMessageV1
if TYPE_CHECKING:
import uuid
from collections.abc import Sequence
FILTERED_ARGS = ("run_manager", "callbacks")
TOOL_MESSAGE_BLOCK_TYPES = ("text", "image_url", "image", "json", "search_result")
TOOL_MESSAGE_BLOCK_TYPES = (
"text",
"image_url",
"image",
"json",
"search_result",
"custom_tool_call_output",
)
class SchemaAnnotationError(TypeError):
@@ -498,6 +506,15 @@ class ChildTool(BaseTool):
two-tuple corresponding to the (content, artifact) of a ToolMessage.
"""
message_version: Literal["v0", "v1"] = "v0"
"""Version of ToolMessage to return given
:class:`~langchain_core.messages.content_blocks.ToolCall` input.
If ``"v0"``, output will be a v0 :class:`~langchain_core.messages.tool.ToolMessage`.
If ``"v1"``, output will be a v1 :class:`~langchain_core.v1.messages.ToolMessage`.
"""
def __init__(self, **kwargs: Any) -> None:
"""Initialize the tool."""
if (
@@ -835,7 +852,7 @@ class ChildTool(BaseTool):
content = None
artifact = None
status = "success"
status: Literal["success", "error"] = "success"
error_to_raise: Union[Exception, KeyboardInterrupt, None] = None
try:
child_config = patch_config(config, callbacks=run_manager.get_child())
@@ -879,7 +896,14 @@ class ChildTool(BaseTool):
if error_to_raise:
run_manager.on_tool_error(error_to_raise)
raise error_to_raise
output = _format_output(content, artifact, tool_call_id, self.name, status)
output = _format_output(
content,
artifact,
tool_call_id,
self.name,
status,
message_version=self.message_version,
)
run_manager.on_tool_end(output, color=color, name=self.name, **kwargs)
return output
@@ -945,7 +969,7 @@ class ChildTool(BaseTool):
)
content = None
artifact = None
status = "success"
status: Literal["success", "error"] = "success"
error_to_raise: Optional[Union[Exception, KeyboardInterrupt]] = None
try:
tool_args, tool_kwargs = self._to_args_and_kwargs(tool_input, tool_call_id)
@@ -993,7 +1017,14 @@ class ChildTool(BaseTool):
await run_manager.on_tool_error(error_to_raise)
raise error_to_raise
output = _format_output(content, artifact, tool_call_id, self.name, status)
output = _format_output(
content,
artifact,
tool_call_id,
self.name,
status,
message_version=self.message_version,
)
await run_manager.on_tool_end(output, color=color, name=self.name, **kwargs)
return output
@@ -1131,7 +1162,9 @@ def _format_output(
artifact: Any,
tool_call_id: Optional[str],
name: str,
status: str,
status: Literal["success", "error"],
*,
message_version: Literal["v0", "v1"] = "v0",
) -> Union[ToolOutputMixin, Any]:
"""Format tool output as a ToolMessage if appropriate.
@@ -1141,6 +1174,7 @@ def _format_output(
tool_call_id: The ID of the tool call.
name: The name of the tool.
status: The execution status.
message_version: The version of the ToolMessage to return.
Returns:
The formatted output, either as a ToolMessage or the original content.
@@ -1149,7 +1183,15 @@ def _format_output(
return content
if not _is_message_content_type(content):
content = _stringify(content)
return ToolMessage(
if message_version == "v0":
return ToolMessage(
content,
artifact=artifact,
tool_call_id=tool_call_id,
name=name,
status=status,
)
return ToolMessageV1(
content,
artifact=artifact,
tool_call_id=tool_call_id,

View File

@@ -22,6 +22,7 @@ def tool(
response_format: Literal["content", "content_and_artifact"] = "content",
parse_docstring: bool = False,
error_on_invalid_docstring: bool = True,
message_version: Literal["v0", "v1"] = "v0",
) -> Callable[[Union[Callable, Runnable]], BaseTool]: ...
@@ -37,6 +38,7 @@ def tool(
response_format: Literal["content", "content_and_artifact"] = "content",
parse_docstring: bool = False,
error_on_invalid_docstring: bool = True,
message_version: Literal["v0", "v1"] = "v0",
) -> BaseTool: ...
@@ -51,6 +53,7 @@ def tool(
response_format: Literal["content", "content_and_artifact"] = "content",
parse_docstring: bool = False,
error_on_invalid_docstring: bool = True,
message_version: Literal["v0", "v1"] = "v0",
) -> BaseTool: ...
@@ -65,6 +68,7 @@ def tool(
response_format: Literal["content", "content_and_artifact"] = "content",
parse_docstring: bool = False,
error_on_invalid_docstring: bool = True,
message_version: Literal["v0", "v1"] = "v0",
) -> Callable[[Union[Callable, Runnable]], BaseTool]: ...
@@ -79,6 +83,7 @@ def tool(
response_format: Literal["content", "content_and_artifact"] = "content",
parse_docstring: bool = False,
error_on_invalid_docstring: bool = True,
message_version: Literal["v0", "v1"] = "v0",
) -> Union[
BaseTool,
Callable[[Union[Callable, Runnable]], BaseTool],
@@ -118,6 +123,11 @@ def tool(
error_on_invalid_docstring: if ``parse_docstring`` is provided, configure
whether to raise ValueError on invalid Google Style docstrings.
Defaults to True.
message_version: Version of ToolMessage to return given
:class:`~langchain_core.messages.content_blocks.ToolCall` input.
If ``"v0"``, output will be a v0 :class:`~langchain_core.messages.tool.ToolMessage`.
If ``"v1"``, output will be a v1 :class:`~langchain_core.v1.messages.ToolMessage`.
Returns:
The tool.
@@ -216,7 +226,7 @@ def tool(
\"\"\"
return bar
""" # noqa: D214, D410, D411
""" # noqa: D214, D410, D411, E501
def _create_tool_factory(
tool_name: str,
@@ -274,6 +284,7 @@ def tool(
response_format=response_format,
parse_docstring=parse_docstring,
error_on_invalid_docstring=error_on_invalid_docstring,
message_version=message_version,
)
# If someone doesn't want a schema applied, we must treat it as
# a simple string->string function
@@ -290,6 +301,7 @@ def tool(
return_direct=return_direct,
coroutine=coroutine,
response_format=response_format,
message_version=message_version,
)
return _tool_factory
@@ -383,6 +395,7 @@ def convert_runnable_to_tool(
name: Optional[str] = None,
description: Optional[str] = None,
arg_types: Optional[dict[str, type]] = None,
message_version: Literal["v0", "v1"] = "v0",
) -> BaseTool:
"""Convert a Runnable into a BaseTool.
@@ -392,10 +405,15 @@ def convert_runnable_to_tool(
name: The name of the tool. Defaults to None.
description: The description of the tool. Defaults to None.
arg_types: The types of the arguments. Defaults to None.
message_version: Version of ToolMessage to return given
:class:`~langchain_core.messages.content_blocks.ToolCall` input.
If ``"v0"``, output will be a v0 :class:`~langchain_core.messages.tool.ToolMessage`.
If ``"v1"``, output will be a v1 :class:`~langchain_core.v1.messages.ToolMessage`.
Returns:
The tool.
"""
""" # noqa: E501
if args_schema:
runnable = runnable.with_types(input_type=args_schema)
description = description or _get_description_from_runnable(runnable)
@@ -408,6 +426,7 @@ def convert_runnable_to_tool(
func=runnable.invoke,
coroutine=runnable.ainvoke,
description=description,
message_version=message_version,
)
async def ainvoke_wrapper(
@@ -435,4 +454,5 @@ def convert_runnable_to_tool(
coroutine=ainvoke_wrapper,
description=description,
args_schema=args_schema,
message_version=message_version,
)

View File

@@ -72,6 +72,7 @@ def create_retriever_tool(
document_prompt: Optional[BasePromptTemplate] = None,
document_separator: str = "\n\n",
response_format: Literal["content", "content_and_artifact"] = "content",
message_version: Literal["v0", "v1"] = "v1",
) -> Tool:
r"""Create a tool to do retrieval of documents.
@@ -88,10 +89,15 @@ def create_retriever_tool(
"content_and_artifact" then the output is expected to be a two-tuple
corresponding to the (content, artifact) of a ToolMessage (artifact
being a list of documents in this case). Defaults to "content".
message_version: Version of ToolMessage to return given
:class:`~langchain_core.messages.content_blocks.ToolCall` input.
If ``"v0"``, output will be a v0 :class:`~langchain_core.messages.tool.ToolMessage`.
If ``"v1"``, output will be a v1 :class:`~langchain_core.v1.messages.ToolMessage`.
Returns:
Tool class to pass to an agent.
"""
""" # noqa: E501
document_prompt = document_prompt or PromptTemplate.from_template("{page_content}")
func = partial(
_get_relevant_documents,
@@ -114,4 +120,5 @@ def create_retriever_tool(
coroutine=afunc,
args_schema=RetrieverInput,
response_format=response_format,
message_version=message_version,
)

View File

@@ -129,6 +129,7 @@ class StructuredTool(BaseTool):
response_format: Literal["content", "content_and_artifact"] = "content",
parse_docstring: bool = False,
error_on_invalid_docstring: bool = False,
message_version: Literal["v0", "v1"] = "v0",
**kwargs: Any,
) -> StructuredTool:
"""Create tool from a given function.
@@ -157,6 +158,12 @@ class StructuredTool(BaseTool):
error_on_invalid_docstring: if ``parse_docstring`` is provided, configure
whether to raise ValueError on invalid Google Style docstrings.
Defaults to False.
message_version: Version of ToolMessage to return given
:class:`~langchain_core.messages.content_blocks.ToolCall` input.
If ``"v0"``, output will be a v0 :class:`~langchain_core.messages.tool.ToolMessage`.
If ``"v1"``, output will be a v1 :class:`~langchain_core.v1.messages.ToolMessage`.
kwargs: Additional arguments to pass to the tool
Returns:
@@ -175,7 +182,7 @@ class StructuredTool(BaseTool):
tool = StructuredTool.from_function(add)
tool.run(1, 2) # 3
"""
""" # noqa: E501
if func is not None:
source_function = func
elif coroutine is not None:
@@ -232,6 +239,7 @@ class StructuredTool(BaseTool):
description=description_,
return_direct=return_direct,
response_format=response_format,
message_version=message_version,
**kwargs,
)

View File

@@ -17,6 +17,7 @@ from typing_extensions import override
from langchain_core.callbacks.base import AsyncCallbackHandler, BaseCallbackHandler
from langchain_core.exceptions import TracerException # noqa: F401
from langchain_core.tracers.core import _TracerCore
from langchain_core.v1.messages import AIMessage, AIMessageChunk, MessageV1
if TYPE_CHECKING:
from collections.abc import Sequence
@@ -54,7 +55,7 @@ class BaseTracer(_TracerCore, BaseCallbackHandler, ABC):
def on_chat_model_start(
self,
serialized: dict[str, Any],
messages: list[list[BaseMessage]],
messages: Union[list[list[BaseMessage]], list[MessageV1]],
*,
run_id: UUID,
tags: Optional[list[str]] = None,
@@ -138,7 +139,9 @@ class BaseTracer(_TracerCore, BaseCallbackHandler, ABC):
self,
token: str,
*,
chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]] = None,
chunk: Optional[
Union[GenerationChunk, ChatGenerationChunk, AIMessageChunk]
] = None,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
**kwargs: Any,
@@ -190,7 +193,9 @@ class BaseTracer(_TracerCore, BaseCallbackHandler, ABC):
)
@override
def on_llm_end(self, response: LLMResult, *, run_id: UUID, **kwargs: Any) -> Run:
def on_llm_end(
self, response: Union[LLMResult, AIMessage], *, run_id: UUID, **kwargs: Any
) -> Run:
"""End a trace for an LLM run.
Args:
@@ -562,7 +567,7 @@ class AsyncBaseTracer(_TracerCore, AsyncCallbackHandler, ABC):
async def on_chat_model_start(
self,
serialized: dict[str, Any],
messages: list[list[BaseMessage]],
messages: Union[list[list[BaseMessage]], list[MessageV1]],
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
@@ -617,7 +622,9 @@ class AsyncBaseTracer(_TracerCore, AsyncCallbackHandler, ABC):
self,
token: str,
*,
chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]] = None,
chunk: Optional[
Union[GenerationChunk, ChatGenerationChunk, AIMessageChunk]
] = None,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
**kwargs: Any,
@@ -646,7 +653,7 @@ class AsyncBaseTracer(_TracerCore, AsyncCallbackHandler, ABC):
@override
async def on_llm_end(
self,
response: LLMResult,
response: Union[LLMResult, AIMessage],
*,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
@@ -882,7 +889,7 @@ class AsyncBaseTracer(_TracerCore, AsyncCallbackHandler, ABC):
self,
run: Run,
token: str,
chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]],
chunk: Optional[Union[GenerationChunk, ChatGenerationChunk, AIMessageChunk]],
) -> None:
"""Process new LLM token."""

View File

@@ -18,6 +18,7 @@ from typing import (
from langchain_core.exceptions import TracerException
from langchain_core.load import dumpd
from langchain_core.messages.utils import convert_from_v1_message
from langchain_core.outputs import (
ChatGeneration,
ChatGenerationChunk,
@@ -25,6 +26,12 @@ from langchain_core.outputs import (
LLMResult,
)
from langchain_core.tracers.schemas import Run
from langchain_core.v1.messages import (
AIMessage,
AIMessageChunk,
MessageV1,
MessageV1Types,
)
if TYPE_CHECKING:
from collections.abc import Coroutine, Sequence
@@ -156,7 +163,7 @@ class _TracerCore(ABC):
def _create_chat_model_run(
self,
serialized: dict[str, Any],
messages: list[list[BaseMessage]],
messages: Union[list[list[BaseMessage]], list[MessageV1]],
run_id: UUID,
tags: Optional[list[str]] = None,
parent_run_id: Optional[UUID] = None,
@@ -181,6 +188,12 @@ class _TracerCore(ABC):
start_time = datetime.now(timezone.utc)
if metadata:
kwargs.update({"metadata": metadata})
if isinstance(messages[0], MessageV1Types):
# Convert from v1 messages to BaseMessage
messages = [
[convert_from_v1_message(msg) for msg in messages] # type: ignore[arg-type]
]
messages = cast("list[list[BaseMessage]]", messages)
return Run(
id=run_id,
parent_run_id=parent_run_id,
@@ -230,7 +243,9 @@ class _TracerCore(ABC):
self,
token: str,
run_id: UUID,
chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]] = None,
chunk: Optional[
Union[GenerationChunk, ChatGenerationChunk, AIMessageChunk]
] = None,
parent_run_id: Optional[UUID] = None, # noqa: ARG002
) -> Run:
"""Append token event to LLM run and return the run."""
@@ -276,7 +291,15 @@ class _TracerCore(ABC):
)
return llm_run
def _complete_llm_run(self, response: LLMResult, run_id: UUID) -> Run:
def _complete_llm_run(
self, response: Union[LLMResult, AIMessage], run_id: UUID
) -> Run:
if isinstance(response, AIMessage):
response = LLMResult(
generations=[
[ChatGeneration(message=convert_from_v1_message(response))]
]
)
llm_run = self._get_run(run_id, run_type={"llm", "chat_model"})
if getattr(llm_run, "outputs", None) is None:
llm_run.outputs = {}
@@ -558,7 +581,7 @@ class _TracerCore(ABC):
self,
run: Run, # noqa: ARG002
token: str, # noqa: ARG002
chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]], # noqa: ARG002
chunk: Optional[Union[GenerationChunk, ChatGenerationChunk, AIMessageChunk]], # noqa: ARG002
) -> Union[None, Coroutine[Any, Any, None]]:
"""Process new LLM token."""
return None

View File

@@ -38,6 +38,7 @@ from langchain_core.runnables.utils import (
from langchain_core.tracers._streaming import _StreamingCallbackHandler
from langchain_core.tracers.memory_stream import _MemoryStream
from langchain_core.utils.aiter import aclosing, py_anext
from langchain_core.v1.messages import MessageV1
if TYPE_CHECKING:
from collections.abc import AsyncIterator, Iterator, Sequence
@@ -45,6 +46,8 @@ if TYPE_CHECKING:
from langchain_core.documents import Document
from langchain_core.runnables import Runnable, RunnableConfig
from langchain_core.tracers.log_stream import LogEntry
from langchain_core.v1.messages import AIMessage as AIMessageV1
from langchain_core.v1.messages import AIMessageChunk as AIMessageChunkV1
logger = logging.getLogger(__name__)
@@ -297,7 +300,7 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
async def on_chat_model_start(
self,
serialized: dict[str, Any],
messages: list[list[BaseMessage]],
messages: Union[list[list[BaseMessage]], list[MessageV1]],
*,
run_id: UUID,
tags: Optional[list[str]] = None,
@@ -307,6 +310,8 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
**kwargs: Any,
) -> None:
"""Start a trace for an LLM run."""
# below cast is because type is converted in handle_event
messages = cast("list[list[BaseMessage]]", messages)
name_ = _assign_name(name, serialized)
run_type = "chat_model"
@@ -407,13 +412,18 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
self,
token: str,
*,
chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]] = None,
chunk: Optional[
Union[GenerationChunk, ChatGenerationChunk, AIMessageChunkV1]
] = None,
run_id: UUID,
parent_run_id: Optional[UUID] = None,
**kwargs: Any,
) -> None:
"""Run on new LLM token. Only available when streaming is enabled."""
run_info = self.run_map.get(run_id)
chunk = cast(
"Optional[Union[GenerationChunk, ChatGenerationChunk]]", chunk
) # converted in handle_event
chunk_: Union[GenerationChunk, BaseMessageChunk]
if run_info is None:
@@ -456,9 +466,10 @@ class _AstreamEventsCallbackHandler(AsyncCallbackHandler, _StreamingCallbackHand
@override
async def on_llm_end(
self, response: LLMResult, *, run_id: UUID, **kwargs: Any
self, response: Union[LLMResult, AIMessageV1], *, run_id: UUID, **kwargs: Any
) -> None:
"""End a trace for an LLM run."""
response = cast("LLMResult", response) # converted in handle_event
run_info = self.run_map.pop(run_id)
inputs_ = run_info["inputs"]

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
import logging
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, Optional, Union
from typing import TYPE_CHECKING, Any, Optional, Union, cast
from uuid import UUID
from langsmith import Client
@@ -21,12 +21,15 @@ from typing_extensions import override
from langchain_core.env import get_runtime_environment
from langchain_core.load import dumpd
from langchain_core.messages.utils import convert_from_v1_message
from langchain_core.tracers.base import BaseTracer
from langchain_core.tracers.schemas import Run
from langchain_core.v1.messages import MessageV1Types
if TYPE_CHECKING:
from langchain_core.messages import BaseMessage
from langchain_core.outputs import ChatGenerationChunk, GenerationChunk
from langchain_core.v1.messages import AIMessageChunk, MessageV1
logger = logging.getLogger(__name__)
_LOGGED = set()
@@ -113,7 +116,7 @@ class LangChainTracer(BaseTracer):
def on_chat_model_start(
self,
serialized: dict[str, Any],
messages: list[list[BaseMessage]],
messages: Union[list[list[BaseMessage]], list[MessageV1]],
*,
run_id: UUID,
tags: Optional[list[str]] = None,
@@ -140,6 +143,12 @@ class LangChainTracer(BaseTracer):
start_time = datetime.now(timezone.utc)
if metadata:
kwargs.update({"metadata": metadata})
if isinstance(messages[0], MessageV1Types):
# Convert from v1 messages to BaseMessage
messages = [
[convert_from_v1_message(msg) for msg in messages] # type: ignore[arg-type]
]
messages = cast("list[list[BaseMessage]]", messages)
chat_model_run = Run(
id=run_id,
parent_run_id=parent_run_id,
@@ -232,7 +241,9 @@ class LangChainTracer(BaseTracer):
self,
token: str,
run_id: UUID,
chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]] = None,
chunk: Optional[
Union[GenerationChunk, ChatGenerationChunk, AIMessageChunk]
] = None,
parent_run_id: Optional[UUID] = None,
) -> Run:
"""Append token event to LLM run and return the run."""

View File

@@ -34,6 +34,7 @@ if TYPE_CHECKING:
from langchain_core.runnables.utils import Input, Output
from langchain_core.tracers.schemas import Run
from langchain_core.v1.messages import AIMessageChunk
class LogEntry(TypedDict):
@@ -176,7 +177,7 @@ class RunLog(RunLogPatch):
# Then compare that the ops are the same
return super().__eq__(other)
__hash__ = None # type: ignore[assignment]
__hash__ = None
T = TypeVar("T")
@@ -485,7 +486,7 @@ class LogStreamCallbackHandler(BaseTracer, _StreamingCallbackHandler):
self,
run: Run,
token: str,
chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]],
chunk: Optional[Union[GenerationChunk, ChatGenerationChunk, AIMessageChunk]],
) -> None:
"""Process new LLM token."""
index = self._key_map_by_run_id.get(run.id)

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